Skip to content

Develop

Lorenzo Cavazzi edited this page Apr 11, 2025 · 20 revisions

Develop πŸ§‘β€πŸ’»

Developing for Renku UI is easy! You will only need a recent version of NodeJS, NPM and Git. You might need a few other tools, depending on which approach you choose.

ℹ️ This page refers to the React-based client and the Cypress tests. There is also an Express-based server, but it's deprecated and will be removed.

Getting started πŸš€

Once you clone the Renku UI repository locally, go into the client folder and install the dependencies with the npm install command. You can now start the Vite server with the command npm run start and the interface will be available at the local URL localhost:3000.

ℹ️ Mind that you need a minimal configuration file in public/config.json. You can run the run-telepresence-sh script once to generate a valid one.

The web interface requires interacting with backend components, and there are two ways to get the API responses you need:

  • Create a full Renkulab deployment for development: this approach is recommended for most scenarios but requires access to the SDSC Kubernetes infrastructure.
  • Mock the responses from the APIs: this is quick and has no requirements, well suited for quick changes but tedious if you need to mock many new responses.

Full deployment πŸ™οΈ

Having a full Renkulab dev deployment is the best way to develop since you have all the infrastructure and can fully use the UI. It is easy and quick to get if you have access to the SDSC Kubernetes infrastructure, available to all the official Renku team members and extendable to other SDSC employees and collaborators.

You can leverage our CI automation on GitHub: create a new PR with at least one commit and include a line with the text /deploy in the description. This will trigger the acceptance-tests workflow that deploys a dev instance of Renkulab reachable at the URL https://renku-ci-ui-<PR_number>.dev.renku.ch/. The process takes a few minutes and Renku Bot will post the exact URL. The deployment can be further customized (renku-action reference).

You need to install locally Telepresence OSS to swap the renku-ui pod with a local instance of the UI; that requires installing Docker and Kubectl too.
Run the run-telepresence script, provide the PR number, and access the instance from the URL you got from Renku Bot. Mind that it takes up to a minute for the new Telepresence-injected pod to start serving pages; until that moment, the page you get won't come from your local code.

When you stop Telepresence, you'll see a 50x page for a short while and then the Kubernetes infrastructure will spin up a new pod serving the UI from the latest commit in the PR.
Mind that Telepresence might get stuck when the connection with your laptop is suddenly lost. If the 50x page persists, you can try to execute telepresence uninstall --all-agents. Wait until a new pristine pod comes online (i.e. the UI loads again) if you need to re-start Telepresence.

ℹ️ Alas, at the moment we don't support out-of-the-box using a public instance like renkulab.io. Deploying all the infrastructure locally or in another Kubernetes cluster is technically feasible but overkill.

Mock responses 🎭

You can use the Cypress tests in the tests folder to mock API responses easily. There are no additional requirements.

See the Test πŸ§ͺ section for further information.

Coding guidelines πŸ“œ

If you wish to contribute new Pull requests, your code must satisfy specific standards we have set to simplify collaboration with the broader community.

  • Stick to our tech stack and use the right tool for the right purpose. This way, we keep our codebase consistent. You can learn about the framework we use in the Architecture page.

  • Run the linter. This points out some bad coding practices, saving time to reviewers and trying to keep out common bugs. We use ESLint with a few standard plugins and some customizations. You can see the status of your code by running npm run lint or integrating it in your IDE of choice.

  • Adhere to our coding rules. Those fill a few gaps that our linting tools cannot easily catch and help keep the codebase consistent to make life easier for developers and reviewers. We have no automation to help with this; reviews will point out inconsistencies.

  • Try to use similar UX patterns. This makes pages consistent and keeps the UI intuitive for the users. When in doubt, check similar components on other pages, access the Storybook section available at <base_URL>/storybook/ with a list of common patterns and examples, or ask to a maintainer to get guidance on the best approach.

  • Format your code. We use Prettier to keep the style as consistent as possible. This helps developers and reviewers, partially avoiding the use too many different flavors of TypeScript. You can run npm run format to format all the files automatically, or use the Husky automation we have in the main folder (install with npm install) to automatically format your code when committing. Prettier also integrates very well with the most common IDEs.

  • Update local tests. We have unit tests and Cypress tests. They ensure complex functions work fine and React components show as expected. Depending on the changes, you might need to add new tests and/or update existing ones. Refer to the Architecture page for further information.

Coding rules πŸ“•

New code should adhere to these rules to ensure code remains maintainable and easy to read as the code base grows.

Keep in mind that older code may not always conform to these guidelines; contributors are encouraged to refactor it.

R001: Use utility functions to create CSS class names

βœ… DO

import cx from "classnames";

const className = cx(
  "rounded",
  disabled && "btn-disabled",
  color && `btn-${color}`
);

❌ DON'T

const className = "rounded" + (disabled ? " disabled" : "");

πŸ’‘ Rationale

Constructing CSS class names by hand leads to frequent mistakes, e.g. having undefined or null as one of the CSS classes of an HTML element.

πŸ’­ Tip

We agreed on using cx in every case, except when there is only one class name as a string.

const padding = someCondition ? "p-2" : "p-3";

<MyComponent className="p-2" />
<MyComponent className={cx("p-2", "text-danger")} />
<MyComponent className={cx(padding)} />

R002: Avoid condition ? true : false

βœ… DO

const isActive = conditionA && x > y && conditionC;

❌ DON'T

const isActive = conditionA && x > y && conditionC ? true : false;

πŸ’‘ Rationale

The ? true : false construct is unnecessary and may surprise the reader, leading to slower code reading.

πŸ’­ Tip

Use double boolean negation to ensure the variable is of type boolean.

const enabled = !!objMaybeUndefined?.field.length;

R003: Avoid nested if/else blocks

βœ… DO

function getStatus(input: Input) {
  if (condA && condB && condC) {
    return "success";
  }
  if (condA && condB) {
    return "fairly good";
  }
  if (condA) {
    return "warning";
  }
  if (condB && condC) {
    return "critical";
  }
  return "not quite there";
}

❌ DON'T

function getStatus(input: Input) {
  if (condA) {
    if (condB) {
      if (condC) {
        return "success";
      } else {
        return "fairly good";
      }
    } else {
      return "warning";
    }
  } else if (condB && condC) {
    return "critical";
  } else {
    return "not quite there";
  }
}

πŸ’‘ Rationale

Nested if/else block are hard to read and follow, especially when they span more than one screen vertically.

If possible, use early returns to flatten the case enumeration. This means in some cases, separating this logic into a utility function is recommended.

When the logic is short, cascading ternary operators can be used, e.g.

const status =
  response === "hello"
    ? "greeting"
    : response === "bye"
    ? "leaving"
    : response.startsWith("says:")
    ? "talking"
    : "idle";

R004: Include a default export when appropriate

Include a default export when a main component is defined in a source file (this should be the usual case).

βœ… DO

export default function MyComponent() {
  return (
    <div>
      <h2>My Component</h2>
      <p>Hello, World!</p>
    </div>
  );
}

πŸ’‘ Rationale

The main component being exported as a default export is a common convention with React projects.

The default export also shows which component is a the top level and which ones are sub-components.

R005: File naming conventions

React Components

Use the component name as a file name. The file name as well as the component name should use CamelCase.

Example: MyComponent.tsx

Type Definitions

Type definition file names start with a lowercase letter and end with .types.ts.

Example: workflows.types.ts

Hook Definitions

Reusable hooks should be defined in their own file, be a default export, use the hook name as a file name and have the extension .hook.ts.

Example: useRenku.hook.ts

Utility Functions

Utility file names start with a lowercase letter and end with .utils.ts.

Example: sessions.utils.ts

API Definitions

Files defining RTK Query endpoints start with a lowercase letter and end with .api.ts.

Example: projects.api.ts

Slice Definitions

Files defining RTK slices start with a lowercase letter and end with .slice.ts.

Example: user.slice.ts

Test Definitions

Test file names start with a lowercase letter and end with .test.ts for unit tests or with .spec.ts for Cypress tests.

Example: login.test.ts or datasets.spec.ts

R006: Use existing Bootstrap classes or import from CSS modules

βœ… DO

<Card className={cx("m-3", "py-2")} />
import styles from "SpecialCard.module.scss";

<Card className={cx(styles.projectCard)} />;

❌ DON'T

import "./SpecialCard.css";

<Card className="my-special-card-class" />;

πŸ’‘ Rationale

We want to avoid an explosion of the number of CSS classes, as well as classes polluting the global namespace. Since everyone knows Bootstrap, it is a good idea to use it as much as possible. Should the need arise for very specific styling, we can always create a new class in a local (S)CSS module file and import it.

πŸ’­ Tip

We want to push for sticking to Bootstrap's utility classes as much as possible. This means we should discuss for deviations from a reference design when that only minimally impacts the interface.

E.G. If a Figma design reference file shows a distance between components of 14.5px and m-3 is 16px, we should use m-3 instead.

R007: Do not perform async actions in React hooks

Do not use .then() or .unwrap() inside React hooks. Instead, listen to the result of the async action and act on the result.

βœ… DO

function MyComponent() {
  const [postRequest, result] = usePostRequest();

  const onClick = useCallback(() => {
    postRequest();
  }, [postRequest]);

  useEffect(() => {
    if (result.isSuccess) {
      // Do something...
    }
  }, [result.isSuccess]);

  return (
    <div>
      <button onClick={onClick}>Some action</button>
    </div>
  );
}

❌ DON'T

function MyComponent() {
  const [postRequest, result] = usePostRequest();

  const onClick = useCallback(() => {
    postRequest()
      .unwrap()
      .then(() => {
        // Do something...
      });
  }, [postRequest]);

  return (
    <div>
      <button onClick={onClick}>Some action</button>
    </div>
  );
}

πŸ’‘ Rationale

Calling code after an async action may happen after a component has re-rendered or has been removed. In this case, if the corresponding code tries to access the component, it will cause an error.

R008: Interactive handlers can only be used on interactive HTML tags

Do not add interactive handlers, e.g. onClick, to HTML tags which are not interactive.

βœ… DO

function MyComponent() {
  return (
    <ul>
      <li>
        <a href="...">
          My Content
        </a>
      <li>
    </ul>
  );
}
function MyComponent() {
  const onClick = ...Some action...;

  return (
    <ul>
      <li>
        <button onClick={onClick}>
          My Content
        </button>
      <li>
    </ul>
  );
}

❌ DON'T

function MyComponent() {
  const onClick = ...Some action...;

  return (
    <ul>
      <li onClick={onClick}>
        My Content
      <li>
    </ul>
  );
}

πŸ’‘ Rationale

Browsers do not expect tags such as <div> or <span> to be interactive, which means they will not get focused with keyboard navigation. Adding interactive handlers to non-interactive tags will therefore hinder accessibility to users which do not have access to a mouse or touch screen.