diff --git a/site/src/components/Badge/Badge.tsx b/site/src/components/Badge/Badge.tsx index 2044db6d20..453e852da7 100644 --- a/site/src/components/Badge/Badge.tsx +++ b/site/src/components/Badge/Badge.tsx @@ -12,7 +12,7 @@ export const badgeVariants = cva( variants: { variant: { default: - "border-transparent bg-surface-secondary text-content-secondary shadow hover:bg-surface-tertiary", + "border-transparent bg-surface-secondary text-content-secondary shadow", }, size: { sm: "text-2xs font-regular", diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index 00a435b82c..7d30b4d769 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -24,7 +24,7 @@ export const OrganizationSettingsContext = createContext< OrganizationSettingsValue | undefined >(undefined); -type OrganizationSettingsValue = Readonly<{ +export type OrganizationSettingsValue = Readonly<{ organizations: readonly Organization[]; organizationPermissionsByOrganizationId: Record< string, @@ -36,9 +36,10 @@ type OrganizationSettingsValue = Readonly<{ export const useOrganizationSettings = (): OrganizationSettingsValue => { const context = useContext(OrganizationSettingsContext); + if (!context) { throw new Error( - "useOrganizationSettings should be used inside of OrganizationSettingsLayout", + "useOrganizationSettings should be used inside of OrganizationSettingsLayout or with the default values in case of testing.", ); } diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index ff5617eaa4..5de8ef0d2e 100644 --- a/site/src/modules/management/OrganizationSidebarView.tsx +++ b/site/src/modules/management/OrganizationSidebarView.tsx @@ -186,11 +186,18 @@ const OrganizationSettingsNavigation: FC< )} {orgPermissions.viewProvisioners && orgPermissions.viewProvisionerJobs && ( - - Provisioners - + <> + + Provisioners + + + Provisioner Jobs + + )} {orgPermissions.viewIdpSyncSettings && ( = { - title: "pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton", + title: "pages/OrganizationProvisionerJobsPage/CancelJobButton", component: CancelJobButton, args: { job: { @@ -28,7 +28,7 @@ export const NotCancellable: Story = { }, }; -export const OnClick: Story = { +export const ConfirmOnClick: Story = { parameters: { chromatic: { disableSnapshot: true }, }, diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobButton.tsx similarity index 100% rename from site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobButton.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobButton.tsx diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobConfirmationDialog.stories.tsx similarity index 94% rename from site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.stories.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobConfirmationDialog.stories.tsx index 8d48fe6d80..f0c117360d 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobConfirmationDialog.stories.tsx @@ -6,8 +6,7 @@ import { withGlobalSnackbar } from "testHelpers/storybook"; import { CancelJobConfirmationDialog } from "./CancelJobConfirmationDialog"; const meta: Meta = { - title: - "pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog", + title: "pages/OrganizationProvisionerJobsPage/CancelJobConfirmationDialog", component: CancelJobConfirmationDialog, args: { open: true, @@ -40,7 +39,7 @@ export const OnCancel: Story = { }, }; -export const onConfirmSuccess: Story = { +export const OnConfirmSuccess: Story = { parameters: { chromatic: { disableSnapshot: true }, }, @@ -60,7 +59,7 @@ export const onConfirmSuccess: Story = { }, }; -export const onConfirmFailure: Story = { +export const OnConfirmFailure: Story = { parameters: { chromatic: { disableSnapshot: true }, }, diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobConfirmationDialog.tsx similarity index 100% rename from site/src/pages/OrganizationSettingsPage/ProvisionersPage/CancelJobConfirmationDialog.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobConfirmationDialog.tsx diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.stories.tsx new file mode 100644 index 0000000000..35818baeed --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.stories.tsx @@ -0,0 +1,58 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { expect, userEvent, waitFor, within } from "@storybook/test"; +import { Table, TableBody } from "components/Table/Table"; +import { MockProvisionerJob } from "testHelpers/entities"; +import { daysAgo } from "utils/time"; +import { JobRow } from "./JobRow"; + +const meta: Meta = { + title: "pages/OrganizationProvisionerJobsPage/JobRow", + component: JobRow, + args: { + job: { + ...MockProvisionerJob, + created_at: daysAgo(2), + }, + }, + render: (args) => { + return ( + + + + +
+ ); + }, +}; + +export default meta; +type Story = StoryObj; + +export const Close: Story = {}; + +export const OpenOnClick: Story = { + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + const showMoreButton = canvas.getByRole("button", { name: /show more/i }); + + await userEvent.click(showMoreButton); + + const jobId = canvas.getByText(args.job.id); + expect(jobId).toBeInTheDocument(); + }, +}; + +export const HideOnClick: Story = { + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const showMoreButton = canvas.getByRole("button", { name: /show more/i }); + await userEvent.click(showMoreButton); + + const hideButton = canvas.getByRole("button", { name: /hide/i }); + await userEvent.click(hideButton); + + const jobId = canvas.queryByText(args.job.id); + expect(jobId).not.toBeInTheDocument(); + }, +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx similarity index 54% rename from site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx index 3d5d9e2d99..9c7aecbba5 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerJobsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx @@ -1,105 +1,24 @@ -import { provisionerJobs } from "api/queries/organizations"; 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 { EmptyState } from "components/EmptyState/EmptyState"; -import { Link } from "components/Link/Link"; -import { Loader } from "components/Loader/Loader"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "components/Table/Table"; +import { TableCell, TableRow } from "components/Table/Table"; import { ChevronDownIcon, ChevronRightIcon, TriangleAlertIcon, } from "lucide-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 { CancelJobButton } from "./CancelJobButton"; -import { DataGrid } from "./DataGrid"; import { JobStatusIndicator } from "./JobStatusIndicator"; import { Tag, Tags, TruncateTags } from "./Tags"; -type ProvisionerJobsPageProps = { - orgId: string; -}; - -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.{" "} - View docs -

- - - - - Created - Type - Template - Tags - Status - - - - - {jobs ? ( - jobs.length > 0 ? ( - jobs.map((j) => ) - ) : ( - - - - - - ) - ) : isLoadingError ? ( - - - refetch()}>Retry} - /> - - - ) : ( - - - - - - )} - -
-
- ); -}; - type JobRowProps = { job: ProvisionerJob; }; -const JobRow: FC = ({ job }) => { +export const JobRow: FC = ({ job }) => { const metadata = job.metadata; const [isOpen, setIsOpen] = useState(false); @@ -133,20 +52,16 @@ const JobRow: FC = ({ job }) => { {job.type} - {job.metadata.template_name ? ( -
- - {metadata.template_display_name ?? metadata.template_name} -
- ) : ( - Not linked - )} +
+ + {metadata.template_display_name || metadata.template_name} +
@@ -173,7 +88,13 @@ const JobRow: FC = ({ job }) => { {job.error} )} - +
Job ID:
{job.id}
@@ -206,7 +127,7 @@ const JobRow: FC = ({ job }) => { ))} - +
)} diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobStatusIndicator.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobStatusIndicator.stories.tsx new file mode 100644 index 0000000000..d77cc98cc1 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobStatusIndicator.stories.tsx @@ -0,0 +1,76 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { MockProvisionerJob } from "testHelpers/entities"; +import { JobStatusIndicator } from "./JobStatusIndicator"; + +const meta: Meta = { + title: "pages/OrganizationProvisionerJobsPage/JobStatusIndicator", + component: JobStatusIndicator, +}; + +export default meta; +type Story = StoryObj; + +export const Succeeded: Story = { + args: { + job: { + ...MockProvisionerJob, + status: "succeeded", + }, + }, +}; + +export const Failed: Story = { + args: { + job: { + ...MockProvisionerJob, + status: "failed", + }, + }, +}; + +export const Pending: Story = { + args: { + job: { + ...MockProvisionerJob, + status: "pending", + queue_position: 1, + queue_size: 1, + }, + }, +}; + +export const Running: Story = { + args: { + job: { + ...MockProvisionerJob, + status: "running", + }, + }, +}; + +export const Canceling: Story = { + args: { + job: { + ...MockProvisionerJob, + status: "canceling", + }, + }, +}; + +export const Canceled: Story = { + args: { + job: { + ...MockProvisionerJob, + status: "canceled", + }, + }, +}; + +export const Unknown: Story = { + args: { + job: { + ...MockProvisionerJob, + status: "unknown", + }, + }, +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/JobStatusIndicator.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobStatusIndicator.tsx similarity index 63% rename from site/src/pages/OrganizationSettingsPage/ProvisionersPage/JobStatusIndicator.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobStatusIndicator.tsx index 0671a6b932..2111b11902 100644 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/JobStatusIndicator.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobStatusIndicator.tsx @@ -1,8 +1,4 @@ -import type { - ProvisionerDaemonJob, - ProvisionerJob, - ProvisionerJobStatus, -} from "api/typesGenerated"; +import type { ProvisionerJob, ProvisionerJobStatus } from "api/typesGenerated"; import { StatusIndicator, StatusIndicatorDot, @@ -40,21 +36,3 @@ export const JobStatusIndicator: FC = ({ job }) => { ); }; - -type DaemonJobStatusIndicatorProps = { - job: ProvisionerDaemonJob; -}; - -export const DaemonJobStatusIndicator: FC = ({ - job, -}) => { - return ( - - - {job.status} - {job.status === "failed" && ( - - )} - - ); -}; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx new file mode 100644 index 0000000000..bae561c4a9 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx @@ -0,0 +1,28 @@ +import { provisionerJobs } from "api/queries/organizations"; +import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; +import type { FC } from "react"; +import { useQuery } from "react-query"; +import OrganizationProvisionerJobsPageView from "./OrganizationProvisionerJobsPageView"; + +const OrganizationProvisionerJobsPage: FC = () => { + const { organization } = useOrganizationSettings(); + const { + data: jobs, + isLoadingError, + refetch, + } = useQuery({ + ...provisionerJobs(organization?.id || ""), + enabled: organization !== undefined, + }); + + return ( + + ); +}; + +export default OrganizationProvisionerJobsPage; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx new file mode 100644 index 0000000000..9b6a25a352 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx @@ -0,0 +1,77 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { expect, fn, userEvent, waitFor, within } from "@storybook/test"; +import type { ProvisionerJob } from "api/typesGenerated"; +import { MockOrganization, MockProvisionerJob } from "testHelpers/entities"; +import { daysAgo } from "utils/time"; +import OrganizationProvisionerJobsPageView from "./OrganizationProvisionerJobsPageView"; + +const MockProvisionerJobs: ProvisionerJob[] = Array.from( + { length: 50 }, + (_, i) => ({ + ...MockProvisionerJob, + id: i.toString(), + created_at: daysAgo(2), + }), +); + +const meta: Meta = { + title: "pages/OrganizationProvisionerJobsPage", + component: OrganizationProvisionerJobsPageView, + args: { + organization: MockOrganization, + jobs: MockProvisionerJobs, + onRetry: fn(), + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const OrganizationNotFound: Story = { + args: { + organization: undefined, + }, +}; + +export const Loading: Story = { + args: { + jobs: undefined, + }, +}; + +export const LoadingError: Story = { + args: { + jobs: undefined, + error: new Error("Failed to load jobs"), + }, +}; + +export const RetryAfterError: Story = { + args: { + jobs: undefined, + error: new Error("Failed to load jobs"), + onRetry: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + const retryButton = await canvas.findByRole("button", { name: "Retry" }); + userEvent.click(retryButton); + + await waitFor(() => { + expect(args.onRetry).toHaveBeenCalled(); + }); + }, + parameters: { + chromatic: { + disableSnapshot: true, + }, + }, +}; + +export const Empty: Story = { + args: { + jobs: [], + }, +}; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx new file mode 100644 index 0000000000..98168ef39a --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx @@ -0,0 +1,113 @@ +import type { Organization, ProvisionerJob } from "api/typesGenerated"; +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, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "components/Table/Table"; +import type { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { docs } from "utils/docs"; +import { pageTitle } from "utils/page"; +import { JobRow } from "./JobRow"; + +type OrganizationProvisionerJobsPageViewProps = { + jobs: ProvisionerJob[] | undefined; + organization: Organization | undefined; + error: unknown; + onRetry: () => void; +}; + +const OrganizationProvisionerJobsPageView: FC< + OrganizationProvisionerJobsPageViewProps +> = ({ jobs, organization, error, onRetry }) => { + if (!organization) { + return ( + <> + + {pageTitle("Provisioner Jobs")} + + + + ); + } + + return ( + <> + + + {pageTitle( + "Provisioner Jobs", + organization.display_name || organization.name, + )} + + + +
+
+
+

Provisioner Jobs

+

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

+
+
+ + + + + Created + Type + Template + Tags + Status + + + + + {jobs ? ( + jobs.length > 0 ? ( + jobs.map((j) => ) + ) : ( + + + + + + ) + ) : error ? ( + + + + Retry + + } + /> + + + ) : ( + + + + + + )} + +
+
+ + ); +}; + +export default OrganizationProvisionerJobsPageView; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/Tags.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/Tags.stories.tsx new file mode 100644 index 0000000000..8d4612d525 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/Tags.stories.tsx @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { + Tag as TagComponent, + Tags as TagsComponent, + TruncateTags as TruncateTagsComponent, +} from "./Tags"; + +const meta: Meta = { + title: "pages/OrganizationProvisionerJobsPage/Tags", +}; + +export default meta; +type Story = StoryObj; + +export const Tag: Story = { + render: () => { + return ; + }, +}; + +export const Tags: Story = { + render: () => { + return ( + + + + + + ); + }, +}; + +export const TruncateTags: Story = { + render: () => { + return ( + + ); + }, +}; diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/Tags.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/Tags.tsx similarity index 100% rename from site/src/pages/OrganizationSettingsPage/ProvisionersPage/Tags.tsx rename to site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/Tags.tsx diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/DataGrid.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/DataGrid.tsx deleted file mode 100644 index 7c9d11a238..0000000000 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/DataGrid.tsx +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index ae57ebb90a..0000000000 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionerDaemonsPage.tsx +++ /dev/null @@ -1,274 +0,0 @@ -import { provisionerDaemons } from "api/queries/organizations"; -import type { 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"; -import { - StatusIndicator, - StatusIndicatorDot, - type StatusIndicatorProps, -} from "components/StatusIndicator/StatusIndicator"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "components/Table/Table"; -import { ChevronDownIcon, ChevronRightIcon } from "lucide-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 { DataGrid, DataGridSpace } from "./DataGrid"; -import { DaemonJobStatusIndicator } from "./JobStatusIndicator"; -import { Tag, Tags, TruncateTags } from "./Tags"; - -type ProvisionerDaemonsPageProps = { - orgId: string; -}; - -export const ProvisionerDaemonsPage: FC = ({ - orgId, -}) => { - const { - data: daemons, - isLoadingError, - refetch, - } = useQuery({ - ...provisionerDaemons(orgId), - select: (data) => - data.toSorted((a, b) => { - if (!a.last_seen_at && !b.last_seen_at) return 0; - 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 ( -
-

Provisioner daemons

-

- Coder server runs provisioner daemons which execute terraform during - workspace and template builds.{" "} - - View docs - -

- - - - - Last seen - Name - Template - Tags - Status - - - - {daemons ? ( - daemons.length > 0 ? ( - daemons.map((d) => ) - ) : ( - - - - - - ) - ) : isLoadingError ? ( - - - refetch()}>Retry} - /> - - - ) : ( - - - - - - )} - -
-
- ); -}; - -type DaemonRowProps = { - daemon: ProvisionerDaemon; -}; - -const DaemonRow: FC = ({ daemon }) => { - const [isOpen, setIsOpen] = useState(false); - - return ( - <> - - - - - - - {daemon.name} - - - - {daemon.current_job ? ( -
- - {daemon.current_job.template_display_name ?? - daemon.current_job.template_name} -
- ) : ( - Not linked - )} -
- - - - - - - - {statusLabel(daemon)} - - - -
- - {isOpen && ( - - - -
Last seen:
-
{daemon.last_seen_at}
- -
Creation time:
-
{daemon.created_at}
- -
Version:
-
{daemon.version}
- -
Tags:
-
- - {Object.entries(daemon.tags).map(([key, value]) => ( - - ))} - -
- - {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( - daemon: ProvisionerDaemon, -): StatusIndicatorProps["variant"] { - if (daemon.previous_job && daemon.previous_job.status === "failed") { - return "failed"; - } - - switch (daemon.status) { - case "idle": - return "success"; - case "busy": - return "pending"; - default: - 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"; - default: - return "Unknown"; - } -} diff --git a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx deleted file mode 100644 index ced95a95e0..0000000000 --- a/site/src/pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage.tsx +++ /dev/null @@ -1,80 +0,0 @@ -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 { RequirePermission } from "modules/permissions/RequirePermission"; -import type { FC } from "react"; -import { Helmet } from "react-helmet-async"; -import { pageTitle } from "utils/page"; -import { ProvisionerDaemonsPage } from "./ProvisionerDaemonsPage"; -import { ProvisionerJobsPage } from "./ProvisionerJobsPage"; - -const ProvisionersPage: FC = () => { - const { organization, organizationPermissions } = useOrganizationSettings(); - const tab = useSearchParamsKey({ - key: "tab", - defaultValue: "jobs", - }); - - if (!organization || !organizationPermissions?.viewProvisionerJobs) { - return ; - } - - const helmet = ( - - - {pageTitle( - "Provisioners", - organization.display_name || organization.name, - )} - - - ); - - if (!organizationPermissions?.viewProvisioners) { - return ( - <> - {helmet} - - - ); - } - - return ( - <> - {helmet} - -
-
-
-

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 06e3c0d6cf..d1e3e903eb 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -306,6 +306,12 @@ const ChangePasswordPage = lazy( const IdpOrgSyncPage = lazy( () => import("./pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage"), ); +const ProvisionerJobsPage = lazy( + () => + import( + "./pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage" + ), +); const RoutesWithSuspense = () => { return ( @@ -426,6 +432,10 @@ export const router = createBrowserRouter( } /> } /> + } + /> } /> } /> diff --git a/site/src/utils/time.ts b/site/src/utils/time.ts index f890cd3f7a..e46ef27617 100644 --- a/site/src/utils/time.ts +++ b/site/src/utils/time.ts @@ -40,3 +40,9 @@ export function durationInDays(duration: number): number { export function relativeTime(date: Date) { return dayjs(date).fromNow(); } + +export function daysAgo(count: number) { + const date = new Date(); + date.setDate(date.getDate() - count); + return date.toISOString(); +}