diff --git a/frontend/src/App/Login/LoginByGithubCallback/index.tsx b/frontend/src/App/Login/LoginByGithubCallback/index.tsx index 07eac4ee51..9037eb121a 100644 --- a/frontend/src/App/Login/LoginByGithubCallback/index.tsx +++ b/frontend/src/App/Login/LoginByGithubCallback/index.tsx @@ -40,10 +40,10 @@ export const LoginByGithubCallback: React.FC = () => { } githubCallback({ code, state }) .unwrap() - .then(async ({ creds: { token } }) => { + .then(async ({ username, creds: { token } }) => { dispatch(setAuthData({ token })); if (product.hasPresets) { - const result = await getProjects({}).unwrap(); + const result = await getProjects({ username, limit: 1 }).unwrap(); if (result.data.length === 0) { navigate(ROUTES.PROJECT.ADD); return; diff --git a/frontend/src/hooks/useCheckingForFleetsInProjectsOfMember.ts b/frontend/src/hooks/useCheckingForFleetsInProjectsOfMember.ts index b330358b46..fca5973169 100644 --- a/frontend/src/hooks/useCheckingForFleetsInProjectsOfMember.ts +++ b/frontend/src/hooks/useCheckingForFleetsInProjectsOfMember.ts @@ -1,40 +1,28 @@ import { useMemo } from 'react'; -import { useGetOnlyNoFleetsProjectsQuery, useGetProjectsQuery } from 'services/project'; +import { useGetOnlyNoFleetsProjectsQuery } from 'services/project'; -type Args = { projectNames?: IProject['project_name'][] }; +type Args = { projectNames: IProject['project_name'][] }; export const useCheckingForFleetsInProjects = ({ projectNames }: Args) => { - const { data: projectsData } = useGetProjectsQuery( - {}, - { - skip: !!projectNames?.length, - }, - ); - const { data: noFleetsProjectsData } = useGetOnlyNoFleetsProjectsQuery(); - const projectNameForChecking = useMemo(() => { - if (projectNames) { - return projectNames; - } - - if (projectsData?.data) { - return projectsData.data.map((project) => project.project_name); - } - - return []; - }, [projectNames, projectsData]); - const projectHavingFleetMap = useMemo>(() => { const map: Record = {}; - projectNameForChecking.forEach((projectName) => { + projectNames.forEach((projectName) => { map[projectName] = !noFleetsProjectsData?.some((i) => i.project_name === projectName); }); return map; - }, [projectNameForChecking, noFleetsProjectsData]); + }, [projectNames, noFleetsProjectsData]); return projectHavingFleetMap; }; + +export const useNewestProjectWithoutFleets = (): IProject['project_name'] | undefined => { + const { data: noFleetsProjectsData } = useGetOnlyNoFleetsProjectsQuery(); + + // The endpoint sorts projects by ascending creation time + return noFleetsProjectsData?.[noFleetsProjectsData.length - 1]?.project_name; +}; diff --git a/frontend/src/hooks/useProjectFilter.ts b/frontend/src/hooks/useProjectFilter.ts index 5c54fd28e0..e5d7706d3c 100644 --- a/frontend/src/hooks/useProjectFilter.ts +++ b/frontend/src/hooks/useProjectFilter.ts @@ -4,6 +4,9 @@ import { SelectCSDProps } from 'components'; import { useGetProjectsQuery } from 'services/project'; +import { selectUserName } from 'App/slice'; + +import useAppSelector from './useAppSelector'; import { useLocalStorageState } from './useLocalStorageState'; type Args = { @@ -16,7 +19,8 @@ export const useProjectFilter = ({ localStorePrefix }: Args) => { null, ); - const { data: projectsData, isLoading } = useGetProjectsQuery({}); + const userName = useAppSelector(selectUserName); + const { data: projectsData, isLoading } = useGetProjectsQuery({ username: userName ?? '' }, { skip: !userName }); const projectOptions = useMemo(() => { if (!projectsData?.data?.length) return []; diff --git a/frontend/src/layouts/AppLayout/TutorialPanel/hooks.ts b/frontend/src/layouts/AppLayout/TutorialPanel/hooks.ts index e1770416d5..5d8bc81802 100644 --- a/frontend/src/layouts/AppLayout/TutorialPanel/hooks.ts +++ b/frontend/src/layouts/AppLayout/TutorialPanel/hooks.ts @@ -45,7 +45,7 @@ export const useTutorials = () => { } = useAppSelector(selectTutorialPanel); const { data: userBillingData } = useGetUserBillingInfoQuery({ username: useName ?? '' }, { skip: !useName }); - const { data: projectData } = useGetProjectsQuery({}); + const { data: projectData } = useGetProjectsQuery({ username: useName ?? '', limit: 1 }, { skip: !useName }); const { data: runsData } = useGetRunsQuery({ limit: 1, job_submissions_limit: 1, diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 9a179d82be..7168d12221 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -844,9 +844,7 @@ "balance": "Balance", "billing_history": "Billing history", "payment_method": "Payment method", - "no_payment_method": "No payment method attached", "top_up_balance": "Top up balance", - "edit_payment_method": "Edit payment method", "payment_amount": "Payment amount", "amount_description": "Minimum: ${{value}}", "make_payment": "Make a payment", diff --git a/frontend/src/pages/Fleets/List/index.tsx b/frontend/src/pages/Fleets/List/index.tsx index 7e5ef21cf5..001a7dbef6 100644 --- a/frontend/src/pages/Fleets/List/index.tsx +++ b/frontend/src/pages/Fleets/List/index.tsx @@ -5,7 +5,7 @@ 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 { useNewestProjectWithoutFleets } from 'hooks/useCheckingForFleetsInProjectsOfMember'; import { ROUTES } from 'routes'; import { useLazyGetFleetsQuery } from 'services/fleet'; @@ -40,7 +40,7 @@ export const FleetList: React.FC = () => { handleLoadItems, } = useFilters(); - const projectHavingFleetMap = useCheckingForFleetsInProjects({}); + const projectDontHasFleet = useNewestProjectWithoutFleets(); const { data, isLoading, refreshList, isLoadingMore } = useInfiniteScroll({ useLazyQuery: useLazyGetFleetsQuery, @@ -74,8 +74,6 @@ export const FleetList: React.FC = () => { deleteFleets([...selectedItems]).catch(console.log); }; - const projectDontHasFleet = Object.keys(projectHavingFleetMap).find((project) => !projectHavingFleetMap[project]); - return ( { filteringStatusType, handleLoadItems, } = useFilters(); - const projectHavingFleetMap = useCheckingForFleetsInProjects({}); + const projectDontHasFleet = useNewestProjectWithoutFleets(); const { data, isLoading, refreshList, isLoadingMore } = useInfiniteScroll({ useLazyQuery: useLazyGetRunsQuery, @@ -131,8 +131,6 @@ export const RunList: React.FC = () => { ); }; - const projectDontHasFleet = Object.keys(projectHavingFleetMap).find((project) => !projectHavingFleetMap[project]); - return (
{ const { data, isLoading } = useGetUserBillingInfoQuery({ username: paramUserName }); const [billingCheckout, { isLoading: isLoadingBillingCheckout }] = useUserBillingCheckoutSessionMutation(); - // const [billingPortalSession, { isLoading: isLoadingBillingPortalSession }] = useUserBillingPortalSessionMutation(); useBreadcrumbs([ { @@ -81,21 +76,6 @@ export const Billing: React.FC = () => { setShowPaymentModal(true); }; - // const editPaymentMethod = () => { - // billingPortalSession({ - // username: paramUserName, - // }) - // .unwrap() - // .then((data) => goToUrl(data.url)) - // .catch((error) => { - // pushNotification({ - // type: 'error', - // content: t('common.server_error', { error: getServerError(error) }), - // }); - // }) - // .finally(closeModal); - // }; - const closeModal = () => { setShowPaymentModal(false); }; @@ -119,12 +99,6 @@ export const Billing: React.FC = () => { )} - - {/* {data?.is_payment_method_attached && ( - - )} */} )} diff --git a/frontend/src/types/user.d.ts b/frontend/src/types/user.d.ts index cafda2920a..4b1883fddd 100644 --- a/frontend/src/types/user.d.ts +++ b/frontend/src/types/user.d.ts @@ -42,7 +42,6 @@ declare interface IUserAuthData extends Pick { declare interface IUserBillingInfo { balance: number; - is_payment_method_attached: boolean; default_payment_amount: number; billing_history: IPayment[]; } diff --git a/src/dstack/_internal/server/routers/runs.py b/src/dstack/_internal/server/routers/runs.py index 7b9e2a7421..4641142d41 100644 --- a/src/dstack/_internal/server/routers/runs.py +++ b/src/dstack/_internal/server/routers/runs.py @@ -105,6 +105,10 @@ async def get_run( If given `id`, returns deleted runs. """ _, project = user_project + # TODO: Limit job submissions returned per job and introduce a paginated API + # to list job submissions. Every call returns the full submission history, + # which grows with retries and redeployments. Note that `Run.cost` is summed + # over the loaded submissions, so it must be computed separately first. run = await runs.get_run( session=session, project=project, diff --git a/src/dstack/_internal/server/services/runs/__init__.py b/src/dstack/_internal/server/services/runs/__init__.py index 6a6eac719c..436b744093 100644 --- a/src/dstack/_internal/server/services/runs/__init__.py +++ b/src/dstack/_internal/server/services/runs/__init__.py @@ -1246,6 +1246,10 @@ async def _get_run_repo_or_error( def _get_run_cost(run: Run) -> float: + # FIXME: The cost is summed only over the loaded job submissions, so it is + # underestimated when `job_submissions_limit` drops older submissions, + # e.g. in the UI run list for runs with retries or redeployments. + # Once metering tables are introduced, calculate run costs efficiently from them. run_cost = math.fsum( _get_job_submission_cost(submission) for job in run.jobs