Skip to content

Commit

Permalink
Add UI for volumes #1683
Browse files Browse the repository at this point in the history
  • Loading branch information
olgenn committed Oct 3, 2024
1 parent f1f5fdc commit afcde1a
Show file tree
Hide file tree
Showing 19 changed files with 603 additions and 40 deletions.
5 changes: 5 additions & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,9 @@ export const API = {
BASE: () => `${API.BASE()}/server`,
INFO: () => `${API.SERVER.BASE()}/get_info`,
},

VOLUME: {
BASE: () => `${API.BASE()}/volumes`,
LIST: () => `${API.VOLUME.BASE()}/list`,
},
};
2 changes: 2 additions & 0 deletions frontend/src/layouts/AppLayout/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const useSideNavigation = () => {
{ type: 'link', text: t('navigation.runs'), href: ROUTES.RUNS.LIST },
{ type: 'link', text: t('navigation.fleets'), href: ROUTES.FLEETS.LIST },
{ type: 'link', text: t('navigation.models'), href: ROUTES.MODELS.LIST },
{ type: 'link', text: t('navigation.volumes'), href: ROUTES.VOLUMES.LIST },

mainProjectSettingsUrl && {
type: 'link',
Expand All @@ -46,6 +47,7 @@ export const useSideNavigation = () => {
{ type: 'link', text: t('navigation.runs'), href: ROUTES.ADMINISTRATION.RUNS.LIST },
{ type: 'link', text: t('navigation.fleets'), href: ROUTES.ADMINISTRATION.FLEETS.LIST },
{ type: 'link', text: t('navigation.project_other'), href: ROUTES.PROJECT.LIST },
{ type: 'link', text: t('navigation.volumes'), href: ROUTES.ADMINISTRATION.VOLUMES.LIST },

{
type: 'link',
Expand Down
12 changes: 7 additions & 5 deletions frontend/src/layouts/AppLayout/index.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
}

[class^='awsui_navigation'] {
[class^='awsui_list']:last-child {
[class^='awsui_list-item']:last-child {
a {
pointer-events: none;
color: awsui.$color-text-status-inactive !important;
[class^='awsui_list-container'] {
[class*='awsui_list-variant-root--last'] {
[class^='awsui_list-item']:last-child {
a {
pointer-events: none;
color: awsui.$color-text-status-inactive !important;
}
}
}
}
Expand Down
6 changes: 0 additions & 6 deletions frontend/src/layouts/AppLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,6 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
/>

<TallyComponent />

{data?.server_version && (
<div className={styles.dstackVersion}>
<Box color="text-status-inactive">Dstack version: {data?.server_version}</Box>
</div>
)}
</AnnotationContext>
);
};
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/libs/volumes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { StatusIndicatorProps } from '@cloudscape-design/components';

export const getStatusIconType = (status: IVolume['status']): StatusIndicatorProps['type'] => {
switch (status) {
case 'failed':
return 'error';
case 'active':
return 'success';
case 'provisioning':
return 'in-progress';
case 'submitted':
default:
console.error(new Error('Undefined volume status'));
}
};
25 changes: 24 additions & 1 deletion frontend/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
"user_settings": "User settings",
"account": "User",
"billing": "Billing",
"resources": "Resources"
"resources": "Resources",
"volumes": "Volumes"
},

"backend": {
Expand Down Expand Up @@ -444,6 +445,28 @@
"price": "Price"
}
},
"volume": {
"volumes": "Volumes",
"empty_message_title": "No volumes",
"empty_message_text": "No volumes to display.",
"nomatch_message_title": "No matches",
"nomatch_message_text": "We can't find a match.",

"name": "Name",
"project": "Project name",
"region": "Region",
"backend": "Backend",
"status": "Status",
"created_at": "Created at",
"price": "Price",

"statuses": {
"failed": "Failed",
"submitted": "Submitted",
"provisioning": "Provisioning",
"active": "Active"
}
},

"users": {
"page_title": "Users",
Expand Down
57 changes: 57 additions & 0 deletions frontend/src/pages/Volumes/AdministrationList/hooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { format } from 'date-fns';

import { StatusIndicator } from 'components';

import { DATE_TIME_FORMAT } from 'consts';
import { getStatusIconType } from 'libs/volumes';

export const useColumnsDefinitions = () => {
const { t } = useTranslation();

const columns = [
{
id: 'name',
header: t('volume.name'),
cell: (item: IVolume) => item.name,
},
{
id: 'project',
header: `${t('volume.project')}`,
cell: (item: IVolume) => item.project_name,
},
{
id: 'backend',
header: `${t('volume.backend')}`,
cell: (item: IVolume) => item.configuration.backend,
},
{
id: 'region',
header: `${t('volume.region')}`,
cell: (item: IVolume) => item.configuration.backend,
},

{
id: 'status',
header: t('volume.status'),
cell: (item: IVolume) => (
<StatusIndicator type={getStatusIconType(item.status)}>{t(`volume.statuses.${item.status}`)}</StatusIndicator>
),
},
{
id: 'created_at',
header: t('volume.created_at'),
cell: (item: IVolume) => format(new Date(item.created_at), DATE_TIME_FORMAT),
},
{
id: 'price',
header: `${t('volume.price')}`,
cell: (item: IVolume) => {
return `$${item.provisioning_data.price}`;
},
},
];

return { columns } as const;
};
96 changes: 96 additions & 0 deletions frontend/src/pages/Volumes/AdministrationList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from 'react';
import { useTranslation } from 'react-i18next';

import { Button, FormField, Header, Pagination, SelectCSD, Table, Toggle } from 'components';

import { useCollection } from 'hooks';

import { useFilters, useVolumesData, useVolumesTableEmptyMessages } from '../List/hooks';
import { useColumnsDefinitions } from './hooks';

import styles from '../List/styles.module.scss';

export const AdministrationVolumeList: React.FC = () => {
const { t } = useTranslation();
const { renderEmptyMessage, renderNoMatchMessage } = useVolumesTableEmptyMessages();

const {
onlyActive,
setOnlyActive,
isDisabledClearFilter,
clearFilters,
projectOptions,
selectedProject,
setSelectedProject,
} = useFilters();

const { data, isLoading, pagesCount, disabledNext, prevPage, nextPage } = useVolumesData({
project_name: selectedProject?.value ?? undefined,
only_active: onlyActive,
});

const { columns } = useColumnsDefinitions();

const { items, actions, collectionProps } = useCollection(data, {
filtering: {
empty: renderEmptyMessage(),
noMatch: renderNoMatchMessage(() => actions.setFiltering('')),
},
pagination: { pageSize: 20 },
selection: {},
});

return (
<Table
{...collectionProps}
variant="full-page"
columnDefinitions={columns}
items={items}
loading={isLoading}
loadingText={t('common.loading')}
stickyHeader={true}
header={<Header>{t('volume.volumes')}</Header>}
filter={
<div className={styles.filters}>
<div className={styles.select}>
<FormField label={t('projects.run.project')}>
<SelectCSD
disabled={!projectOptions?.length}
options={projectOptions}
selectedOption={selectedProject}
onChange={(event) => {
setSelectedProject(event.detail.selectedOption);
}}
placeholder={t('projects.run.project_placeholder')}
expandToViewport={true}
filteringType="auto"
/>
</FormField>
</div>

<div className={styles.activeOnly}>
<Toggle onChange={({ detail }) => setOnlyActive(detail.checked)} checked={onlyActive}>
{t('fleets.active_only')}
</Toggle>
</div>

<div className={styles.clear}>
<Button formAction="none" onClick={clearFilters} disabled={isDisabledClearFilter}>
{t('common.clearFilter')}
</Button>
</div>
</div>
}
pagination={
<Pagination
currentPageIndex={pagesCount}
pagesCount={pagesCount}
openEnd={!disabledNext}
disabled={isLoading || data.length === 0}
onPreviousPageClick={prevPage}
onNextPageClick={nextPage}
/>
}
/>
);
};
Loading

0 comments on commit afcde1a

Please sign in to comment.