-
Notifications
You must be signed in to change notification settings - Fork 6
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.
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 therun-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.
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.
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.
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 withnpm 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.
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.
β 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)} />
β 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;
β 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";
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.
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
β 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.
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.
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.