Skip to content
Merged
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
1 change: 1 addition & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const API = {
PROJECTS: {
BASE: () => `${API.BASE()}/projects`,
LIST: () => `${API.PROJECTS.BASE()}/list`,
LIST_ONLY_NO_FLEETS: () => `${API.PROJECTS.BASE()}/list_only_no_fleets`,
CREATE: () => `${API.PROJECTS.BASE()}/create`,
CREATE_WIZARD: () => `${API.PROJECTS.BASE()}/create_wizard`,
DELETE: () => `${API.PROJECTS.BASE()}/delete`,
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/hooks/useCheckingForFleetsInProjectsOfMember.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useMemo } from 'react';

import { useGetOnlyNoFleetsProjectsQuery, useGetProjectsQuery } from 'services/project';

type Args = { projectNames?: IProject['project_name'][] };

export const useCheckingForFleetsInProjects = ({ projectNames }: Args) => {
const { data: projectsData } = useGetProjectsQuery(undefined, {
skip: !!projectNames?.length,
});

const { data: noFleetsProjectsData } = useGetOnlyNoFleetsProjectsQuery();

const projectNameForChecking = useMemo<IProject['project_name'][]>(() => {
if (projectNames) {
return projectNames;
}

if (projectsData) {
return projectsData.map((project) => project.project_name);
}

return [];
}, [projectNames, projectsData]);

const projectHavingFleetMap = useMemo<Record<IProject['project_name'], boolean>>(() => {
const map: Record<IProject['project_name'], boolean> = {};

projectNameForChecking.forEach((projectName) => {
map[projectName] = !noFleetsProjectsData?.some((i) => i.project_name === projectName);
});

return map;
}, [projectNameForChecking, noFleetsProjectsData]);

return projectHavingFleetMap;
};
5 changes: 5 additions & 0 deletions frontend/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,11 @@
},

"fleets": {
"no_alert": {
"title": "No fleets",
"description": "The project has no fleets. Create one before submitting a run.",
"button_title": "Create a fleet"
},
"fleet": "Fleet",
"fleet_placeholder": "Filtering by fleet",
"fleet_name": "Fleet name",
Expand Down
53 changes: 34 additions & 19 deletions frontend/src/pages/Fleets/List/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { Button, Header, Loader, PropertyFilter, SpaceBetween, Table, Toggle } f

import { DEFAULT_TABLE_PAGE_SIZE } from 'consts';
import { useBreadcrumbs, useCollection, useInfiniteScroll } from 'hooks';
import { useCheckingForFleetsInProjects } from 'hooks/useCheckingForFleetsInProjectsOfMember';
import { ROUTES } from 'routes';
import { useLazyGetFleetsQuery } from 'services/fleet';

import { NoFleetProjectAlert } from 'pages/Project/components/NoFleetProjectAlert';

import { useColumnsDefinitions, useEmptyMessages, useFilters } from './hooks';
import { useDeleteFleet } from './useDeleteFleet';

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

const projectHavingFleetMap = useCheckingForFleetsInProjects({});

const { data, isLoading, refreshList, isLoadingMore } = useInfiniteScroll<IFleet, TFleetListRequestParams>({
useLazyQuery: useLazyGetFleetsQuery,
args: { ...filteringRequestParams, limit: DEFAULT_TABLE_PAGE_SIZE },
Expand Down Expand Up @@ -67,6 +72,8 @@ export const FleetList: React.FC = () => {
deleteFleets([...selectedItems]).catch(console.log);
};

const projectDontHasFleet = Object.keys(projectHavingFleetMap).find((project) => !projectHavingFleetMap[project]);

return (
<Table
{...collectionProps}
Expand All @@ -78,25 +85,33 @@ export const FleetList: React.FC = () => {
stickyHeader={true}
selectionType="multi"
header={
<Header
variant="awsui-h1-sticky"
actions={
<SpaceBetween size="xs" direction="horizontal">
<Button formAction="none" onClick={deleteClickHandle} disabled={isDisabledDeleteButton}>
{t('common.delete')}
</Button>

<Button
iconName="refresh"
disabled={isLoading}
ariaLabel={t('common.refresh')}
onClick={refreshList}
/>
</SpaceBetween>
}
>
{t('navigation.fleets')}
</Header>
<>
<NoFleetProjectAlert
className={styles.noFleetAlert}
projectName={projectDontHasFleet ?? ''}
show={!!projectDontHasFleet}
/>

<Header
variant="awsui-h1-sticky"
actions={
<SpaceBetween size="xs" direction="horizontal">
<Button formAction="none" onClick={deleteClickHandle} disabled={isDisabledDeleteButton}>
{t('common.delete')}
</Button>

<Button
iconName="refresh"
disabled={isLoading}
ariaLabel={t('common.refresh')}
onClick={refreshList}
/>
</SpaceBetween>
}
>
{t('navigation.fleets')}
</Header>
</>
}
filter={
<div className={styles.filters}>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/pages/Fleets/List/styles.module.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.noFleetAlert {
margin-bottom: 12px;
}
.filters {
display: flex;
flex-wrap: wrap;
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/pages/Project/Details/Settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { HotspotIds } from 'layouts/AppLayout/TutorialPanel/constants';

import { useBreadcrumbs, useNotifications } from 'hooks';
import { useCheckingForFleetsInProjects } from 'hooks/useCheckingForFleetsInProjectsOfMember';
import { riseRouterException } from 'libs';
import { copyToClipboard } from 'libs';
import { ROUTES } from 'routes';
Expand All @@ -37,6 +38,7 @@ import { getProjectRoleByUserName } from 'pages/Project/utils';

import { useBackendsTable } from '../../Backends/hooks';
import { BackendsTable } from '../../Backends/Table';
import { NoFleetProjectAlert } from '../../components/NoFleetProjectAlert';
import { GatewaysTable } from '../../Gateways';
import { useGatewaysTable } from '../../Gateways/hooks';
import { ProjectSecrets } from '../../Secrets';
Expand All @@ -60,6 +62,10 @@ export const ProjectSettings: React.FC = () => {
const { deleteProject, isDeleting } = useDeleteProject();
const { data: currentUser } = useGetUserDataQuery({});

const projectNames = useMemo(() => [paramProjectName], [paramProjectName]);

const projectHavingFleetMap = useCheckingForFleetsInProjects({ projectNames });

const { data, isLoading, error } = useGetProjectQuery({ name: paramProjectName });

const { data: runsData } = useGetRunsQuery({
Expand Down Expand Up @@ -180,6 +186,8 @@ export const ProjectSettings: React.FC = () => {

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

const projectDontHasFleet = !projectHavingFleetMap?.[paramProjectName];

if (isLoadingPage)
return (
<Container>
Expand All @@ -191,6 +199,8 @@ export const ProjectSettings: React.FC = () => {
<>
{data && backendsData && gatewaysData && (
<SpaceBetween size="l">
<NoFleetProjectAlert projectName={paramProjectName} show={projectDontHasFleet} dismissible />

{isProjectMember && (
<ExpandableSection
variant="container"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import cn from 'classnames';

import type { ButtonProps } from 'components';
import { Alert, AlertProps, Button } from 'components';

import { useLocalStorageState } from 'hooks/useLocalStorageState';
import { goToUrl } from 'libs';

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

type NoFleetProjectAlertProps = {
show?: boolean;
projectName: string;
className?: string;
dismissible?: boolean;
};

export const NoFleetProjectAlert: React.FC<NoFleetProjectAlertProps> = ({ projectName, show, className, dismissible }) => {
const { t } = useTranslation();
const [dontShowAgain, setDontShowAgain] = useLocalStorageState(`noFleetProjectAlert-${projectName}`, false);

const onCreateAFleet: ButtonProps['onClick'] = (event) => {
event.preventDefault();
goToUrl('https://dstack.ai/docs/quickstart/#create-a-fleet', true);
};

const onDismiss: AlertProps['onDismiss'] = () => setDontShowAgain(true);

if (!show || dontShowAgain) {
return null;
}

return (
<div className={cn(styles.alertBox, className)}>
<Alert
header={t('fleets.no_alert.title')}
type="info"
dismissible={dismissible}
onDismiss={onDismiss}
action={
<Button iconName="external" formAction="none" onClick={onCreateAFleet}>
{t('fleets.no_alert.button_title')}
</Button>
}
>
The project <code>{projectName}</code> has no fleets. Create one before submitting a run.
</Alert>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.alertBox {
:global {
& [class^="awsui_alert"] {
& [class^="awsui_action-slot"] {
display: flex;
align-items: center;
}
}
}
}
17 changes: 16 additions & 1 deletion frontend/src/pages/Runs/CreateDevEnvironment/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ import * as yup from 'yup';
import { Box, Link, WizardProps } from '@cloudscape-design/components';
import { CardsProps } from '@cloudscape-design/components/cards';

import type { TabsProps, ToggleProps } from 'components';
import { TabsProps, ToggleProps } from 'components';
import { Container, FormCodeEditor, FormField, FormInput, FormSelect, SpaceBetween, Tabs, Toggle, Wizard } from 'components';

import { useBreadcrumbs, useNotifications } from 'hooks';
import { useCheckingForFleetsInProjects } from 'hooks/useCheckingForFleetsInProjectsOfMember';
import { getServerError } from 'libs';
import { ROUTES } from 'routes';
import { useApplyRunMutation } from 'services/run';

import { OfferList } from 'pages/Offers/List';
import { NoFleetProjectAlert } from 'pages/Project/components/NoFleetProjectAlert';

import { useGenerateYaml } from './hooks/useGenerateYaml';
import { useGetRunSpecFromYaml } from './hooks/useGetRunSpecFromYaml';
Expand Down Expand Up @@ -117,6 +119,9 @@ export const CreateDevEnvironment: React.FC = () => {

const [getRunSpecFromYaml] = useGetRunSpecFromYaml({ projectName: selectedProject ?? '' });

const projectHavingFleetMap = useCheckingForFleetsInProjects({ projectNames: selectedProject ? [selectedProject] : [] });
const projectDontHasFleets = !!selectedProject && !projectHavingFleetMap[selectedProject];

const [applyRun, { isLoading: isApplying }] = useApplyRunMutation();

const loading = isApplying;
Expand Down Expand Up @@ -174,6 +179,10 @@ export const CreateDevEnvironment: React.FC = () => {
const stepValidators = [validateOffer, validateSecondStep, validateConfig];

if (reason === 'next') {
if (projectDontHasFleets) {
window.scrollTo(0, 0);
}

stepValidators[activeStepIndex]?.().then((isValid) => {
if (isValid) {
setActiveStepIndex(requestedStepIndex);
Expand Down Expand Up @@ -277,6 +286,12 @@ export const CreateDevEnvironment: React.FC = () => {

return (
<form className={cn({ [styles.wizardForm]: activeStepIndex === 0 })} onSubmit={handleSubmit(onSubmit)}>
<NoFleetProjectAlert
className={styles.noFleetAlert}
projectName={selectedProject ?? ''}
show={projectDontHasFleets}
/>

<Wizard
activeStepIndex={activeStepIndex}
onNavigate={onNavigateHandler}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
@use '@cloudscape-design/design-tokens/index' as awsui;

.noFleetAlert {
margin: 12px 0 0;
}

.wizardForm {
& [class^="awsui_wizard"] {
& [class^="awsui_footer"] {
Expand Down
Loading