From 52a29307325638ca5af221d4ce4ead786c8e9ce0 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Mon, 2 Nov 2026 11:23:49 +0500 Subject: [PATCH 1/5] Drop is_payment_method_attached from billing info The field is unused since the edit payment method button was commented out. Remove the type field and the dead commented-out code. --- frontend/src/locale/en.json | 2 -- .../src/pages/User/Details/Billing/index.tsx | 28 +------------------ frontend/src/types/user.d.ts | 1 - 3 files changed, 1 insertion(+), 30 deletions(-) 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/User/Details/Billing/index.tsx b/frontend/src/pages/User/Details/Billing/index.tsx index 5e346169a6..c2bd5ba208 100644 --- a/frontend/src/pages/User/Details/Billing/index.tsx +++ b/frontend/src/pages/User/Details/Billing/index.tsx @@ -9,11 +9,7 @@ import { HotspotIds } from 'layouts/AppLayout/TutorialPanel/constants'; import { useAppSelector, useBreadcrumbs, useNotifications } from 'hooks'; import { centsToFormattedString, getServerError, goToUrl } from 'libs'; import { ROUTES } from 'routes'; -import { - useGetUserBillingInfoQuery, - useUserBillingCheckoutSessionMutation, - // useUserBillingPortalSessionMutation, -} from 'services/user'; +import { useGetUserBillingInfoQuery, useUserBillingCheckoutSessionMutation } from 'services/user'; import { GlobalUserRole } from 'types'; import { selectUserName } from 'App/slice'; @@ -37,7 +33,6 @@ export const Billing: React.FC = () => { 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[]; } From 1cb4fd21bae7a5966cb47fc14d2013e31c5b99ec Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Mon, 2 Nov 2026 11:38:52 +0500 Subject: [PATCH 2/5] Stop fetching all projects where the UI needs only a few The tutorial panel, GitHub login callback, Run launch project picker, and the no-fleet alerts on Runs and Fleets lists requested every visible project (all projects for admins, plus all public ones for members). Request only the current user's projects, with limit 1 for existence checks, and use list_only_no_fleets directly for the alerts. --- .../App/Login/LoginByGithubCallback/index.tsx | 4 +-- .../useCheckingForFleetsInProjectsOfMember.ts | 34 ++++++------------- frontend/src/hooks/useProjectFilter.ts | 6 +++- .../layouts/AppLayout/TutorialPanel/hooks.ts | 2 +- frontend/src/pages/Fleets/List/index.tsx | 6 ++-- frontend/src/pages/Runs/List/index.tsx | 6 ++-- 6 files changed, 23 insertions(+), 35 deletions(-) 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/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 (
Date: Mon, 2 Nov 2026 11:51:16 +0500 Subject: [PATCH 3/5] Add TODO to limit job submissions in runs/get --- src/dstack/_internal/server/routers/runs.py | 4 ++++ 1 file changed, 4 insertions(+) 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, From df40cf93801fcfb7f9e420ec58c3ffdb17b06ce3 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Mon, 2 Nov 2026 11:53:52 +0500 Subject: [PATCH 4/5] Add FIXME about run cost undercounting with job_submissions_limit --- src/dstack/_internal/server/services/runs/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/dstack/_internal/server/services/runs/__init__.py b/src/dstack/_internal/server/services/runs/__init__.py index 6a6eac719c..b1b784a7c5 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. + # Calculate run costs efficiently using metering tables instead. run_cost = math.fsum( _get_job_submission_cost(submission) for job in run.jobs From c93440654d0b9d35f5570690252c7143b4fe17d4 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Mon, 2 Nov 2026 11:54:02 +0500 Subject: [PATCH 5/5] Clarify that metering tables are not introduced yet --- src/dstack/_internal/server/services/runs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dstack/_internal/server/services/runs/__init__.py b/src/dstack/_internal/server/services/runs/__init__.py index b1b784a7c5..436b744093 100644 --- a/src/dstack/_internal/server/services/runs/__init__.py +++ b/src/dstack/_internal/server/services/runs/__init__.py @@ -1249,7 +1249,7 @@ 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. - # Calculate run costs efficiently using metering tables instead. + # 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