Skip to content

Commit 0b6040f

Browse files
committed
Merge branch 'fleet_alert' into projects_only_no_fleets
2 parents 0cc421a + 7365425 commit 0b6040f

File tree

9 files changed

+249
-66
lines changed

9 files changed

+249
-66
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useEffect, useMemo, useState } from 'react';
2+
3+
import { useLazyGetFleetsQuery } from '../services/fleet';
4+
import { useGetProjectsQuery } from '../services/project';
5+
6+
type Args = { projectNames?: IProject['project_name'][] };
7+
8+
export const useCheckingForFleetsInProjects = ({ projectNames }: Args) => {
9+
const [projectFleetMap, setProjectFleetMap] = useState<Record<IProject['project_name'], boolean>>({});
10+
const { data: projectsData } = useGetProjectsQuery(undefined, {
11+
skip: !!projectNames?.length,
12+
});
13+
14+
const [getFleets] = useLazyGetFleetsQuery();
15+
16+
const projectNameForChecking = useMemo<IProject['project_name'][]>(() => {
17+
if (projectNames) {
18+
return projectNames;
19+
}
20+
21+
if (projectsData) {
22+
return projectsData.map((project) => project.project_name);
23+
}
24+
25+
return [];
26+
}, [projectNames, projectsData]);
27+
28+
useEffect(() => {
29+
const fetchFleets = async () => {
30+
const map: Record<IProject['project_name'], boolean> = {};
31+
32+
await Promise.all(
33+
projectNameForChecking.map((projectName) =>
34+
getFleets({
35+
limit: 1,
36+
project_name: projectName,
37+
only_active: true,
38+
})
39+
.unwrap()
40+
.then((data) => (map[projectName] = Boolean(data.length))),
41+
),
42+
);
43+
44+
setProjectFleetMap(map);
45+
};
46+
47+
fetchFleets();
48+
}, [projectNameForChecking]);
49+
50+
return projectFleetMap;
51+
};

frontend/src/locale/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,11 @@
564564
},
565565

566566
"fleets": {
567+
"no_alert": {
568+
"title": "No fleets",
569+
"description": "The project has no fleets. Create one before submitting a run.",
570+
"button_title": "Create a fleet"
571+
},
567572
"fleet": "Fleet",
568573
"fleet_placeholder": "Filtering by fleet",
569574
"fleet_name": "Fleet name",

frontend/src/pages/Fleets/List/index.tsx

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import React from 'react';
22
import { useTranslation } from 'react-i18next';
3+
import { ButtonProps } from '@cloudscape-design/components/button';
34

4-
import { Button, Header, Loader, PropertyFilter, SpaceBetween, Table, Toggle } from 'components';
5+
import { Alert, Button, Header, Loader, PropertyFilter, SpaceBetween, Table, Toggle } from 'components';
56

67
import { DEFAULT_TABLE_PAGE_SIZE } from 'consts';
78
import { useBreadcrumbs, useCollection, useInfiniteScroll } from 'hooks';
9+
import { useCheckingForFleetsInProjects } from 'hooks/useCheckingForFleetsInProjectsOfMember';
10+
import { goToUrl } from 'libs';
811
import { ROUTES } from 'routes';
912
import { useLazyGetFleetsQuery } from 'services/fleet';
1013

@@ -35,6 +38,8 @@ export const FleetList: React.FC = () => {
3538
isDisabledClearFilter,
3639
} = useFilters();
3740

41+
const projectHavingFleetMap = useCheckingForFleetsInProjects({});
42+
3843
const { data, isLoading, refreshList, isLoadingMore } = useInfiniteScroll<IFleet, TFleetListRequestParams>({
3944
useLazyQuery: useLazyGetFleetsQuery,
4045
args: { ...filteringRequestParams, limit: DEFAULT_TABLE_PAGE_SIZE },
@@ -67,6 +72,13 @@ export const FleetList: React.FC = () => {
6772
deleteFleets([...selectedItems]).catch(console.log);
6873
};
6974

75+
const projectDontHasFleet = Object.keys(projectHavingFleetMap).find((project) => !projectHavingFleetMap[project]);
76+
77+
const onCreateAFleet: ButtonProps['onClick'] = (event) => {
78+
event.preventDefault();
79+
goToUrl('https://dstack.ai/docs/quickstart/#create-a-fleet', true);
80+
};
81+
7082
return (
7183
<Table
7284
{...collectionProps}
@@ -78,25 +90,44 @@ export const FleetList: React.FC = () => {
7890
stickyHeader={true}
7991
selectionType="multi"
8092
header={
81-
<Header
82-
variant="awsui-h1-sticky"
83-
actions={
84-
<SpaceBetween size="xs" direction="horizontal">
85-
<Button formAction="none" onClick={deleteClickHandle} disabled={isDisabledDeleteButton}>
86-
{t('common.delete')}
87-
</Button>
88-
89-
<Button
90-
iconName="refresh"
91-
disabled={isLoading}
92-
ariaLabel={t('common.refresh')}
93-
onClick={refreshList}
94-
/>
95-
</SpaceBetween>
96-
}
97-
>
98-
{t('navigation.fleets')}
99-
</Header>
93+
<>
94+
{projectDontHasFleet && (
95+
<div className={styles.alertBox}>
96+
<Alert
97+
header={t('fleets.no_alert.title')}
98+
type="info"
99+
action={
100+
<Button iconName="external" formAction="none" onClick={onCreateAFleet}>
101+
{t('fleets.no_alert.button_title')}
102+
</Button>
103+
}
104+
>
105+
The project <code>{projectDontHasFleet}</code> has no fleets. Create one before submitting a
106+
run.
107+
</Alert>
108+
</div>
109+
)}
110+
111+
<Header
112+
variant="awsui-h1-sticky"
113+
actions={
114+
<SpaceBetween size="xs" direction="horizontal">
115+
<Button formAction="none" onClick={deleteClickHandle} disabled={isDisabledDeleteButton}>
116+
{t('common.delete')}
117+
</Button>
118+
119+
<Button
120+
iconName="refresh"
121+
disabled={isLoading}
122+
ariaLabel={t('common.refresh')}
123+
onClick={refreshList}
124+
/>
125+
</SpaceBetween>
126+
}
127+
>
128+
{t('navigation.fleets')}
129+
</Header>
130+
</>
100131
}
101132
filter={
102133
<div className={styles.filters}>

frontend/src/pages/Fleets/List/styles.module.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
.alertBox {
2+
margin-bottom: 12px;
3+
4+
:global {
5+
& [class^="awsui_alert"] {
6+
& [class^="awsui_action-slot"] {
7+
display: flex;
8+
align-items: center;
9+
}
10+
}
11+
}
12+
}
113
.filters {
214
display: flex;
315
flex-wrap: wrap;

frontend/src/pages/Project/Details/Settings/index.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { useTranslation } from 'react-i18next';
33
import { useNavigate, useParams } from 'react-router-dom';
44
import { debounce } from 'lodash';
55
import { ExpandableSection, Tabs } from '@cloudscape-design/components';
6+
import { ButtonProps } from '@cloudscape-design/components/button';
67
import Wizard from '@cloudscape-design/components/wizard';
78

89
import {
10+
Alert,
911
Box,
1012
Button,
1113
ButtonWithConfirmation,
@@ -22,7 +24,7 @@ import {
2224
import { HotspotIds } from 'layouts/AppLayout/TutorialPanel/constants';
2325

2426
import { useBreadcrumbs, useNotifications } from 'hooks';
25-
import { riseRouterException } from 'libs';
27+
import { goToUrl, riseRouterException } from 'libs';
2628
import { copyToClipboard } from 'libs';
2729
import { ROUTES } from 'routes';
2830
import { useGetProjectQuery, useUpdateProjectMembersMutation, useUpdateProjectMutation } from 'services/project';
@@ -35,6 +37,7 @@ import { useDeleteProject } from 'pages/Project/hooks/useDeleteProject';
3537
import { ProjectMembers } from 'pages/Project/Members';
3638
import { getProjectRoleByUserName } from 'pages/Project/utils';
3739

40+
import { useCheckingForFleetsInProjects } from '../../../../hooks/useCheckingForFleetsInProjectsOfMember';
3841
import { useBackendsTable } from '../../Backends/hooks';
3942
import { BackendsTable } from '../../Backends/Table';
4043
import { GatewaysTable } from '../../Gateways';
@@ -60,6 +63,10 @@ export const ProjectSettings: React.FC = () => {
6063
const { deleteProject, isDeleting } = useDeleteProject();
6164
const { data: currentUser } = useGetUserDataQuery({});
6265

66+
const projectNames = useMemo(() => [paramProjectName], [paramProjectName]);
67+
68+
const projectHavingFleetMap = useCheckingForFleetsInProjects({ projectNames });
69+
6370
const { data, isLoading, error } = useGetProjectQuery({ name: paramProjectName });
6471

6572
const { data: runsData } = useGetRunsQuery({
@@ -180,6 +187,13 @@ export const ProjectSettings: React.FC = () => {
180187

181188
const [activeStepIndex, setActiveStepIndex] = React.useState(0);
182189

190+
const projectDontHasFleet = !projectHavingFleetMap?.[paramProjectName];
191+
192+
const onCreateAFleet: ButtonProps['onClick'] = (event) => {
193+
event.preventDefault();
194+
goToUrl('https://dstack.ai/docs/quickstart/#create-a-fleet', true);
195+
};
196+
183197
if (isLoadingPage)
184198
return (
185199
<Container>
@@ -191,6 +205,22 @@ export const ProjectSettings: React.FC = () => {
191205
<>
192206
{data && backendsData && gatewaysData && (
193207
<SpaceBetween size="l">
208+
{projectDontHasFleet && (
209+
<div className={styles.alertBox}>
210+
<Alert
211+
header={t('fleets.no_alert.title')}
212+
type="info"
213+
action={
214+
<Button iconName="external" formAction="none" onClick={onCreateAFleet}>
215+
{t('fleets.no_alert.button_title')}
216+
</Button>
217+
}
218+
>
219+
The project <code>{paramProjectName}</code> has no fleets. Create one before submitting a run.
220+
</Alert>
221+
</div>
222+
)}
223+
194224
{isProjectMember && (
195225
<ExpandableSection
196226
variant="container"

frontend/src/pages/Project/Details/Settings/styles.module.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@
88
width: 300px;
99
}
1010

11+
.alertBox {
12+
:global {
13+
& [class^="awsui_alert"] {
14+
& [class^="awsui_action-slot"] {
15+
display: flex;
16+
align-items: center;
17+
}
18+
}
19+
}
20+
}
21+
1122
.codeWrapper {
1223
position: relative;
1324

0 commit comments

Comments
 (0)