From b6430bb4942f0c10a44eb0dbc34b03f7a2ce86b1 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 5 Feb 2025 13:34:08 +0000 Subject: [PATCH 01/15] Set base structure to display the provisioner jobs --- site/src/components/Badge/Badge.tsx | 19 +++- site/src/components/Button/Button.tsx | 8 +- .../management/DeploymentSidebarView.tsx | 5 + .../ProvisionersPage/ProvisionersPage.tsx | 96 +++++++++++++++++++ site/src/router.tsx | 5 + 5 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 site/src/pages/DeploymentSettingsPage/ProvisionersPage/ProvisionersPage.tsx diff --git a/site/src/components/Badge/Badge.tsx b/site/src/components/Badge/Badge.tsx index 94d0fa9052340..2044db6d20614 100644 --- a/site/src/components/Badge/Badge.tsx +++ b/site/src/components/Badge/Badge.tsx @@ -7,16 +7,21 @@ import type { FC } from "react"; import { cn } from "utils/cn"; export const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-1 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + "inline-flex items-center rounded-md border px-2 py-1 transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { default: "border-transparent bg-surface-secondary text-content-secondary shadow hover:bg-surface-tertiary", }, + size: { + sm: "text-2xs font-regular", + md: "text-xs font-medium", + }, }, defaultVariants: { variant: "default", + size: "md", }, }, ); @@ -25,8 +30,16 @@ export interface BadgeProps extends React.HTMLAttributes, VariantProps {} -export const Badge: FC = ({ className, variant, ...props }) => { +export const Badge: FC = ({ + className, + variant, + size, + ...props +}) => { return ( -
+
); }; diff --git a/site/src/components/Button/Button.tsx b/site/src/components/Button/Button.tsx index 93e1a479aa6cc..23803b89add15 100644 --- a/site/src/components/Button/Button.tsx +++ b/site/src/components/Button/Button.tsx @@ -9,7 +9,7 @@ import { cn } from "utils/cn"; export const buttonVariants = cva( `inline-flex items-center justify-center gap-1 whitespace-nowrap - border-solid rounded-md transition-colors min-w-20 + border-solid rounded-md transition-colors text-sm font-semibold font-medium cursor-pointer no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link disabled:pointer-events-none disabled:text-content-disabled @@ -28,9 +28,9 @@ export const buttonVariants = cva( }, size: { - lg: "h-10 px-3 py-2 [&_svg]:size-icon-lg", - sm: "h-[30px] px-2 py-1.5 text-xs [&_svg]:size-icon-sm", - icon: "h-[30px] min-w-[30px] px-1 py-1.5 [&_svg]:size-icon-sm", + lg: "min-w-20 h-10 px-3 py-2 [&_svg]:size-icon-lg", + sm: "min-w-20 h-8 px-2 py-1.5 text-xs [&_svg]:size-icon-sm", + icon: "size-8 px-1.5 [&_svg]:size-icon-sm", }, }, defaultVariants: { diff --git a/site/src/modules/management/DeploymentSidebarView.tsx b/site/src/modules/management/DeploymentSidebarView.tsx index 4783133a872bb..21ff6f84b4a48 100644 --- a/site/src/modules/management/DeploymentSidebarView.tsx +++ b/site/src/modules/management/DeploymentSidebarView.tsx @@ -94,6 +94,11 @@ export const DeploymentSidebarView: FC = ({ IdP Organization Sync )} + {permissions.viewDeploymentValues && ( + + Provisioners + + )} {!hasPremiumLicense && ( Premium )} diff --git a/site/src/pages/DeploymentSettingsPage/ProvisionersPage/ProvisionersPage.tsx b/site/src/pages/DeploymentSettingsPage/ProvisionersPage/ProvisionersPage.tsx new file mode 100644 index 0000000000000..6bee4e485a12e --- /dev/null +++ b/site/src/pages/DeploymentSettingsPage/ProvisionersPage/ProvisionersPage.tsx @@ -0,0 +1,96 @@ +import { AvatarFallback } from "@radix-ui/react-avatar"; +import type { ProvisionerJob } from "api/typesGenerated"; +import { Avatar } from "components/Avatar/Avatar"; +import { Badge } from "components/Badge/Badge"; +import { Button } from "components/Button/Button"; +import { Link } from "components/Link/Link"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "components/Table/Table"; +import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; +import { BanIcon } from "lucide-react"; +import type { FC } from "react"; +import { docs } from "utils/docs"; + +export const ProvisionersPage: FC = () => { + return ( +
+
+
+

Provisioners

+
+
+ +
+ + + + Jobs + + + Daemons + + + + +
+ +
+
+
+ ); +}; + +const JobsTabContent: FC = () => { + return ( +
+

+ Provisioner Jobs are the individual tasks assigned to Provisioners when + the workspaces are being built.{" "} + View docs +

+ + + + + Last seen + Name + Template + Tags + Status + + + + + 5 min ago + + workspace_build + + +
+ + Write Coder on Coder +
+
+ + [foo=bar] + + Completed + + + +
+
+
+
+ ); +}; + +export default ProvisionersPage; diff --git a/site/src/router.tsx b/site/src/router.tsx index acaf417cecbcd..c56bd1ee27ca1 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -304,6 +304,10 @@ const ChangePasswordPage = lazy( const IdpOrgSyncPage = lazy( () => import("./pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage"), ); +const ProvisionersPage = lazy( + () => + import("./pages/DeploymentSettingsPage/ProvisionersPage/ProvisionersPage"), +); const RoutesWithSuspense = () => { return ( @@ -452,6 +456,7 @@ export const router = createBrowserRouter( /> } /> } /> + } /> From 643c3626ea522c584bd4f40bd86d14d42acbdbee Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 7 Feb 2025 18:56:03 +0000 Subject: [PATCH 02/15] [WIP]: Load data and display them in the table --- .devcontainer/devcontainer.json | 11 +- site/src/api/api.ts | 7 + site/src/api/queries/organizations.ts | 7 + .../ProvisionersPage/ProvisionersPage.tsx | 96 ---------- .../OrganizationProvisionersPage.tsx | 167 ++++++++++++++++-- ...ganizationProvisionersPageView.stories.tsx | 142 --------------- .../OrganizationProvisionersPageView.tsx | 148 ---------------- site/src/router.tsx | 5 - site/src/utils/time.ts | 11 ++ 9 files changed, 187 insertions(+), 407 deletions(-) delete mode 100644 site/src/pages/DeploymentSettingsPage/ProvisionersPage/ProvisionersPage.tsx delete mode 100644 site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.stories.tsx delete mode 100644 site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.tsx diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index de550f174bc9f..be0dbfd3e89e9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,5 +9,14 @@ } }, // SYS_PTRACE to enable go debugging - "runArgs": ["--cap-add=SYS_PTRACE"] + "runArgs": [ + "--cap-add=SYS_PTRACE" + ], + "customizations": { + "vscode": { + "extensions": [ + "biomejs.biome" + ] + } + } } diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 5a314ddde151a..85cc2b6eff47f 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2295,6 +2295,13 @@ class ApiMethods { ); return res.data; }; + + getProvisionerJobs = async (orgId: string) => { + const res = await this.axios.get( + `/api/v2/organizations/${orgId}/provisionerjobs`, + ); + return res.data; + }; } // This is a hard coded CSRF token/cookie pair for local development. In prod, diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 6246664e6ecf0..f99976fcd2422 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -244,6 +244,13 @@ export const organizationPermissions = (organizationId: string | undefined) => { }; }; +export const provisionerJobs = (orgId: string) => { + return { + queryKey: ["organization", orgId, "provisionerjobs"], + queryFn: () => API.getProvisionerJobs(orgId), + }; +}; + /** * Fetch permissions for all provided organizations. * diff --git a/site/src/pages/DeploymentSettingsPage/ProvisionersPage/ProvisionersPage.tsx b/site/src/pages/DeploymentSettingsPage/ProvisionersPage/ProvisionersPage.tsx deleted file mode 100644 index 6bee4e485a12e..0000000000000 --- a/site/src/pages/DeploymentSettingsPage/ProvisionersPage/ProvisionersPage.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { AvatarFallback } from "@radix-ui/react-avatar"; -import type { ProvisionerJob } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; -import { Badge } from "components/Badge/Badge"; -import { Button } from "components/Button/Button"; -import { Link } from "components/Link/Link"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "components/Table/Table"; -import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; -import { BanIcon } from "lucide-react"; -import type { FC } from "react"; -import { docs } from "utils/docs"; - -export const ProvisionersPage: FC = () => { - return ( -
-
-
-

Provisioners

-
-
- -
- - - - Jobs - - - Daemons - - - - -
- -
-
-
- ); -}; - -const JobsTabContent: FC = () => { - return ( -
-

- Provisioner Jobs are the individual tasks assigned to Provisioners when - the workspaces are being built.{" "} - View docs -

- - - - - Last seen - Name - Template - Tags - Status - - - - - 5 min ago - - workspace_build - - -
- - Write Coder on Coder -
-
- - [foo=bar] - - Completed - - - -
-
-
-
- ); -}; - -export default ProvisionersPage; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx index 5a4965c039e1f..5f5227f36dc9f 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx @@ -1,25 +1,52 @@ import { buildInfo } from "api/queries/buildInfo"; -import { provisionerDaemonGroups } from "api/queries/organizations"; +import { + provisionerDaemonGroups, + provisionerJobs, +} from "api/queries/organizations"; +import type { Organization } from "api/typesGenerated"; +import { Avatar } from "components/Avatar/Avatar"; +import { Badge } from "components/Badge/Badge"; +import { Button } from "components/Button/Button"; import { EmptyState } from "components/EmptyState/EmptyState"; +import { Link } from "components/Link/Link"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "components/Table/Table"; +import { TableEmpty } from "components/TableEmpty/TableEmpty"; +import { TableLoader } from "components/TableLoader/TableLoader"; +import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; +import { BanIcon } from "lucide-react"; import { useDashboard } from "modules/dashboard/useDashboard"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; import { useParams } from "react-router-dom"; +import { docs } from "utils/docs"; import { pageTitle } from "utils/page"; -import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView"; +import { relativeTime } from "utils/time"; const OrganizationProvisionersPage: FC = () => { - const { organization: organizationName } = useParams() as { - organization: string; - }; + // const { organization: organizationName } = useParams() as { + // organization: string; + // }; const { organization } = useOrganizationSettings(); - const { entitlements } = useDashboard(); - const { metadata } = useEmbeddedMetadata(); - const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); - const provisionersQuery = useQuery(provisionerDaemonGroups(organizationName)); + // const { entitlements } = useDashboard(); + // const { metadata } = useEmbeddedMetadata(); + // const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); + // const provisionersQuery = useQuery(provisionerDaemonGroups(organizationName)); if (!organization) { return ; @@ -35,14 +62,124 @@ const OrganizationProvisionersPage: FC = () => { )} - + +
+
+
+

Provisioners

+
+
+ +
+ + + + Jobs + + + Daemons + + + + +
+ +
+
+
); }; +type JobsTabContentProps = { + org: Organization; +}; + +const JobsTabContent: FC = ({ org }) => { + const { organization } = useOrganizationSettings(); + const { data: jobs, isLoadingError } = useQuery(provisionerJobs(org.id)); + + return ( +
+

+ Provisioner Jobs are the individual tasks assigned to Provisioners when + the workspaces are being built.{" "} + View docs +

+ + + + + Last seen + Type + Template + Tags + Status + + + + {jobs ? ( + jobs.length > 0 ? ( + jobs.map(({ metadata, ...job }) => { + if (!metadata) { + throw new Error( + `Metadata is required but it is missing in the job ${job.id}`, + ); + } + return ( + + + {relativeTime(new Date(job.created_at))} + + + {job.type} + + +
+ + {metadata.template_display_name ?? + metadata.template_name} +
+
+ + [foo=bar] + + Completed + + + + + + + Cancel job + + + +
+ ); + }) + ) : ( + + ) + ) : isLoadingError ? ( + + ) : ( + + )} +
+
+
+ ); +}; + export default OrganizationProvisionersPage; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.stories.tsx deleted file mode 100644 index 5bbf6cfe81731..0000000000000 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.stories.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { screen, userEvent } from "@storybook/test"; -import { - MockBuildInfo, - MockProvisioner, - MockProvisioner2, - MockProvisionerBuiltinKey, - MockProvisionerKey, - MockProvisionerPskKey, - MockProvisionerUserAuthKey, - MockProvisionerWithTags, - MockUserProvisioner, - mockApiError, -} from "testHelpers/entities"; -import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView"; - -const meta: Meta = { - title: "pages/OrganizationProvisionersPage", - component: OrganizationProvisionersPageView, - args: { - buildInfo: MockBuildInfo, - }, -}; - -export default meta; -type Story = StoryObj; - -export const Provisioners: Story = { - args: { - provisioners: [ - { - key: MockProvisionerBuiltinKey, - daemons: [MockProvisioner, MockProvisioner2], - }, - { - key: MockProvisionerPskKey, - daemons: [ - MockProvisioner, - MockUserProvisioner, - MockProvisionerWithTags, - ], - }, - { - key: MockProvisionerPskKey, - daemons: [MockProvisioner, MockProvisioner2], - }, - { - key: { ...MockProvisionerKey, id: "ジェイデン", name: "ジェイデン" }, - daemons: [ - MockProvisioner, - { ...MockProvisioner2, tags: { scope: "organization", owner: "" } }, - ], - }, - { - key: { ...MockProvisionerKey, id: "ベン", name: "ベン" }, - daemons: [ - MockProvisioner, - { - ...MockProvisioner2, - version: "2.0.0", - api_version: "1.0", - }, - ], - }, - { - key: { - ...MockProvisionerKey, - id: "ケイラ", - name: "ケイラ", - tags: { - ...MockProvisioner.tags, - 都市: "ユタ", - きっぷ: "yes", - ちいさい: "no", - }, - }, - daemons: Array.from({ length: 117 }, (_, i) => ({ - ...MockProvisioner, - id: `ケイラ-${i}`, - name: `ケイラ-${i}`, - })), - }, - { - key: MockProvisionerUserAuthKey, - daemons: [ - MockUserProvisioner, - { - ...MockUserProvisioner, - id: "mock-user-provisioner-2", - name: "Test User Provisioner 2", - }, - ], - }, - ], - }, - play: async ({ step }) => { - await step("open all details", async () => { - const expandButtons = await screen.findAllByRole("button", { - name: "Show provisioner details", - }); - for (const it of expandButtons) { - await userEvent.click(it); - } - }); - - await step("close uninteresting/large details", async () => { - const collapseButtons = await screen.findAllByRole("button", { - name: "Hide provisioner details", - }); - - await userEvent.click(collapseButtons[2]); - await userEvent.click(collapseButtons[3]); - await userEvent.click(collapseButtons[5]); - }); - - await step("show version popover", async () => { - const outOfDate = await screen.findByText("Out of date"); - await userEvent.hover(outOfDate); - }); - }, -}; - -export const Empty: Story = { - args: { - provisioners: [], - }, -}; - -export const WithError: Story = { - args: { - error: mockApiError({ - message: "Fern is mad", - detail: "Frieren slept in and didn't get groceries", - }), - }, -}; - -export const Paywall: Story = { - args: { - showPaywall: true, - }, -}; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.tsx deleted file mode 100644 index 649a75836b603..0000000000000 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPageView.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import OpenInNewIcon from "@mui/icons-material/OpenInNew"; -import Button from "@mui/material/Button"; -import type { - BuildInfoResponse, - ProvisionerKey, - ProvisionerKeyDaemons, -} from "api/typesGenerated"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { EmptyState } from "components/EmptyState/EmptyState"; -import { Loader } from "components/Loader/Loader"; -import { Paywall } from "components/Paywall/Paywall"; -import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; -import { Stack } from "components/Stack/Stack"; -import { ProvisionerGroup } from "modules/provisioners/ProvisionerGroup"; -import type { FC } from "react"; -import { docs } from "utils/docs"; - -interface OrganizationProvisionersPageViewProps { - /** Determines if the paywall will be shown or not */ - showPaywall?: boolean; - - /** An error to display instead of the page content */ - error?: unknown; - - /** Info about the version of coderd */ - buildInfo?: BuildInfoResponse; - - /** Groups of provisioners, along with their key information */ - provisioners?: readonly ProvisionerKeyDaemons[]; -} - -export const OrganizationProvisionersPageView: FC< - OrganizationProvisionersPageViewProps -> = ({ showPaywall, error, buildInfo, provisioners }) => { - return ( -
- - - {!showPaywall && ( - - )} - - {showPaywall ? ( - - ) : error ? ( - - ) : !buildInfo || !provisioners ? ( - - ) : ( - - )} -
- ); -}; - -type ViewContentProps = Required< - Pick ->; - -const ViewContent: FC = ({ buildInfo, provisioners }) => { - const isEmpty = provisioners.every((group) => group.daemons.length === 0); - - const provisionerGroupsCount = provisioners.length; - const provisionersCount = provisioners.reduce( - (a, group) => a + group.daemons.length, - 0, - ); - - return ( - <> - {isEmpty ? ( - } - target="_blank" - href={docs("/admin/provisioners")} - > - Create a provisioner - - } - /> - ) : ( -
({ - margin: 0, - fontSize: 12, - paddingBottom: 18, - color: theme.palette.text.secondary, - })} - > - Showing {provisionerGroupsCount} groups and {provisionersCount}{" "} - provisioners -
- )} - - {provisioners.map((group) => ( - - ))} - - - ); -}; - -// Ideally these would be generated and appear in typesGenerated.ts, but that is -// not currently the case. In the meantime, these are taken from verbatim from -// the corresponding codersdk declarations. The names remain unchanged to keep -// usage of these special values "grep-able". -// https://github.com/coder/coder/blob/7c77a3cc832fb35d9da4ca27df163c740f786137/codersdk/provisionerdaemons.go#L291-L295 -const ProvisionerKeyIDBuiltIn = "00000000-0000-0000-0000-000000000001"; -const ProvisionerKeyIDUserAuth = "00000000-0000-0000-0000-000000000002"; -const ProvisionerKeyIDPSK = "00000000-0000-0000-0000-000000000003"; - -function getGroupType(key: ProvisionerKey) { - switch (key.id) { - case ProvisionerKeyIDBuiltIn: - return "builtin"; - case ProvisionerKeyIDUserAuth: - return "userAuth"; - case ProvisionerKeyIDPSK: - return "psk"; - default: - return "key"; - } -} diff --git a/site/src/router.tsx b/site/src/router.tsx index c56bd1ee27ca1..acaf417cecbcd 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -304,10 +304,6 @@ const ChangePasswordPage = lazy( const IdpOrgSyncPage = lazy( () => import("./pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage"), ); -const ProvisionersPage = lazy( - () => - import("./pages/DeploymentSettingsPage/ProvisionersPage/ProvisionersPage"), -); const RoutesWithSuspense = () => { return ( @@ -456,7 +452,6 @@ export const router = createBrowserRouter( /> } /> } /> - } /> diff --git a/site/src/utils/time.ts b/site/src/utils/time.ts index 3b945c665769f..f890cd3f7a6ea 100644 --- a/site/src/utils/time.ts +++ b/site/src/utils/time.ts @@ -1,3 +1,10 @@ +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import DayJSRelativeTime from "dayjs/plugin/relativeTime"; + +dayjs.extend(duration); +dayjs.extend(DayJSRelativeTime); + export type TimeUnit = "days" | "hours"; export function humanDuration(durationInMs: number) { @@ -29,3 +36,7 @@ export function durationInHours(duration: number): number { export function durationInDays(duration: number): number { return duration / 1000 / 60 / 60 / 24; } + +export function relativeTime(date: Date) { + return dayjs(date).fromNow(); +} From 6e967f10ec5f61575c8b4531fa680b6f17056a35 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 10 Feb 2025 13:38:35 +0000 Subject: [PATCH 03/15] Update table to use API data --- .../OrganizationProvisionersPage.tsx | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx index 5f5227f36dc9f..f588086ece241 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx @@ -3,12 +3,17 @@ import { provisionerDaemonGroups, provisionerJobs, } from "api/queries/organizations"; -import type { Organization } from "api/typesGenerated"; +import type { Organization, ProvisionerJobStatus } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { Badge } from "components/Badge/Badge"; import { Button } from "components/Button/Button"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Link } from "components/Link/Link"; +import { + StatusIndicator, + StatusIndicatorDot, + type StatusIndicatorProps, +} from "components/StatusIndicator/StatusIndicator"; import { Table, TableBody, @@ -27,7 +32,7 @@ import { TooltipTrigger, } from "components/Tooltip/Tooltip"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; -import { BanIcon } from "lucide-react"; +import { BanIcon, TriangleAlertIcon } from "lucide-react"; import { useDashboard } from "modules/dashboard/useDashboard"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import type { FC } from "react"; @@ -110,7 +115,7 @@ const JobsTabContent: FC = ({ org }) => { - Last seen + Created Type Template Tags @@ -126,6 +131,9 @@ const JobsTabContent: FC = ({ org }) => { `Metadata is required but it is missing in the job ${job.id}`, ); } + + const canCancel = ["pending", "running"].includes(job.status); + return ( @@ -138,8 +146,11 @@ const JobsTabContent: FC = ({ org }) => {
{metadata.template_display_name ?? metadata.template_name} @@ -148,12 +159,28 @@ const JobsTabContent: FC = ({ org }) => { [foo=bar] - Completed + + + + + {job.status} + + {job.status === "failed" && ( + + )} + {job.status === "pending" && + `(${job.queue_position}/${job.queue_size})`} + +
@@ -101,7 +117,6 @@ type JobsTabContentProps = { }; const JobsTabContent: FC = ({ org }) => { - const { organization } = useOrganizationSettings(); const { data: jobs, isLoadingError } = useQuery(provisionerJobs(org.id)); return ( @@ -125,76 +140,7 @@ const JobsTabContent: FC = ({ org }) => { {jobs ? ( jobs.length > 0 ? ( - jobs.map(({ metadata, ...job }) => { - if (!metadata) { - throw new Error( - `Metadata is required but it is missing in the job ${job.id}`, - ); - } - - const canCancel = ["pending", "running"].includes(job.status); - - return ( - - - {relativeTime(new Date(job.created_at))} - - - {job.type} - - -
- - {metadata.template_display_name ?? - metadata.template_name} -
-
- - [foo=bar] - - - - - - {job.status} - - {job.status === "failed" && ( - - )} - {job.status === "pending" && - `(${job.queue_position}/${job.queue_size})`} - - - - - - - - - Cancel job - - - -
- ); - }) + jobs.map((j) => ) ) : ( ) @@ -209,6 +155,136 @@ const JobsTabContent: FC = ({ org }) => { ); }; +type JobRowProps = { + job: ProvisionerJob; +}; + +const JobRow: FC = ({ job }) => { + const metadata = job.metadata; + const canCancel = ["pending", "running"].includes(job.status); + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + + + + + + {job.type} + + + {job.metadata.template_name ? ( +
+ + {metadata.template_display_name ?? metadata.template_name} +
+ ) : ( + "Not linked to any template" + )} +
+ + [foo=bar] + + + + + {job.status} + {job.status === "failed" && ( + + )} + {job.status === "pending" && + `(${job.queue_position}/${job.queue_size})`} + + + + + + + + + Cancel job + + + +
+ + {isOpen && ( + + +
+ Job ID: + {job.id} + + Available provisioners: + + {job.available_workers + ? JSON.stringify(job.available_workers) + : "[]"} + + + Completed by provisioner: + {job.worker_id} + + Associated workspace: + {job.metadata.workspace_name ?? "null"} + + Creation time: + {job.created_at} + + Queue: + + {job.queue_position}/{job.queue_size} + +
+
+
+ )} + + ); +}; + function statusIndicatorVariant( status: ProvisionerJobStatus, ): StatusIndicatorProps["variant"] { From 2bc6ccfe2d87c7bba15b8bb78e77e5704cd13eda Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 10 Feb 2025 14:51:34 +0000 Subject: [PATCH 05/15] Display tiny alert for error --- .../OrganizationProvisionersPage.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx index 2014369bad290..9c76989d2f0cb 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx @@ -247,6 +247,17 @@ const JobRow: FC = ({ job }) => { {isOpen && ( + {job.status === "failed" && ( +
+ + {job.error} +
+ )}
Date: Mon, 10 Feb 2025 16:47:49 +0000 Subject: [PATCH 06/15] Fix tags --- .../OrganizationProvisionersPage.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx index 9c76989d2f0cb..7fa761e022254 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx @@ -171,7 +171,7 @@ const JobRow: FC = ({ job }) => {
+ + + Last seen + Name + Template + Tags + Status + + + + {daemons ? ( + daemons.length > 0 ? ( + daemons.map((d) => ) + ) : ( + + ) + ) : isLoadingError ? ( + + ) : ( + + )} + +
+ + ); +}; + +type DaemonRowProps = { + daemon: ProvisionerDaemon; +}; + +const DaemonRow: FC = ({ daemon }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + + + + + {daemon.name} + Template + +
+ {Object.entries(daemon.tags).map(([k, v]) => ( + + [{k} + {v && `=${v}`}] + + ))} +
+
+ + + + {daemon.status} + + +
+ + {isOpen && ( + + +
+ Last seen: + {daemon.last_seen_at} + + Creation time: + {daemon.created_at} + + Version: + {daemon.version} + + {daemon.current_job && ( + <> + Last job: + {daemon.current_job.id} + + Last job state: + + + + + )} + + {daemon.previous_job && ( + <> + Previous job: + {daemon.previous_job.id} + + Previous job state: + + + + + )} +
+
+
+ )} + + ); +}; + +function statusIndicatorVariant( + status: ProvisionerDaemonStatus | null, +): StatusIndicatorProps["variant"] { + switch (status) { + case "idle": + return "success"; + case "busy": + return "pending"; + case "offline": + case null: + return "inactive"; + } +} diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx similarity index 63% rename from site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx rename to site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx index 7fa761e022254..e0cccce8551c3 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx @@ -1,23 +1,9 @@ -import { buildInfo } from "api/queries/buildInfo"; -import { - provisionerDaemonGroups, - provisionerJobs, -} from "api/queries/organizations"; -import type { - Organization, - ProvisionerJob, - ProvisionerJobStatus, -} from "api/typesGenerated"; +import { provisionerJobs } from "api/queries/organizations"; +import type { Organization, ProvisionerJob } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { Badge } from "components/Badge/Badge"; import { Button } from "components/Button/Button"; -import { EmptyState } from "components/EmptyState/EmptyState"; import { Link } from "components/Link/Link"; -import { - StatusIndicator, - StatusIndicatorDot, - type StatusIndicatorProps, -} from "components/StatusIndicator/StatusIndicator"; import { Table, TableBody, @@ -28,95 +14,30 @@ import { } from "components/Table/Table"; import { TableEmpty } from "components/TableEmpty/TableEmpty"; import { TableLoader } from "components/TableLoader/TableLoader"; -import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "components/Tooltip/Tooltip"; -import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; -import { useSearchParamsKey } from "hooks/useSearchParamsKey"; import { BanIcon, ChevronDownIcon, ChevronRightIcon, - Tangent, TriangleAlertIcon, } from "lucide-react"; -import { useDashboard } from "modules/dashboard/useDashboard"; -import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import { useState, type FC } from "react"; -import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; -import { useParams } from "react-router-dom"; import { cn } from "utils/cn"; import { docs } from "utils/docs"; -import { pageTitle } from "utils/page"; import { relativeTime } from "utils/time"; +import { JobStatusIndicator } from "./JobStatusIndicator"; -const OrganizationProvisionersPage: FC = () => { - // const { organization: organizationName } = useParams() as { - // organization: string; - // }; - const { organization } = useOrganizationSettings(); - const tab = useSearchParamsKey({ - key: "tab", - defaultValue: "jobs", - }); - // const { entitlements } = useDashboard(); - // const { metadata } = useEmbeddedMetadata(); - // const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); - // const provisionersQuery = useQuery(provisionerDaemonGroups(organizationName)); - - if (!organization) { - return ; - } - - return ( - <> - - - {pageTitle( - "Provisioners", - organization.display_name || organization.name, - )} - - - -
-
-
-

Provisioners

-
-
- -
- - - - Jobs - - - Daemons - - - - -
- {tab.value === "jobs" && } -
-
-
- - ); -}; - -type JobsTabContentProps = { +type ProvisionerJobsPageProps = { org: Organization; }; -const JobsTabContent: FC = ({ org }) => { +export const ProvisionerJobsPage: FC = ({ org }) => { const { data: jobs, isLoadingError } = useQuery(provisionerJobs(org.id)); return ( @@ -135,6 +56,7 @@ const JobsTabContent: FC = ({ org }) => { Template Tags Status + @@ -219,18 +141,7 @@ const JobRow: FC = ({ job }) => {
- - - {job.status} - {job.status === "failed" && ( - - )} - {job.status === "pending" && - `(${job.queue_position}/${job.queue_size})`} - + @@ -302,23 +213,3 @@ const JobRow: FC = ({ job }) => { ); }; - -function statusIndicatorVariant( - status: ProvisionerJobStatus, -): StatusIndicatorProps["variant"] { - switch (status) { - case "succeeded": - return "success"; - case "failed": - return "failed"; - case "pending": - case "running": - case "canceling": - return "pending"; - case "canceled": - case "unknown": - return "inactive"; - } -} - -export default OrganizationProvisionersPage; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx new file mode 100644 index 0000000000000..3751015d1830f --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx @@ -0,0 +1,64 @@ +import { EmptyState } from "components/EmptyState/EmptyState"; +import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; +import { useSearchParamsKey } from "hooks/useSearchParamsKey"; +import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; +import type { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { ProvisionerJobsPage } from "./ProvisionerJobsPage"; +import { ProvisionerDaemonsPage } from "./ProvisionerDaemonsPage"; + +const ProvisionersPage: FC = () => { + const { organization } = useOrganizationSettings(); + const tab = useSearchParamsKey({ + key: "tab", + defaultValue: "jobs", + }); + + if (!organization) { + return ; + } + + return ( + <> + + + {pageTitle( + "Provisioners", + organization.display_name || organization.name, + )} + + + +
+
+
+

Provisioners

+
+
+ +
+ + + + Jobs + + + Daemons + + + + +
+ {tab.value === "jobs" && } + {tab.value === "daemons" && ( + + )} +
+
+
+ + ); +}; + +export default ProvisionersPage; diff --git a/site/src/router.tsx b/site/src/router.tsx index acaf417cecbcd..7e7776eeecf18 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -261,8 +261,11 @@ const CreateEditRolePage = lazy( "./pages/OrganizationSettingsPage/CustomRolesPage/CreateEditRolePage" ), ); -const OrganizationProvisionersPage = lazy( - () => import("./pages/OrganizationSettingsPage/OrganizationProvisionersPage"), +const ProvisionersPage = lazy( + () => + import( + "./pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage" + ), ); const TemplateEmbedPage = lazy( () => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"), @@ -422,10 +425,7 @@ export const router = createBrowserRouter( } /> } /> - } - /> + } /> } /> } /> From d66141e74b7d3404466e2754b7fb2cce22534f11 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 11 Feb 2025 19:57:26 +0000 Subject: [PATCH 08/15] Display all daemon data from server --- site/src/api/queries/organizations.ts | 1 + .../ProvisionersPage/DataGrid.tsx | 28 ++++ .../ProvisionerDaemonsPage.tsx | 137 +++++++++++++----- .../ProvisionersPage/ProvisionerJobsPage.tsx | 53 ++++--- .../ProvisionersPage/Tags.tsx | 52 +++++++ 5 files changed, 212 insertions(+), 59 deletions(-) create mode 100644 site/src/pages/OrganizationSettingsPage/ProvisionersPage/DataGrid.tsx create mode 100644 site/src/pages/OrganizationSettingsPage/ProvisionersPage/Tags.tsx diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index f99976fcd2422..b288e5336e3bb 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -3,6 +3,7 @@ import type { AuthorizationResponse, CreateOrganizationRequest, GroupSyncSettings, + ProvisionerDaemon, RoleSyncSettings, UpdateOrganizationRequest, } from "api/typesGenerated"; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/DataGrid.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/DataGrid.tsx new file mode 100644 index 0000000000000..bea97aebc466c --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/DataGrid.tsx @@ -0,0 +1,28 @@ +import type { FC, HTMLProps } from "react"; +import { cn } from "utils/cn"; + +export const DataGrid: FC> = ({ + className, + ...props +}) => { + return ( +
+ ); +}; + +export const DataGridSpace: FC> = ({ + className, + ...props +}) => { + return ( +
+ ); +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx index a01ac63680216..68453b02d30b5 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx @@ -1,10 +1,5 @@ import { provisionerDaemons } from "api/queries/organizations"; -import type { - Organization, - ProvisionerDaemon, - ProvisionerDaemonStatus, -} from "api/typesGenerated"; -import { Badge } from "components/Badge/Badge"; +import type { Organization, ProvisionerDaemon } from "api/typesGenerated"; import { Link } from "components/Link/Link"; import { StatusIndicator, @@ -20,7 +15,6 @@ import { TableRow, } from "components/Table/Table"; import { TableEmpty } from "components/TableEmpty/TableEmpty"; -import { TableLoader } from "components/TableLoader/TableLoader"; import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"; import { useState, type FC } from "react"; import { useQuery } from "react-query"; @@ -28,6 +22,11 @@ import { cn } from "utils/cn"; import { docs } from "utils/docs"; import { relativeTime } from "utils/time"; import { JobStatusIndicator } from "./JobStatusIndicator"; +import { Avatar } from "components/Avatar/Avatar"; +import { DataGrid, DataGridSpace } from "./DataGrid"; +import { ShrinkTags, Tag, Tags } from "./Tags"; +import { Loader } from "components/Loader/Loader"; +import { EmptyState } from "components/EmptyState/EmptyState"; type ProvisionerDaemonsPageProps = { org: Organization; @@ -36,9 +35,18 @@ type ProvisionerDaemonsPageProps = { export const ProvisionerDaemonsPage: FC = ({ org, }) => { - const { data: daemons, isLoadingError } = useQuery( - provisionerDaemons(org.id), - ); + const { data: daemons, isLoadingError } = useQuery({ + ...provisionerDaemons(org.id), + select: (data) => + data.toSorted((a, b) => { + if (!a.last_seen_at) return 1; + if (!b.last_seen_at) return -1; + return ( + new Date(b.last_seen_at).getTime() - + new Date(a.last_seen_at).getTime() + ); + }), + }); return (
@@ -69,12 +77,24 @@ export const ProvisionerDaemonsPage: FC = ({ daemons.length > 0 ? ( daemons.map((d) => ) ) : ( - + + + + + ) ) : isLoadingError ? ( - + + + + + ) : ( - + + + + + )} @@ -116,25 +136,38 @@ const DaemonRow: FC = ({ daemon }) => { - {daemon.name} - Template -
- {Object.entries(daemon.tags).map(([k, v]) => ( - - [{k} - {v && `=${v}`}] - - ))} -
+ + {daemon.name} +
- + {daemon.current_job ? ( +
+ + {daemon.current_job.template_display_name ?? + daemon.current_job.template_name} +
+ ) : ( + Not linked + )} +
+ + + + + - {daemon.status} + + {statusLabel(daemon)} + @@ -142,13 +175,7 @@ const DaemonRow: FC = ({ daemon }) => { {isOpen && ( -
+ Last seen: {daemon.last_seen_at} @@ -158,8 +185,19 @@ const DaemonRow: FC = ({ daemon }) => { Version: {daemon.version} + Tags: + + + {Object.entries(daemon.tags).map(([key, value]) => ( + + ))} + + + {daemon.current_job && ( <> + + Last job: {daemon.current_job.id} @@ -172,6 +210,8 @@ const DaemonRow: FC = ({ daemon }) => { {daemon.previous_job && ( <> + + Previous job: {daemon.previous_job.id} @@ -181,7 +221,7 @@ const DaemonRow: FC = ({ daemon }) => { )} -
+
)} @@ -190,9 +230,13 @@ const DaemonRow: FC = ({ daemon }) => { }; function statusIndicatorVariant( - status: ProvisionerDaemonStatus | null, + daemon: ProvisionerDaemon, ): StatusIndicatorProps["variant"] { - switch (status) { + if (daemon.previous_job && daemon.previous_job.status === "failed") { + return "failed"; + } + + switch (daemon.status) { case "idle": return "success"; case "busy": @@ -202,3 +246,20 @@ function statusIndicatorVariant( return "inactive"; } } + +function statusLabel(daemon: ProvisionerDaemon) { + if (daemon.previous_job && daemon.previous_job.status === "failed") { + return "Last job failed"; + } + + switch (daemon.status) { + case "idle": + return "Idle"; + case "busy": + return "Busy..."; + case "offline": + return "Disconnected"; + case null: + return "Unknown"; + } +} diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx index e0cccce8551c3..95b42fc07bdef 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx @@ -13,7 +13,6 @@ import { TableRow, } from "components/Table/Table"; import { TableEmpty } from "components/TableEmpty/TableEmpty"; -import { TableLoader } from "components/TableLoader/TableLoader"; import { Tooltip, TooltipContent, @@ -32,6 +31,10 @@ import { cn } from "utils/cn"; import { docs } from "utils/docs"; import { relativeTime } from "utils/time"; import { JobStatusIndicator } from "./JobStatusIndicator"; +import { DataGrid } from "./DataGrid"; +import { ShrinkTags, Tag, Tags } from "./Tags"; +import { Loader } from "components/Loader/Loader"; +import { EmptyState } from "components/EmptyState/EmptyState"; type ProvisionerJobsPageProps = { org: Organization; @@ -64,12 +67,24 @@ export const ProvisionerJobsPage: FC = ({ org }) => { jobs.length > 0 ? ( jobs.map((j) => ) ) : ( - + + + + + ) ) : isLoadingError ? ( - + + + + + ) : ( - + + + + + )} @@ -127,18 +142,11 @@ const JobRow: FC = ({ job }) => { {metadata.template_display_name ?? metadata.template_name}
) : ( - "Not linked to any template" + Not linked )} -
- {Object.entries(job.tags).map(([k, v]) => ( - - [{k} - {v && `=${v}`}] - - ))} -
+
@@ -176,13 +184,7 @@ const JobRow: FC = ({ job }) => { {job.error}
)} -
+ Job ID: {job.id} @@ -206,7 +208,16 @@ const JobRow: FC = ({ job }) => { {job.queue_position}/{job.queue_size} -
+ + Tags: + + + {Object.entries(job.tags).map(([key, value]) => ( + + ))} + + +
)} diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/Tags.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/Tags.tsx new file mode 100644 index 0000000000000..7cb48b22df1a6 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/Tags.tsx @@ -0,0 +1,52 @@ +import { Badge } from "components/Badge/Badge"; +import type { FC, HTMLProps } from "react"; +import { cn } from "utils/cn"; + +export const Tags: FC> = ({ + className, + ...props +}) => { + return ( +
+ ); +}; + +type TagProps = { + label: string; + value?: string; +}; + +export const Tag: FC = ({ label, value }) => { + return ( + + [{label} + {value && `=${value}`}] + + ); +}; + +type TagsProps = { + tags: Record; +}; + +export const ShrinkTags: FC = ({ tags }) => { + const keys = Object.keys(tags); + + if (keys.length === 0) { + return null; + } + + const firstKey = keys[0]; + const firstValue = tags[firstKey]; + const restKeys = keys.slice(1); + + return ( + + + {restKeys.length > 0 && +{restKeys.length}} + + ); +}; From 49a7ec7ab7e0458f02d3df398ac96340e3981371 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 11 Feb 2025 19:57:46 +0000 Subject: [PATCH 09/15] Remove unused imports --- .../ProvisionersPage/ProvisionerDaemonsPage.tsx | 1 - .../ProvisionersPage/ProvisionerJobsPage.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx index 68453b02d30b5..bce739417088e 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx @@ -14,7 +14,6 @@ import { TableHeader, TableRow, } from "components/Table/Table"; -import { TableEmpty } from "components/TableEmpty/TableEmpty"; import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"; import { useState, type FC } from "react"; import { useQuery } from "react-query"; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx index 95b42fc07bdef..ee5645585e145 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx @@ -12,7 +12,6 @@ import { TableHeader, TableRow, } from "components/Table/Table"; -import { TableEmpty } from "components/TableEmpty/TableEmpty"; import { Tooltip, TooltipContent, From ffee2ed2f8af00f2d3b5a529cd135b9e09946d3c Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 11 Feb 2025 20:05:17 +0000 Subject: [PATCH 10/15] Run fmt --- .../ProvisionersPage/ProvisionerDaemonsPage.tsx | 10 +++++----- .../ProvisionersPage/ProvisionerJobsPage.tsx | 8 ++++---- .../ProvisionersPage/ProvisionersPage.tsx | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx index bce739417088e..ed1603092f3ac 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx @@ -1,6 +1,9 @@ import { provisionerDaemons } from "api/queries/organizations"; import type { Organization, ProvisionerDaemon } from "api/typesGenerated"; +import { Avatar } from "components/Avatar/Avatar"; +import { EmptyState } from "components/EmptyState/EmptyState"; import { Link } from "components/Link/Link"; +import { Loader } from "components/Loader/Loader"; import { StatusIndicator, StatusIndicatorDot, @@ -15,17 +18,14 @@ import { TableRow, } from "components/Table/Table"; import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"; -import { useState, type FC } from "react"; +import { type FC, useState } from "react"; import { useQuery } from "react-query"; import { cn } from "utils/cn"; import { docs } from "utils/docs"; import { relativeTime } from "utils/time"; -import { JobStatusIndicator } from "./JobStatusIndicator"; -import { Avatar } from "components/Avatar/Avatar"; import { DataGrid, DataGridSpace } from "./DataGrid"; +import { JobStatusIndicator } from "./JobStatusIndicator"; import { ShrinkTags, Tag, Tags } from "./Tags"; -import { Loader } from "components/Loader/Loader"; -import { EmptyState } from "components/EmptyState/EmptyState"; type ProvisionerDaemonsPageProps = { org: Organization; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx index ee5645585e145..c64e84e6bbf48 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx @@ -3,7 +3,9 @@ import type { Organization, ProvisionerJob } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { Badge } from "components/Badge/Badge"; import { Button } from "components/Button/Button"; +import { EmptyState } from "components/EmptyState/EmptyState"; import { Link } from "components/Link/Link"; +import { Loader } from "components/Loader/Loader"; import { Table, TableBody, @@ -24,16 +26,14 @@ import { ChevronRightIcon, TriangleAlertIcon, } from "lucide-react"; -import { useState, type FC } from "react"; +import { type FC, useState } from "react"; import { useQuery } from "react-query"; import { cn } from "utils/cn"; import { docs } from "utils/docs"; import { relativeTime } from "utils/time"; -import { JobStatusIndicator } from "./JobStatusIndicator"; import { DataGrid } from "./DataGrid"; +import { JobStatusIndicator } from "./JobStatusIndicator"; import { ShrinkTags, Tag, Tags } from "./Tags"; -import { Loader } from "components/Loader/Loader"; -import { EmptyState } from "components/EmptyState/EmptyState"; type ProvisionerJobsPageProps = { org: Organization; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx index 3751015d1830f..1612073481ead 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx @@ -5,8 +5,8 @@ import { useOrganizationSettings } from "modules/management/OrganizationSettings import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { pageTitle } from "utils/page"; -import { ProvisionerJobsPage } from "./ProvisionerJobsPage"; import { ProvisionerDaemonsPage } from "./ProvisionerDaemonsPage"; +import { ProvisionerJobsPage } from "./ProvisionerJobsPage"; const ProvisionersPage: FC = () => { const { organization } = useOrganizationSettings(); From 7802636aebe26a6f4a5d73bfbf2dbec25ada85b1 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 12 Feb 2025 18:32:13 +0000 Subject: [PATCH 11/15] Add cancel provisioner job --- site/src/api/api.ts | 36 +++++++ site/src/api/queries/organizations.ts | 9 +- .../CancelJobButton.stories.tsx | 46 +++++++++ .../ProvisionersPage/CancelJobButton.tsx | 55 +++++++++++ .../CancelJobConfirmationDialog.stories.tsx | 98 +++++++++++++++++++ .../CancelJobConfirmationDialog.tsx | 69 +++++++++++++ .../ProvisionersPage/ProvisionerJobsPage.tsx | 26 +---- 7 files changed, 313 insertions(+), 26 deletions(-) create mode 100644 site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.stories.tsx create mode 100644 site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx create mode 100644 site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.stories.tsx create mode 100644 site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.tsx diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 2561248c5bf00..0dcef564f563e 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1247,6 +1247,17 @@ class ApiMethods { return response.data; }; + cancelTemplateVersionDryRun = async ( + templateVersionId: TypesGen.TemplateVersion["id"], + jobId: string, + ): Promise => { + const response = await this.axios.patch( + `/api/v2/templateversions/${templateVersionId}/dry-run/${jobId}/cancel`, + ); + + return response.data; + }; + createUser = async ( user: TypesGen.CreateUserRequestWithOrgs, ): Promise => { @@ -2302,6 +2313,31 @@ class ApiMethods { ); return res.data; }; + + cancelProvisionerJob = async (job: TypesGen.ProvisionerJob) => { + switch (job.type) { + case "workspace_build": + if (!job.input.workspace_build_id) { + throw new Error("Workspace build ID is required to cancel this job"); + } + return this.cancelWorkspaceBuild(job.input.workspace_build_id); + + case "template_version_import": + if (!job.input.template_version_id) { + throw new Error("Template version ID is required to cancel this job"); + } + return this.cancelTemplateVersionBuild(job.input.template_version_id); + + case "template_version_dry_run": + if (!job.input.template_version_id) { + throw new Error("Template version ID is required to cancel this job"); + } + return this.cancelTemplateVersionDryRun( + job.input.template_version_id, + job.id, + ); + } + }; } // This is a hard coded CSRF token/cookie pair for local development. In prod, diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index b288e5336e3bb..70cd57628f578 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -3,7 +3,6 @@ import type { AuthorizationResponse, CreateOrganizationRequest, GroupSyncSettings, - ProvisionerDaemon, RoleSyncSettings, UpdateOrganizationRequest, } from "api/typesGenerated"; @@ -245,9 +244,15 @@ export const organizationPermissions = (organizationId: string | undefined) => { }; }; +export const provisionerJobQueryKey = (orgId: string) => [ + "organization", + orgId, + "provisionerjobs", +]; + export const provisionerJobs = (orgId: string) => { return { - queryKey: ["organization", orgId, "provisionerjobs"], + queryKey: provisionerJobQueryKey(orgId), queryFn: () => API.getProvisionerJobs(orgId), }; }; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.stories.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.stories.tsx new file mode 100644 index 0000000000000..e5bb0d2ce4ff4 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.stories.tsx @@ -0,0 +1,46 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { CancelJobButton } from "./CancelJobButton"; +import { MockProvisionerJob } from "testHelpers/entities"; +import { userEvent, waitFor, within } from "@storybook/test"; + +const meta: Meta = { + title: "pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton", + component: CancelJobButton, + args: { + job: { + ...MockProvisionerJob, + status: "running", + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Cancellable: Story = {}; + +export const NotCancellable: Story = { + args: { + job: { + ...MockProvisionerJob, + status: "succeeded", + }, + }, +}; + +export const OnClick: Story = { + parameters: { + chromatic: { disableSnapshot: true }, + }, + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const canvas = within(canvasElement); + const button = canvas.getByRole("button"); + await user.click(button); + + const body = within(canvasElement.ownerDocument.body); + await waitFor(() => { + body.getByText("Cancel provisioner job"); + }); + }, +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx new file mode 100644 index 0000000000000..a58fc568b0bad --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx @@ -0,0 +1,55 @@ +import { useState, type FC } from "react"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; +import { Button } from "components/Button/Button"; +import { BanIcon } from "lucide-react"; +import type { ProvisionerJob } from "api/typesGenerated"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; + +type CancelJobButtonProps = { + job: ProvisionerJob; +}; + +export const CancelJobButton: FC = ({ job }) => { + const [isDialogOpen, setIsDialogOpen] = useState(false); + const cancellable = ["pending", "running"].includes(job.status); + + return ( + <> + + + + + + Cancel job + + + + { + setIsDialogOpen(false); + }} + open={isDialogOpen} + title="Cancel provisioner job" + description={`Are you sure you want to cancel the provisioner job "${job.id}"? This operation will result in the associated workspaces not getting created.`} + confirmText="Confirm" + cancelText="Discard" + /> + + ); +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.stories.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.stories.tsx new file mode 100644 index 0000000000000..4dbe348b98b9d --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.stories.tsx @@ -0,0 +1,98 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { CancelJobConfirmationDialog } from "./CancelJobConfirmationDialog"; +import { MockProvisionerJob } from "testHelpers/entities"; +import { fn, userEvent, within, expect, waitFor } from "@storybook/test"; +import { withGlobalSnackbar } from "testHelpers/storybook"; +import type { Response } from "api/typesGenerated"; + +const meta: Meta = { + title: + "pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog", + component: CancelJobConfirmationDialog, + args: { + open: true, + onClose: fn(), + cancelProvisionerJob: fn(), + job: { + ...MockProvisionerJob, + status: "running", + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Idle: Story = {}; + +export const OnCancel: Story = { + parameters: { + chromatic: { disableSnapshot: true }, + }, + play: async ({ canvasElement, args }) => { + const user = userEvent.setup(); + const body = within(canvasElement.ownerDocument.body); + const cancelButton = body.getByRole("button", { name: "Discard" }); + user.click(cancelButton); + await waitFor(() => { + expect(args.onClose).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const onConfirmSuccess: Story = { + parameters: { + chromatic: { disableSnapshot: true }, + }, + decorators: [withGlobalSnackbar], + play: async ({ canvasElement, args }) => { + const user = userEvent.setup(); + const body = within(canvasElement.ownerDocument.body); + const confirmButton = body.getByRole("button", { name: "Confirm" }); + + user.click(confirmButton); + await waitFor(() => { + body.getByText("Provisioner job canceled successfully"); + }); + expect(args.cancelProvisionerJob).toHaveBeenCalledTimes(1); + expect(args.cancelProvisionerJob).toHaveBeenCalledWith(args.job); + expect(args.onClose).toHaveBeenCalledTimes(1); + }, +}; + +export const onConfirmFailure: Story = { + parameters: { + chromatic: { disableSnapshot: true }, + }, + decorators: [withGlobalSnackbar], + args: { + cancelProvisionerJob: fn(() => { + throw new Error("API Error"); + }), + }, + play: async ({ canvasElement, args }) => { + const user = userEvent.setup(); + const body = within(canvasElement.ownerDocument.body); + const confirmButton = body.getByRole("button", { name: "Confirm" }); + + user.click(confirmButton); + await waitFor(() => { + body.getByText("Failed to cancel provisioner job"); + }); + expect(args.cancelProvisionerJob).toHaveBeenCalledTimes(1); + expect(args.cancelProvisionerJob).toHaveBeenCalledWith(args.job); + expect(args.onClose).toHaveBeenCalledTimes(0); + }, +}; + +export const Confirming: Story = { + args: { + cancelProvisionerJob: fn(() => new Promise(() => {})), + }, + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const body = within(canvasElement.ownerDocument.body); + const confirmButton = body.getByRole("button", { name: "Confirm" }); + user.click(confirmButton); + }, +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.tsx new file mode 100644 index 0000000000000..bcfde3cf4f184 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.tsx @@ -0,0 +1,69 @@ +import type { ProvisionerJob } from "api/typesGenerated"; +import { + ConfirmDialog, + type ConfirmDialogProps, +} from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import type { FC } from "react"; +import { API } from "api/api"; +import { useMutation, useQueryClient } from "react-query"; +import { + getProvisionerDaemonsKey, + provisionerJobQueryKey, +} from "api/queries/organizations"; +import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; + +type CancelJobConfirmationDialogProps = Omit< + ConfirmDialogProps, + | "type" + | "title" + | "description" + | "confirmText" + | "cancelText" + | "onConfirm" + | "confirmLoading" +> & { + job: ProvisionerJob; + cancelProvisionerJob: typeof API.cancelProvisionerJob; +}; + +export const CancelJobConfirmationDialog: FC< + CancelJobConfirmationDialogProps +> = ({ + job, + cancelProvisionerJob = API.cancelProvisionerJob, + ...dialogProps +}) => { + const queryClient = useQueryClient(); + const cancelMutation = useMutation({ + mutationFn: cancelProvisionerJob, + onSuccess: () => { + queryClient.invalidateQueries( + provisionerJobQueryKey(job.organization_id), + ); + queryClient.invalidateQueries( + getProvisionerDaemonsKey(job.organization_id, job.tags), + ); + }, + }); + + return ( + { + try { + await cancelMutation.mutateAsync(job); + displaySuccess("Provisioner job canceled successfully"); + dialogProps.onClose(); + } catch { + displayError("Failed to cancel provisioner job"); + } + }} + /> + ); +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx index c64e84e6bbf48..52a28b5444638 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx @@ -2,7 +2,6 @@ import { provisionerJobs } from "api/queries/organizations"; import type { Organization, ProvisionerJob } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { Badge } from "components/Badge/Badge"; -import { Button } from "components/Button/Button"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Link } from "components/Link/Link"; import { Loader } from "components/Loader/Loader"; @@ -15,13 +14,6 @@ import { TableRow, } from "components/Table/Table"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "components/Tooltip/Tooltip"; -import { - BanIcon, ChevronDownIcon, ChevronRightIcon, TriangleAlertIcon, @@ -34,6 +26,7 @@ import { relativeTime } from "utils/time"; import { DataGrid } from "./DataGrid"; import { JobStatusIndicator } from "./JobStatusIndicator"; import { ShrinkTags, Tag, Tags } from "./Tags"; +import { CancelJobButton } from "./CancelJobButton"; type ProvisionerJobsPageProps = { org: Organization; @@ -97,7 +90,6 @@ type JobRowProps = { const JobRow: FC = ({ job }) => { const metadata = job.metadata; - const canCancel = ["pending", "running"].includes(job.status); const [isOpen, setIsOpen] = useState(false); return ( @@ -151,21 +143,7 @@ const JobRow: FC = ({ job }) => { - - - - - - Cancel job - - + From 4f9030fa63687c32b12c3142ba51a7e87db3d59e Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 12 Feb 2025 18:34:32 +0000 Subject: [PATCH 12/15] Run fmt --- .../ProvisionersPage/CancelJobButton.stories.tsx | 4 ++-- .../ProvisionersPage/CancelJobButton.tsx | 8 ++++---- .../CancelJobConfirmationDialog.stories.tsx | 6 +++--- .../ProvisionersPage/CancelJobConfirmationDialog.tsx | 12 ++++++------ .../ProvisionersPage/ProvisionerJobsPage.tsx | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.stories.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.stories.tsx index e5bb0d2ce4ff4..337149f17639c 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { CancelJobButton } from "./CancelJobButton"; -import { MockProvisionerJob } from "testHelpers/entities"; import { userEvent, waitFor, within } from "@storybook/test"; +import { MockProvisionerJob } from "testHelpers/entities"; +import { CancelJobButton } from "./CancelJobButton"; const meta: Meta = { title: "pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton", diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx index a58fc568b0bad..7c20f4636dcf3 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx @@ -1,14 +1,14 @@ -import { useState, type FC } from "react"; +import type { ProvisionerJob } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "components/Tooltip/Tooltip"; -import { Button } from "components/Button/Button"; import { BanIcon } from "lucide-react"; -import type { ProvisionerJob } from "api/typesGenerated"; -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { type FC, useState } from "react"; type CancelJobButtonProps = { job: ProvisionerJob; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.stories.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.stories.tsx index 4dbe348b98b9d..8d48fe6d80d1a 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.stories.tsx @@ -1,9 +1,9 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { CancelJobConfirmationDialog } from "./CancelJobConfirmationDialog"; +import { expect, fn, userEvent, waitFor, within } from "@storybook/test"; +import type { Response } from "api/typesGenerated"; import { MockProvisionerJob } from "testHelpers/entities"; -import { fn, userEvent, within, expect, waitFor } from "@storybook/test"; import { withGlobalSnackbar } from "testHelpers/storybook"; -import type { Response } from "api/typesGenerated"; +import { CancelJobConfirmationDialog } from "./CancelJobConfirmationDialog"; const meta: Meta = { title: diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.tsx index bcfde3cf4f184..b8741e0527ffe 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.tsx @@ -1,16 +1,16 @@ +import { API } from "api/api"; +import { + getProvisionerDaemonsKey, + provisionerJobQueryKey, +} from "api/queries/organizations"; import type { ProvisionerJob } from "api/typesGenerated"; import { ConfirmDialog, type ConfirmDialogProps, } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; import type { FC } from "react"; -import { API } from "api/api"; import { useMutation, useQueryClient } from "react-query"; -import { - getProvisionerDaemonsKey, - provisionerJobQueryKey, -} from "api/queries/organizations"; -import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; type CancelJobConfirmationDialogProps = Omit< ConfirmDialogProps, diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx index 52a28b5444638..e82fa302e02e9 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx @@ -23,10 +23,10 @@ import { useQuery } from "react-query"; import { cn } from "utils/cn"; import { docs } from "utils/docs"; import { relativeTime } from "utils/time"; +import { CancelJobButton } from "./CancelJobButton"; import { DataGrid } from "./DataGrid"; import { JobStatusIndicator } from "./JobStatusIndicator"; import { ShrinkTags, Tag, Tags } from "./Tags"; -import { CancelJobButton } from "./CancelJobButton"; type ProvisionerJobsPageProps = { org: Organization; From 59539607bc6cd52c9cd3747c8b4cda674e2aa157 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 14 Feb 2025 17:19:49 +0000 Subject: [PATCH 13/15] Apply PR reviews --- site/src/api/api.ts | 4 +- .../ProvisionersPage/CancelJobButton.tsx | 20 +++-- .../CancelJobConfirmationDialog.tsx | 20 ++--- .../ProvisionersPage/DataGrid.tsx | 13 ++-- .../ProvisionersPage/JobStatusIndicator.tsx | 56 ++++++++------ .../ProvisionerDaemonsPage.tsx | 74 +++++++++++-------- .../ProvisionersPage/ProvisionerJobsPage.tsx | 58 +++++++++------ .../ProvisionersPage/ProvisionersPage.tsx | 15 +++- .../ProvisionersPage/Tags.tsx | 6 +- 9 files changed, 146 insertions(+), 120 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 0dcef564f563e..eca34261340a0 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1238,7 +1238,7 @@ class ApiMethods { }; cancelTemplateVersionBuild = async ( - templateVersionId: TypesGen.TemplateVersion["id"], + templateVersionId: string, ): Promise => { const response = await this.axios.patch( `/api/v2/templateversions/${templateVersionId}/cancel`, @@ -1248,7 +1248,7 @@ class ApiMethods { }; cancelTemplateVersionDryRun = async ( - templateVersionId: TypesGen.TemplateVersion["id"], + templateVersionId: string, jobId: string, ): Promise => { const response = await this.axios.patch( diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx index 7c20f4636dcf3..4c024911ee23f 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx @@ -1,6 +1,5 @@ import type { ProvisionerJob } from "api/typesGenerated"; import { Button } from "components/Button/Button"; -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { Tooltip, TooltipContent, @@ -9,6 +8,9 @@ import { } from "components/Tooltip/Tooltip"; import { BanIcon } from "lucide-react"; import { type FC, useState } from "react"; +import { CancelJobConfirmationDialog } from "./CancelJobConfirmationDialog"; + +const CANCELLABLE = ["pending", "running"]; type CancelJobButtonProps = { job: ProvisionerJob; @@ -16,7 +18,7 @@ type CancelJobButtonProps = { export const CancelJobButton: FC = ({ job }) => { const [isDialogOpen, setIsDialogOpen] = useState(false); - const cancellable = ["pending", "running"].includes(job.status); + const isCancellable = CANCELLABLE.includes(job.status); return ( <> @@ -24,7 +26,7 @@ export const CancelJobButton: FC = ({ job }) => { } + /> ) : ( @@ -128,6 +138,7 @@ const DaemonRow: FC = ({ daemon }) => { ) : ( )} + ({isOpen ? "Hide" : "Show more"}) {relativeTime( new Date(daemon.last_seen_at ?? new Date().toISOString()), @@ -159,7 +170,7 @@ const DaemonRow: FC = ({ daemon }) => { )} - + @@ -175,35 +186,35 @@ const DaemonRow: FC = ({ daemon }) => { - Last seen: - {daemon.last_seen_at} +
Last seen:
+
{daemon.last_seen_at}
- Creation time: - {daemon.created_at} +
Creation time:
+
{daemon.created_at}
- Version: - {daemon.version} +
Version:
+
{daemon.version}
- Tags: - +
Tags:
+
{Object.entries(daemon.tags).map(([key, value]) => ( ))} - +
{daemon.current_job && ( <> - Last job: - {daemon.current_job.id} +
Last job:
+
{daemon.current_job.id}
- Last job state: - - - +
Last job state:
+
+ +
)} @@ -211,13 +222,13 @@ const DaemonRow: FC = ({ daemon }) => { <> - Previous job: - {daemon.previous_job.id} +
Previous job:
+
{daemon.previous_job.id}
- Previous job state: - - - +
Previous job state:
+
+ +
)}
@@ -240,8 +251,7 @@ function statusIndicatorVariant( return "success"; case "busy": return "pending"; - case "offline": - case null: + default: return "inactive"; } } @@ -258,7 +268,7 @@ function statusLabel(daemon: ProvisionerDaemon) { return "Busy..."; case "offline": return "Disconnected"; - case null: + default: return "Unknown"; } } diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx index e82fa302e02e9..93644d7b8be27 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx @@ -26,17 +26,25 @@ import { relativeTime } from "utils/time"; import { CancelJobButton } from "./CancelJobButton"; import { DataGrid } from "./DataGrid"; import { JobStatusIndicator } from "./JobStatusIndicator"; -import { ShrinkTags, Tag, Tags } from "./Tags"; +import { TruncateTags, Tag, Tags } from "./Tags"; +import { Button } from "components/Button/Button"; type ProvisionerJobsPageProps = { - org: Organization; + orgId: string; }; -export const ProvisionerJobsPage: FC = ({ org }) => { - const { data: jobs, isLoadingError } = useQuery(provisionerJobs(org.id)); +export const ProvisionerJobsPage: FC = ({ + orgId, +}) => { + const { + data: jobs, + isLoadingError, + refetch, + } = useQuery(provisionerJobs(orgId)); return (
+

Provisioner jobs

Provisioner Jobs are the individual tasks assigned to Provisioners when the workspaces are being built.{" "} @@ -68,7 +76,10 @@ export const ProvisionerJobsPage: FC = ({ org }) => { ) : isLoadingError ? ( - + refetch()}>Retry} + /> ) : ( @@ -112,6 +123,7 @@ const JobRow: FC = ({ job }) => { ) : ( )} + ({isOpen ? "Hide" : "Show more"}) {relativeTime(new Date(job.created_at))} @@ -137,7 +149,7 @@ const JobRow: FC = ({ job }) => { )} - + @@ -162,38 +174,38 @@ const JobRow: FC = ({ job }) => {

)} - Job ID: - {job.id} +
Job ID:
+
{job.id}
- Available provisioners: - +
Available provisioners:
+
{job.available_workers ? JSON.stringify(job.available_workers) : "[]"} - +
- Completed by provisioner: - {job.worker_id} +
Completed by provisioner:
+
{job.worker_id}
- Associated workspace: - {job.metadata.workspace_name ?? "null"} +
Associated workspace:
+
{job.metadata.workspace_name ?? "null"}
- Creation time: - {job.created_at} +
Creation time:
+
{job.created_at}
- Queue: - +
Queue:
+
{job.queue_position}/{job.queue_size} - +
- Tags: - +
Tags:
+
{Object.entries(job.tags).map(([key, value]) => ( ))} - +
diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx index 1612073481ead..871eb7b91fa0f 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx @@ -16,7 +16,14 @@ const ProvisionersPage: FC = () => { }); if (!organization) { - return ; + return ( + <> + + {pageTitle("Provisioners")} + + + + ); } return ( @@ -50,9 +57,11 @@ const ProvisionersPage: FC = () => {
- {tab.value === "jobs" && } + {tab.value === "jobs" && ( + + )} {tab.value === "daemons" && ( - + )}
diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/Tags.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/Tags.tsx index 7cb48b22df1a6..449aa25593f1c 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/Tags.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/Tags.tsx @@ -32,7 +32,7 @@ type TagsProps = { tags: Record; }; -export const ShrinkTags: FC = ({ tags }) => { +export const TruncateTags: FC = ({ tags }) => { const keys = Object.keys(tags); if (keys.length === 0) { @@ -41,12 +41,12 @@ export const ShrinkTags: FC = ({ tags }) => { const firstKey = keys[0]; const firstValue = tags[firstKey]; - const restKeys = keys.slice(1); + const remainderCount = keys.length - 1; return ( - {restKeys.length > 0 && +{restKeys.length}} + {remainderCount > 0 && +{remainderCount}} ); }; From aabf8dfcc5ccab16fc6b9f462c0fd03193564f80 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 14 Feb 2025 17:20:08 +0000 Subject: [PATCH 14/15] FMT --- .../ProvisionersPage/ProvisionerDaemonsPage.tsx | 4 ++-- .../ProvisionersPage/ProvisionerJobsPage.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx index 49b50471b6f30..93d670eb9b42a 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx @@ -1,6 +1,7 @@ import { provisionerDaemons } from "api/queries/organizations"; import type { Organization, ProvisionerDaemon } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; +import { Button } from "components/Button/Button"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Link } from "components/Link/Link"; import { Loader } from "components/Loader/Loader"; @@ -25,8 +26,7 @@ import { docs } from "utils/docs"; import { relativeTime } from "utils/time"; import { DataGrid, DataGridSpace } from "./DataGrid"; import { DaemonJobStatusIndicator } from "./JobStatusIndicator"; -import { TruncateTags, Tag, Tags } from "./Tags"; -import { Button } from "components/Button/Button"; +import { Tag, Tags, TruncateTags } from "./Tags"; type ProvisionerDaemonsPageProps = { orgId: string; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx index 93644d7b8be27..e852e90f2cf7f 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx @@ -2,6 +2,7 @@ import { provisionerJobs } from "api/queries/organizations"; import type { Organization, ProvisionerJob } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { Badge } from "components/Badge/Badge"; +import { Button } from "components/Button/Button"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Link } from "components/Link/Link"; import { Loader } from "components/Loader/Loader"; @@ -26,8 +27,7 @@ import { relativeTime } from "utils/time"; import { CancelJobButton } from "./CancelJobButton"; import { DataGrid } from "./DataGrid"; import { JobStatusIndicator } from "./JobStatusIndicator"; -import { TruncateTags, Tag, Tags } from "./Tags"; -import { Button } from "components/Button/Button"; +import { Tag, Tags, TruncateTags } from "./Tags"; type ProvisionerJobsPageProps = { orgId: string; From ed61ce785b4da779bc447a8a641f9f353a5679f1 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 18 Feb 2025 14:17:58 +0000 Subject: [PATCH 15/15] Reset devcontainer.json --- .devcontainer/devcontainer.json | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index be0dbfd3e89e9..de550f174bc9f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,14 +9,5 @@ } }, // SYS_PTRACE to enable go debugging - "runArgs": [ - "--cap-add=SYS_PTRACE" - ], - "customizations": { - "vscode": { - "extensions": [ - "biomejs.biome" - ] - } - } + "runArgs": ["--cap-add=SYS_PTRACE"] } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy