diff --git a/site/src/api/api.ts b/site/src/api/api.ts index fa62afadee608..6667ad98d8d13 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -396,7 +396,17 @@ export class MissingBuildParameters extends Error { } export type GetProvisionerJobsParams = { - status?: TypesGen.ProvisionerJobStatus; + status?: string; + limit?: number; + // IDs separated by comma + ids?: string; +}; + +export type GetProvisionerDaemonsParams = { + // IDs separated by comma + ids?: string; + // Stringified JSON Object + tags?: string; limit?: number; }; @@ -711,22 +721,13 @@ class ApiMethods { return response.data; }; - /** - * @param organization Can be the organization's ID or name - * @param tags to filter provisioner daemons by. - */ getProvisionerDaemonsByOrganization = async ( organization: string, - tags?: Record, + params?: GetProvisionerDaemonsParams, ): Promise => { - const params = new URLSearchParams(); - - if (tags) { - params.append("tags", JSON.stringify(tags)); - } - const response = await this.axios.get( - `/api/v2/organizations/${organization}/provisionerdaemons?${params}`, + `/api/v2/organizations/${organization}/provisionerdaemons`, + { params }, ); return response.data; }; diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 632b5f0c730ad..238fb4493fb52 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -1,4 +1,8 @@ -import { API, type GetProvisionerJobsParams } from "api/api"; +import { + API, + type GetProvisionerDaemonsParams, + type GetProvisionerJobsParams, +} from "api/api"; import type { CreateOrganizationRequest, GroupSyncSettings, @@ -164,16 +168,17 @@ export const organizations = () => { export const getProvisionerDaemonsKey = ( organization: string, - tags?: Record, -) => ["organization", organization, tags, "provisionerDaemons"]; + params?: GetProvisionerDaemonsParams, +) => ["organization", organization, "provisionerDaemons", params]; export const provisionerDaemons = ( organization: string, - tags?: Record, + params?: GetProvisionerDaemonsParams, ) => { return { - queryKey: getProvisionerDaemonsKey(organization, tags), - queryFn: () => API.getProvisionerDaemonsByOrganization(organization, tags), + queryKey: getProvisionerDaemonsKey(organization, params), + queryFn: () => + API.getProvisionerDaemonsByOrganization(organization, params), }; }; diff --git a/site/src/components/Button/Button.tsx b/site/src/components/Button/Button.tsx index d9daae9c59252..1a01588af341a 100644 --- a/site/src/components/Button/Button.tsx +++ b/site/src/components/Button/Button.tsx @@ -8,7 +8,7 @@ import { forwardRef } from "react"; import { cn } from "utils/cn"; export const buttonVariants = cva( - `inline-flex items-center justify-center gap-1 whitespace-nowrap + `inline-flex items-center justify-center gap-1 whitespace-nowrap font-sans 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 @@ -30,6 +30,7 @@ export const buttonVariants = cva( size: { 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", + xs: "min-w-8 py-1 px-2 text-2xs rounded-md", icon: "size-8 px-1.5 [&_svg]:size-icon-sm", "icon-lg": "size-10 px-2 [&_svg]:size-icon-lg", }, diff --git a/site/src/pages/CreateTokenPage/CreateTokenForm.tsx b/site/src/pages/CreateTokenPage/CreateTokenForm.tsx index ee5c3bf8f3a6e..57d1587e92590 100644 --- a/site/src/pages/CreateTokenPage/CreateTokenForm.tsx +++ b/site/src/pages/CreateTokenPage/CreateTokenForm.tsx @@ -119,7 +119,6 @@ export const CreateTokenForm: FC = ({ {lifetimeDays === "custom" && ( = ({ setExpDays(lt); }} inputProps={{ + "data-chromatic": "ignore", min: dayjs().add(1, "day").format("YYYY-MM-DD"), max: maxTokenLifetime ? dayjs() diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx index 56eb382067d84..7a62a8f955747 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx @@ -15,7 +15,7 @@ const meta: Meta = { ], parameters: { chromatic: { - diffThreshold: 0.5, + diffThreshold: 0.6, }, }, }; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx index 3e20863b25d51..e97749db3d6f4 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx @@ -15,17 +15,19 @@ import { ProvisionerTruncateTags, } from "modules/provisioners/ProvisionerTags"; import { type FC, useState } from "react"; +import { Link as RouterLink } from "react-router-dom"; import { cn } from "utils/cn"; import { relativeTime } from "utils/time"; import { CancelJobButton } from "./CancelJobButton"; type JobRowProps = { job: ProvisionerJob; + defaultIsOpen: boolean; }; -export const JobRow: FC = ({ job }) => { +export const JobRow: FC = ({ job, defaultIsOpen = false }) => { const metadata = job.metadata; - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(defaultIsOpen); const queue = { size: job.queue_size, position: job.queue_position, @@ -114,8 +116,21 @@ export const JobRow: FC = ({ job }) => { : "[]"} -
Completed by provisioner:
-
{job.worker_id}
+ {job.worker_id && ( + <> +
Completed by provisioner:
+
+ {job.worker_id} + +
+ + )}
Associated workspace:
{job.metadata.workspace_name ?? "null"}
@@ -123,10 +138,14 @@ export const JobRow: FC = ({ job }) => {
Creation time:
{job.created_at}
-
Queue:
-
- {job.queue_position}/{job.queue_size} -
+ {job.queue_position > 0 && ( + <> +
Queue:
+
+ {job.queue_position}/{job.queue_size} +
+ + )}
Tags:
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx index 8602fe0c23727..e7c8e30efcf17 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPage.tsx @@ -11,18 +11,18 @@ const OrganizationProvisionerJobsPage: FC = () => { const { organization } = useOrganizationSettings(); const [searchParams, setSearchParams] = useSearchParams(); const filter = { - status: searchParams.get("status") || "", + status: searchParams.get("status") ?? "", + ids: searchParams.get("ids") ?? "", }; - const queryParams = { - ...filter, - limit: 100, - } as GetProvisionerJobsParams; const { data: jobs, isLoadingError, refetch, } = useQuery({ - ...provisionerJobs(organization?.id || "", queryParams), + ...provisionerJobs(organization?.id ?? "", { + ...filter, + limit: 100, + }), enabled: organization !== undefined, }); diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx index a5837cf527fc2..35a96a1b3bd5f 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.stories.tsx @@ -21,7 +21,7 @@ const meta: Meta = { args: { organization: MockOrganization, jobs: MockProvisionerJobs, - filter: { status: "" }, + filter: { status: "", ids: "" }, onRetry: fn(), }, }; @@ -81,8 +81,8 @@ export const Empty: Story = { export const OnFilter: Story = { render: function FilterWithState({ ...args }) { const [jobs, setJobs] = useState([]); - const [filter, setFilter] = useState({ status: "pending" }); - const handleFilterChange = (newFilter: { status: string }) => { + const [filter, setFilter] = useState({ status: "pending", ids: "" }); + const handleFilterChange = (newFilter: { status: string; ids: string }) => { setFilter(newFilter); const filteredJobs = MockProvisionerJobs.filter((job) => newFilter.status ? job.status === newFilter.status : true, @@ -109,3 +109,13 @@ export const OnFilter: Story = { await userEvent.click(option); }, }; + +export const FilterByID: Story = { + args: { + jobs: [MockProvisionerJob], + filter: { + ids: MockProvisionerJob.id, + status: "", + }, + }, +}; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx index 6aa372c7c6205..8b6a2a839b8af 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx @@ -3,6 +3,7 @@ import type { ProvisionerJob, ProvisionerJobStatus, } from "api/typesGenerated"; +import { Badge } from "components/Badge/Badge"; import { Button } from "components/Button/Button"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Link } from "components/Link/Link"; @@ -33,6 +34,13 @@ import { TableHeader, TableRow, } from "components/Table/Table"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; +import { XIcon } from "lucide-react"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { docs } from "utils/docs"; @@ -64,6 +72,7 @@ const StatusFilters: ProvisionerJobStatus[] = [ type JobProvisionersFilter = { status: string; + ids: string; }; type OrganizationProvisionerJobsPageViewProps = { @@ -110,30 +119,62 @@ const OrganizationProvisionerJobsPageView: FC< - +
+ {filter.ids && ( +
+ + {filter.ids} + +
+ + + + + + Clear ID + + +
+
+ )} + + +
@@ -149,7 +190,13 @@ const OrganizationProvisionerJobsPageView: FC< {jobs ? ( jobs.length > 0 ? ( - jobs.map((j) => ) + jobs.map((j) => ( + + )) ) : ( diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPage.tsx index 181bbbb4c62a3..242c0acdf842b 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPage.tsx @@ -8,7 +8,7 @@ import { RequirePermission } from "modules/permissions/RequirePermission"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; -import { useParams } from "react-router-dom"; +import { useParams, useSearchParams } from "react-router-dom"; import { pageTitle } from "utils/page"; import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView"; @@ -16,14 +16,20 @@ const OrganizationProvisionersPage: FC = () => { const { organization: organizationName } = useParams() as { organization: string; }; + const [searchParams, setSearchParams] = useSearchParams(); + const queryParams = { + ids: searchParams.get("ids") ?? "", + tags: searchParams.get("tags") ?? "", + }; const { organization, organizationPermissions } = useOrganizationSettings(); const { entitlements } = useDashboard(); const { metadata } = useEmbeddedMetadata(); const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); const provisionersQuery = useQuery({ - ...provisionerDaemons(organizationName), - select: (provisioners) => - provisioners.filter((p) => p.status !== "offline"), + ...provisionerDaemons(organizationName, { + ...queryParams, + limit: 100, + }), }); if (!organization) { @@ -59,6 +65,8 @@ const OrganizationProvisionersPage: FC = () => { provisioners={provisionersQuery.data} buildVersion={buildInfoQuery.data?.version} onRetry={provisionersQuery.refetch} + filter={queryParams} + onFilterChange={setSearchParams} /> ); diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.stories.tsx index 93d47e97d6a9f..a559af512bbe3 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.stories.tsx @@ -24,6 +24,9 @@ const meta: Meta = { version: "0.0.0", }, ], + filter: { + ids: "", + }, }, }; @@ -60,3 +63,12 @@ export const Paywall: Story = { showPaywall: true, }, }; + +export const FilterByID: Story = { + args: { + provisioners: [MockProvisioner], + filter: { + ids: MockProvisioner.id, + }, + }, +}; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx index e0ccddd9f5448..387baf31519cb 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx @@ -1,4 +1,5 @@ import type { ProvisionerDaemon } from "api/typesGenerated"; +import { Badge } from "components/Badge/Badge"; import { Button } from "components/Button/Button"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Link } from "components/Link/Link"; @@ -17,23 +18,43 @@ import { TableHeader, TableRow, } from "components/Table/Table"; -import { SquareArrowOutUpRightIcon } from "lucide-react"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; +import { SquareArrowOutUpRightIcon, XIcon } from "lucide-react"; import type { FC } from "react"; import { docs } from "utils/docs"; import { LastConnectionHead } from "./LastConnectionHead"; import { ProvisionerRow } from "./ProvisionerRow"; +type ProvisionersFilter = { + ids: string; +}; + interface OrganizationProvisionersPageViewProps { showPaywall: boolean | undefined; provisioners: readonly ProvisionerDaemon[] | undefined; buildVersion: string | undefined; error: unknown; + filter: ProvisionersFilter; onRetry: () => void; + onFilterChange: (filter: ProvisionersFilter) => void; } export const OrganizationProvisionersPageView: FC< OrganizationProvisionersPageViewProps -> = ({ showPaywall, error, provisioners, buildVersion, onRetry }) => { +> = ({ + showPaywall, + error, + provisioners, + buildVersion, + filter, + onFilterChange, + onRetry, +}) => { return (
@@ -45,6 +66,35 @@ export const OrganizationProvisionersPageView: FC< + {filter.ids && ( +
+
+ + {filter.ids} + +
+ + + + + + Clear ID + + +
+
+
+ )} + {showPaywall ? ( )) ) : ( diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/ProvisionerRow.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/ProvisionerRow.tsx index 2c47578f67a6a..ca5af240d1b02 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/ProvisionerRow.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/ProvisionerRow.tsx @@ -18,6 +18,7 @@ import { } from "modules/provisioners/ProvisionerTags"; import { ProvisionerKey } from "pages/OrganizationSettingsPage/OrganizationProvisionersPage/ProvisionerKey"; import { type FC, useState } from "react"; +import { Link as RouterLink } from "react-router-dom"; import { cn } from "utils/cn"; import { relativeTime } from "utils/time"; import { ProvisionerVersion } from "./ProvisionerVersion"; @@ -34,13 +35,15 @@ const variantByStatus: Record< type ProvisionerRowProps = { provisioner: ProvisionerDaemon; buildVersion: string | undefined; + defaultIsOpen: boolean; }; export const ProvisionerRow: FC = ({ provisioner, buildVersion, + defaultIsOpen = false, }) => { - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(defaultIsOpen); return ( <> @@ -151,7 +154,16 @@ export const ProvisionerRow: FC = ({ {provisioner.previous_job && ( <>
Previous job:
-
{provisioner.previous_job.id}
+
+ {provisioner.previous_job.id} + +
Previous job status:
diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx index 7a34d57fbf83d..d58f3e328e3ff 100644 --- a/site/src/pages/TerminalPage/TerminalPage.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -91,7 +91,7 @@ const meta = { }, ], chromatic: { - diffThreshold: 0.5, + diffThreshold: 0.8, }, }, decorators: [ diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx index 1ae3ff9e2ebc9..ce2ad840a1df0 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx @@ -39,7 +39,7 @@ const meta: Meta = { layout: "fullscreen", features: ["advanced_template_scheduling"], chromatic: { - diffThreshold: 0.3, + diffThreshold: 0.6, }, }, }; @@ -321,7 +321,7 @@ export const TemplateInfoPopover: Story = { }, parameters: { chromatic: { - diffThreshold: 0.3, + diffThreshold: 0.6, }, }, }; diff --git a/site/tailwind.config.js b/site/tailwind.config.js index 142a4711b56f3..d2935698e5d9e 100644 --- a/site/tailwind.config.js +++ b/site/tailwind.config.js @@ -8,6 +8,9 @@ module.exports = { important: ["#root", "#storybook-root"], theme: { extend: { + fontFamily: { + sans: `"Inter Variable", system-ui, sans-serif`, + }, size: { "icon-lg": "1.5rem", "icon-sm": "1.125rem",