Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Agent guidelines

Coriolis Web is the web UI for Coriolis, which migrates VMs between
clouds. It is a React 18 + TypeScript app with MobX 5 stores, served by
a small Express server (`server/`). Target Node 22 (CI) and Yarn 3.
The backend lives in a separate repo (`coriolis`). Do not add behaviour
here that the API does not support.

## Coriolis terminology

- **Minion**: temporary worker VM for disk transfer and os-morphing
(guest prep: network, packages, drivers). **Minion pools** reuse them.
- **Transfer**: creates destination volumes and copies disk data.
Re-run a transfer as a new **execution** to pick up later changes.
- **Deployment**: creates the destination VM from a completed transfer.
- **Replica** vs **migration** (`transfer.scenario`: `replica` /
`live_migration`) is a licensing split, not two engines. The UI labels
and counts them separately (Dashboard, Endpoints).

Users configure source/destination **endpoints** (credentials) and
**environment options** (transfer and resulting VM settings). Provider
specific UI lives in `src/plugins/<provider>`.


## Working in this repo

- Install with `corepack enable && yarn install`. Ignore `node_modules`,
`dist`, `coverage`, `.yarn/cache`.
- CI (`.github/workflows/build.yml`) runs, in order: `npm run tsc`,
`npm run eslint`, `npm run format` (Prettier, `arrowParens: avoid`),
`npm run test`, `npm run build:dev`, `npm run e2e`. Run the relevant
ones before handing work back.
- Use the repo's Prettier (`yarn prettier`), not a global `npx` one. A
different version reformats unrelated code.
- Source files need the AGPL license header (copy from a nearby file and
use the current year for new files).
- Follow existing patterns: class components with MobX decorators, no
new state libraries or hooks-only rewrites of existing components.
- Do not add helpers for trivial checks such as
`transfer.scenario === "replica"`; keep those inline.
- Do not strip still-relevant inline comments.
- If regenerating a file, replace its contents; do not append duplicates.
- Avoid unrelated refactors and formatting changes in touched files.

## Tests

- Unit tests: Jest + React Testing Library, files named `*.spec.tsx`
next to the code (only that pattern runs). Use `TestUtils` from
`@tests/TestUtils` to select styled elements by class prefix.
- Mock sources and stores with `jest.mock("@src/...")` at the top of the
file; do not hit the network.
- E2E: Cypress specs in `cypress/e2e/<area>/*.cy.ts`. All API calls are
mocked with `cy.intercept` using `routeSelectors` and fixtures in
`cypress/fixtures`; use `cy.mockAuth()` / `cy.waitMockAuth()` for login.
Run with `NODE_ENV=development`, `CORIOLIS_URL=http://invalidd.it/`,
`npm run build:dev && npm run start`, then `npm run e2e`.
- Match nearby tests in style and level of detail.
11 changes: 11 additions & 0 deletions config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ const conf: Config = {
// The timeout between polling requests
requestPollTimeout: 5000,

// The timeout between polls on pages that collect a whole list
// (transfers, deployments, dashboard, endpoints). Each poll makes one
// request per `fullListFetchBatchSize` batch, so it is kept higher than
// `requestPollTimeout` to keep the request rate down.
listPollTimeout: 15000,

// The `limit` per request when collecting a full list. Independent of how
// many items the UI shows per page (`defaultListItemsPerPage`). Raise it
// on large installations to walk the list in fewer requests.
fullListFetchBatchSize: 100,

// The timeout for an inactive user session (in ms). Set 0 to disable.
inactiveSessionTimeout: 0,

Expand Down
2 changes: 2 additions & 0 deletions src/@types/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export type Config = {
showOpenstackCurrentUserSwitch: boolean;
useBarbicanSecrets: boolean;
requestPollTimeout: number;
listPollTimeout: number;
fullListFetchBatchSize: number;
instancesListBackgroundLoading: { default: number; [prop: string]: number };
extraOptionsApiCalls: ExtraOption[];
providerSortPriority: { [providerName in ProviderTypes]: number };
Expand Down
20 changes: 16 additions & 4 deletions src/components/smart/DashboardPage/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,30 @@ class ProjectsPage extends React.Component<Props, State> {
return;
}

await this.loadData(showLoading);
try {
await this.loadData(showLoading);
} catch (err) {
// A failed poll must not stop the polling loop.
console.error(err);
}
if (this.stopPolling) {
return;
}
this.pollTimeout = window.setTimeout(() => {
this.pollData(false);
}, configLoader.config.requestPollTimeout);
}, configLoader.config.listPollTimeout);
}

async loadData(showLoading: boolean) {
this.loadAdminData(showLoading);

await Promise.all([
transferStore.getTransfers({ skipLog: true, showLoading }),
deploymentStore.getDeployments({ skipLog: true, showLoading }),
// No `showLoading` on the two full-list stores: they are shared with the
// transfers and deployments pages, where it blanks a list that is
// already on screen for as long as the collection runs. A first load
// raises `loading` on its own, which is all this page needs.
transferStore.getTransfers({ skipLog: true }),
deploymentStore.getDeployments({ skipLog: true }),
endpointStore.getEndpoints({ skipLog: true, showLoading }),
projectStore.getProjects({ skipLog: true, showLoading }),
]);
Expand Down
36 changes: 14 additions & 22 deletions src/components/smart/DeploymentsPage/DeploymentsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,6 @@ class DeploymentsPage extends React.Component<Props, State> {
componentDidMount() {
document.title = "Coriolis Deployments";

deploymentStore.resetDeploymentPagination();

projectStore.getProjects();
endpointStore.getEndpoints({ showLoading: true });
userStore.getAllUsers({
Expand Down Expand Up @@ -114,7 +112,6 @@ class DeploymentsPage extends React.Component<Props, State> {
}

handleProjectChange() {
deploymentStore.resetDeploymentPagination();
endpointStore.getEndpoints({ showLoading: true });
deploymentStore.getDeployments({ showLoading: true });
}
Expand Down Expand Up @@ -235,14 +232,22 @@ class DeploymentsPage extends React.Component<Props, State> {
return;
}

await Promise.all([
deploymentStore.getDeployments({ skipLog: true }),
endpointStore.getEndpoints({ skipLog: true }),
userStore.getAllUsers({ skipLog: true, quietError: true }),
]);
try {
await Promise.all([
deploymentStore.getDeployments({ skipLog: true }),
endpointStore.getEndpoints({ skipLog: true }),
userStore.getAllUsers({ skipLog: true, quietError: true }),
]);
} catch (err) {
// A failed poll must not stop the polling loop.
console.error(err);
}
if (this.stopPolling) {
return;
}
this.pollTimeout = window.setTimeout(() => {
this.pollData();
}, configLoader.config.requestPollTimeout);
}, configLoader.config.listPollTimeout);
}

render() {
Expand Down Expand Up @@ -300,19 +305,6 @@ class DeploymentsPage extends React.Component<Props, State> {
this.setState({ selectedDeployments });
}}
dropdownActions={BulkActions}
apiPagination={{
currentPage: deploymentStore.deploymentsPage,
hasNextPage: deploymentStore.deploymentsHasNextPage,
itemsPerPage: deploymentStore.deploymentsItemsPerPage,
onPageChange: page => {
deploymentStore.setDeploymentsPage(page);
},
onItemsPerPageChange: e => {
deploymentStore.setDeploymentsItemsPerPage(
parseInt(e.target.value, 10),
);
},
}}
renderItemComponent={options => (
<TransferListItem
{...options}
Expand Down
20 changes: 14 additions & 6 deletions src/components/smart/EndpointsPage/EndpointsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,14 +263,22 @@ class EndpointsPage extends React.Component<Props, State> {
return;
}

await Promise.all([
endpointStore.getEndpoints({ showLoading, skipLog: true }),
deploymentStore.getDeployments({ skipLog: true }),
transferStore.getTransfers({ skipLog: true }),
]);
try {
await Promise.all([
endpointStore.getEndpoints({ showLoading, skipLog: true }),
deploymentStore.getDeployments({ skipLog: true }),
transferStore.getTransfers({ skipLog: true }),
]);
} catch (err) {
// A failed poll must not stop the polling loop.
console.error(err);
}
if (this.stopPolling) {
return;
}
this.pollTimeout = window.setTimeout(() => {
this.pollData();
}, configLoader.config.requestPollTimeout);
}, configLoader.config.listPollTimeout);
}

itemFilterFunction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,18 +200,26 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
return;
}

await Promise.all([
minionPoolStore.loadMinionPoolDetails(this.minionPoolId, {
showLoading,
skipLog: true,
}),
transferStore.getTransfers(),
deploymentStore.getDeployments(),
]);
try {
await Promise.all([
minionPoolStore.loadMinionPoolDetails(this.minionPoolId, {
showLoading,
skipLog: true,
}),
transferStore.getTransfers(),
deploymentStore.getDeployments(),
]);
} catch (err) {
// A failed poll must not stop the polling loop.
console.error(err);
}
if (this.state.pausePolling || this.stopPolling) {
return;
}

setTimeout(() => {
this.pollData(false);
}, configLoader.config.requestPollTimeout);
}, configLoader.config.listPollTimeout);
}

closeEditModal() {
Expand Down
48 changes: 24 additions & 24 deletions src/components/smart/TransfersPage/TransfersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,6 @@ class TransfersPage extends React.Component<Props, State> {
componentDidMount() {
document.title = "Coriolis Transfers";

transferStore.resetTransferPagination();

projectStore.getProjects();
endpointStore.getEndpoints({ showLoading: true });
userStore.getAllUsers({
Expand Down Expand Up @@ -126,7 +124,6 @@ class TransfersPage extends React.Component<Props, State> {
}

handleProjectChange() {
transferStore.resetTransferPagination();
transferStore.getTransfers();
endpointStore.getEndpoints({ showLoading: true });
}
Expand Down Expand Up @@ -267,7 +264,7 @@ class TransfersPage extends React.Component<Props, State> {

handleShowCreateDeploymentsModal() {
instanceStore.loadInstancesDetailsBulk(
transferStore.transfers.map(r => ({
this.state.selectedTransfers.map(r => ({
endpointId: r.origin_endpoint_id,
instanceIds: r.instances,
env: r.source_environment,
Expand All @@ -282,17 +279,25 @@ class TransfersPage extends React.Component<Props, State> {
return;
}

await Promise.all([
transferStore.getTransfers({ skipLog: true }),
endpointStore.getEndpoints({ skipLog: true }),
userStore.getAllUsers({ skipLog: true, quietError: true }),
]);
try {
await Promise.all([
transferStore.getTransfers({ skipLog: true }),
endpointStore.getEndpoints({ skipLog: true }),
userStore.getAllUsers({ skipLog: true, quietError: true }),
]);
} catch (err) {
// A failed poll must not stop the polling loop.
console.error(err);
}
if (this.stopPolling) {
return;
}
if (!this.schedulePolling) {
this.pollSchedule();
}
this.pollTimeout = window.setTimeout(() => {
this.pollData();
}, configLoader.config.requestPollTimeout);
}, configLoader.config.listPollTimeout);
}

async pollSchedule() {
Expand All @@ -304,7 +309,15 @@ class TransfersPage extends React.Component<Props, State> {
return;
}
this.schedulePolling = true;
await scheduleStore.getSchedulesBulk(this.paginatedTransferIds);
try {
await scheduleStore.getSchedulesBulk(this.paginatedTransferIds);
} catch (err) {
// A failed poll must not stop the polling loop.
console.error(err);
}
if (this.stopPolling) {
return;
}
this.schedulePollTimeout = window.setTimeout(() => {
this.pollSchedule();
}, SCHEDULE_POLL_TIMEOUT);
Expand Down Expand Up @@ -448,19 +461,6 @@ class TransfersPage extends React.Component<Props, State> {
onPaginatedItemsChange={paginatedTransfers => {
this.handlePaginatedItemsChange(paginatedTransfers);
}}
apiPagination={{
currentPage: transferStore.transfersPage,
hasNextPage: transferStore.transfersHasNextPage,
itemsPerPage: transferStore.transfersItemsPerPage,
onPageChange: page => {
transferStore.setTransfersPage(page);
},
onItemsPerPageChange: e => {
transferStore.setTransfersItemsPerPage(
parseInt(e.target.value, 10),
);
},
}}
renderItemComponent={options => (
<TransferListItem
{...options}
Expand Down
Loading
Loading