feat: add provisioners view to organization settings (#14501)

This commit is contained in:
Kayla Washburn-Love
2024-09-04 16:21:24 -06:00
committed by GitHub
parent c3f0db3671
commit 84922e239f
16 changed files with 443 additions and 205 deletions
+7
View File
@@ -223,6 +223,13 @@ export const organizationsPermissions = (
},
action: "create",
},
viewProvisioners: {
object: {
resource_type: "provisioner_daemon",
organization_id: organizationId,
},
action: "read",
},
});
// The endpoint takes a flat array, so to avoid collisions prepend each
+24 -2
View File
@@ -14,6 +14,7 @@ import type { ThemeRole } from "theme/roles";
export type PillProps = HTMLAttributes<HTMLDivElement> & {
icon?: ReactNode;
type?: ThemeRole;
size?: "md" | "lg";
};
const themeStyles = (type: ThemeRole) => (theme: Theme) => {
@@ -30,13 +31,25 @@ const PILL_ICON_SPACING = (PILL_HEIGHT - PILL_ICON_SIZE) / 2;
export const Pill: FC<PillProps> = forwardRef<HTMLDivElement, PillProps>(
(props, ref) => {
const { icon, type = "inactive", children, ...divProps } = props;
const {
icon,
type = "inactive",
children,
size = "md",
...divProps
} = props;
const typeStyles = useMemo(() => themeStyles(type), [type]);
return (
<div
ref={ref}
css={[styles.pill, icon && styles.pillWithIcon, typeStyles]}
css={[
styles.pill,
icon && size === "md" && styles.pillWithIcon,
size === "lg" && styles.pillLg,
icon && size === "lg" && styles.pillLgWithIcon,
typeStyles,
]}
{...divProps}
>
{icon}
@@ -80,6 +93,15 @@ const styles = {
paddingLeft: PILL_ICON_SPACING,
},
pillLg: {
gap: PILL_ICON_SPACING * 2,
padding: "14px 16px",
},
pillLgWithIcon: {
paddingLeft: PILL_ICON_SPACING * 2,
},
spinner: (theme) => ({
color: theme.experimental.l1.text,
// It is necessary to align it with the MUI Icons internal padding
@@ -0,0 +1,117 @@
import { useTheme } from "@emotion/react";
import Business from "@mui/icons-material/Business";
import Person from "@mui/icons-material/Person";
import Tooltip from "@mui/material/Tooltip";
import type { HealthMessage, ProvisionerDaemon } from "api/typesGenerated";
import { Pill } from "components/Pill/Pill";
import type { FC } from "react";
import { createDayString } from "utils/createDayString";
import { ProvisionerTag } from "./ProvisionerTag";
interface ProvisionerProps {
readonly provisioner: ProvisionerDaemon;
readonly warnings?: readonly HealthMessage[];
}
export const Provisioner: FC<ProvisionerProps> = ({
provisioner,
warnings,
}) => {
const theme = useTheme();
const daemonScope = provisioner.tags.scope || "organization";
const iconScope = daemonScope === "organization" ? <Business /> : <Person />;
const extraTags = Object.entries(provisioner.tags).filter(
([key]) => key !== "scope" && key !== "owner",
);
const isWarning = warnings && warnings.length > 0;
return (
<div
key={provisioner.name}
css={[
{
borderRadius: 8,
border: `1px solid ${theme.palette.divider}`,
fontSize: 14,
},
isWarning && { borderColor: theme.palette.warning.light },
]}
>
<header
css={{
padding: 24,
display: "flex",
alignItems: "center",
justifyContenxt: "space-between",
gap: 24,
}}
>
<div
css={{
display: "flex",
alignItems: "center",
gap: 24,
objectFit: "fill",
}}
>
<div css={{ lineHeight: "160%" }}>
<h4 css={{ fontWeight: 500, margin: 0 }}>{provisioner.name}</h4>
<span css={{ color: theme.palette.text.secondary }}>
<code>{provisioner.version}</code>
</span>
</div>
</div>
<div
css={{
marginLeft: "auto",
display: "flex",
flexWrap: "wrap",
gap: 12,
}}
>
<Tooltip title="Scope">
<Pill size="lg" icon={iconScope}>
<span
css={{
":first-letter": { textTransform: "uppercase" },
}}
>
{daemonScope}
</span>
</Pill>
</Tooltip>
{extraTags.map(([key, value]) => (
<ProvisionerTag key={key} tagName={key} tagValue={value} />
))}
</div>
</header>
<div
css={{
borderTop: `1px solid ${theme.palette.divider}`,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "8px 24px",
fontSize: 12,
color: theme.palette.text.secondary,
}}
>
{warnings && warnings.length > 0 ? (
<div css={{ display: "flex", flexDirection: "column" }}>
{warnings.map((warning) => (
<span key={warning.code}>{warning.message}</span>
))}
</div>
) : (
<span>No warnings</span>
)}
{provisioner.last_seen_at && (
<span css={{ color: theme.roles.info.text }} data-chromatic="ignore">
Last seen {createDayString(provisioner.last_seen_at)}
</span>
)}
</div>
</div>
);
};
@@ -0,0 +1,105 @@
import type { Interpolation, Theme } from "@emotion/react";
import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined";
import CloseIcon from "@mui/icons-material/Close";
import DoNotDisturbOnOutlined from "@mui/icons-material/DoNotDisturbOnOutlined";
import Sell from "@mui/icons-material/Sell";
import IconButton from "@mui/material/IconButton";
import { Pill } from "components/Pill/Pill";
import type { ComponentProps, FC } from "react";
const parseBool = (s: string): { valid: boolean; value: boolean } => {
switch (s.toLowerCase()) {
case "true":
case "yes":
case "1":
return { valid: true, value: true };
case "false":
case "no":
case "0":
case "":
return { valid: true, value: false };
default:
return { valid: false, value: false };
}
};
interface ProvisionerTagProps {
tagName: string;
tagValue: string;
/** Only used in the TemplateVersionEditor */
onDelete?: (tagName: string) => void;
}
export const ProvisionerTag: FC<ProvisionerTagProps> = ({
tagName,
tagValue,
onDelete,
}) => {
const { valid, value: boolValue } = parseBool(tagValue);
const kv = (
<>
<span css={{ fontWeight: 600 }}>{tagName}</span> <span>{tagValue}</span>
</>
);
const content = onDelete ? (
<>
{kv}
<IconButton
aria-label={`delete-${tagName}`}
size="small"
color="secondary"
onClick={() => {
onDelete(tagName);
}}
>
<CloseIcon fontSize="inherit" css={{ width: 14, height: 14 }} />
</IconButton>
</>
) : (
kv
);
if (valid) {
return <BooleanPill value={boolValue}>{content}</BooleanPill>;
}
return (
<Pill size="lg" icon={<Sell />}>
{content}
</Pill>
);
};
type BooleanPillProps = Omit<ComponentProps<typeof Pill>, "icon" | "value"> & {
value: boolean;
};
export const BooleanPill: FC<BooleanPillProps> = ({
value,
children,
...divProps
}) => {
return (
<Pill
type={value ? "active" : "danger"}
size="lg"
icon={
value ? (
<CheckCircleOutlined css={styles.truePill} />
) : (
<DoNotDisturbOnOutlined css={styles.falsePill} />
)
}
{...divProps}
>
{children}
</Pill>
);
};
const styles = {
truePill: (theme) => ({
color: theme.roles.active.outline,
}),
falsePill: (theme) => ({
color: theme.roles.danger.outline,
}),
} satisfies Record<string, Interpolation<Theme>>;
+1 -1
View File
@@ -195,7 +195,7 @@ export const BooleanPill: FC<BooleanPillProps> = ({
...divProps
}) => {
const theme = useTheme();
const color = value ? theme.palette.success.light : theme.palette.error.light;
const color = value ? theme.roles.success.outline : theme.roles.error.outline;
return (
<Pill
@@ -1,33 +1,23 @@
import { useTheme } from "@emotion/react";
import Business from "@mui/icons-material/Business";
import CloseIcon from "@mui/icons-material/Close";
import Person from "@mui/icons-material/Person";
import Sell from "@mui/icons-material/Sell";
import SwapHoriz from "@mui/icons-material/SwapHoriz";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import type { HealthcheckReport } from "api/typesGenerated";
import { Alert } from "components/Alert/Alert";
import { Provisioner } from "modules/provisioners/Provisioner";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useOutletContext } from "react-router-dom";
import { createDayString } from "utils/createDayString";
import { pageTitle } from "utils/page";
import {
BooleanPill,
Header,
HeaderTitle,
HealthMessageDocsLink,
HealthyDot,
Main,
Pill,
} from "./Content";
import { DismissWarningButton } from "./DismissWarningButton";
export const ProvisionerDaemonsPage: FC = () => {
const healthStatus = useOutletContext<HealthcheckReport>();
const { provisioner_daemons: daemons } = healthStatus;
const theme = useTheme();
return (
<>
<Helmet>
@@ -56,169 +46,16 @@ export const ProvisionerDaemonsPage: FC = () => {
);
})}
{daemons.items.map(({ provisioner_daemon: daemon, warnings }) => {
const daemonScope = daemon.tags.scope || "organization";
const iconScope =
daemonScope === "organization" ? <Business /> : <Person />;
const extraTags = Object.entries(daemon.tags).filter(
([key]) => key !== "scope" && key !== "owner",
);
const isWarning = warnings.length > 0;
return (
<div
key={daemon.name}
css={{
borderRadius: 8,
border: `1px solid ${
isWarning
? theme.palette.warning.light
: theme.palette.divider
}`,
fontSize: 14,
}}
>
<header
css={{
padding: 24,
display: "flex",
alignItems: "center",
justifyContenxt: "space-between",
gap: 24,
}}
>
<div
css={{
display: "flex",
alignItems: "center",
gap: 24,
objectFit: "fill",
}}
>
<div css={{ lineHeight: "160%" }}>
<h4 css={{ fontWeight: 500, margin: 0 }}>{daemon.name}</h4>
<span css={{ color: theme.palette.text.secondary }}>
<code>{daemon.version}</code>
</span>
</div>
</div>
<div
css={{
marginLeft: "auto",
display: "flex",
flexWrap: "wrap",
gap: 12,
}}
>
<Tooltip title="API Version">
<Pill icon={<SwapHoriz />}>
<code>{daemon.api_version}</code>
</Pill>
</Tooltip>
<Tooltip title="Scope">
<Pill icon={iconScope}>
<span
css={{
":first-letter": { textTransform: "uppercase" },
}}
>
{daemonScope}
</span>
</Pill>
</Tooltip>
{extraTags.map(([key, value]) => (
<ProvisionerTag key={key} tagName={key} tagValue={value} />
))}
</div>
</header>
<div
css={{
borderTop: `1px solid ${theme.palette.divider}`,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "8px 24px",
fontSize: 12,
color: theme.palette.text.secondary,
}}
>
{warnings.length > 0 ? (
<div css={{ display: "flex", flexDirection: "column" }}>
{warnings.map((warning) => (
<span key={warning.code}>{warning.message}</span>
))}
</div>
) : (
<span>No warnings</span>
)}
{daemon.last_seen_at && (
<span
css={{ color: theme.palette.text.secondary }}
data-chromatic="ignore"
>
Last seen {createDayString(daemon.last_seen_at)}
</span>
)}
</div>
</div>
);
})}
{daemons.items.map(({ provisioner_daemon, warnings }) => (
<Provisioner
key={provisioner_daemon.id}
provisioner={provisioner_daemon}
warnings={warnings}
/>
))}
</Main>
</>
);
};
const parseBool = (s: string): { valid: boolean; value: boolean } => {
switch (s.toLowerCase()) {
case "true":
case "yes":
case "1":
return { valid: true, value: true };
case "false":
case "no":
case "0":
case "":
return { valid: true, value: false };
default:
return { valid: false, value: false };
}
};
interface ProvisionerTagProps {
tagName: string;
tagValue: string;
onDelete?: (tagName: string) => void;
}
export const ProvisionerTag: FC<ProvisionerTagProps> = ({
tagName,
tagValue,
onDelete,
}) => {
const { valid, value: boolValue } = parseBool(tagValue);
const kv = `${tagName}: ${tagValue}`;
const content = onDelete ? (
<>
{kv}
<IconButton
aria-label={`delete-${tagName}`}
size="small"
color="secondary"
onClick={() => {
onDelete(tagName);
}}
>
<CloseIcon fontSize="inherit" css={{ width: 14, height: 14 }} />
</IconButton>
</>
) : (
kv
);
if (valid) {
return <BooleanPill value={boolValue}>{content}</BooleanPill>;
}
return <Pill icon={<Sell />}>{content}</Pill>;
};
export default ProvisionerDaemonsPage;
+3 -3
View File
@@ -4,10 +4,10 @@ import type { HealthSeverity } from "api/typesGenerated";
export const healthyColor = (theme: Theme, severity: HealthSeverity) => {
switch (severity) {
case "ok":
return theme.palette.success.light;
return theme.roles.success.fill.solid;
case "warning":
return theme.palette.warning.light;
return theme.roles.warning.fill.solid;
case "error":
return theme.palette.error.light;
return theme.roles.error.fill.solid;
}
};
@@ -0,0 +1,63 @@
import {
organizationsPermissions,
provisionerDaemons,
} from "api/queries/organizations";
import type { Organization } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Loader } from "components/Loader/Loader";
import NotFoundPage from "pages/404Page/404Page";
import type { FC } from "react";
import { useQuery } from "react-query";
import { useParams } from "react-router-dom";
import { useOrganizationSettings } from "./ManagementSettingsLayout";
import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView";
const OrganizationProvisionersPage: FC = () => {
const { organization: organizationName } = useParams() as {
organization: string;
};
const { organizations } = useOrganizationSettings();
const organization = organizations
? getOrganizationByName(organizations, organizationName)
: undefined;
const permissionsQuery = useQuery(
organizationsPermissions(organizations?.map((o) => o.id)),
);
const provisionersQuery = useQuery(provisionerDaemons(organizationName));
if (!organization) {
return <EmptyState message="Organization not found" />;
}
if (permissionsQuery.isLoading || provisionersQuery.isLoading) {
return <Loader />;
}
const permissions = permissionsQuery.data;
const provisioners = provisionersQuery.data;
const error = permissionsQuery.error || provisionersQuery.error;
if (error || !permissions || !provisioners) {
return <ErrorAlert error={error} />;
}
// The user may not be able to edit this org but they can still see it because
// they can edit members, etc. In this case they will be shown a read-only
// summary page instead of the settings form.
// Similarly, if the feature is not entitled then the user will not be able to
// edit the organization.
if (!permissions[organization.id]?.viewProvisioners) {
// This probably doesn't work with the layout................fix this pls
// Kayla, hey, yes you, you gotta fix this.
// Don't scroll past this. It's important. Fix it!!!
return <NotFoundPage />;
}
return <OrganizationProvisionersPageView provisioners={provisioners} />;
};
export default OrganizationProvisionersPage;
const getOrganizationByName = (organizations: Organization[], name: string) =>
organizations.find((org) => org.name === name);
@@ -0,0 +1,29 @@
import type { Meta, StoryObj } from "@storybook/react";
import { MockProvisioner, MockUserProvisioner } from "testHelpers/entities";
import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView";
const meta: Meta<typeof OrganizationProvisionersPageView> = {
title: "pages/OrganizationProvisionersPage",
component: OrganizationProvisionersPageView,
};
export default meta;
type Story = StoryObj<typeof OrganizationProvisionersPageView>;
export const Provisioners: Story = {
args: {
provisioners: [
MockProvisioner,
MockUserProvisioner,
{
...MockProvisioner,
tags: {
...MockProvisioner.tags,
: "ユタ",
: "yes",
: "no",
},
},
],
},
};
@@ -0,0 +1,41 @@
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import Button from "@mui/material/Button";
import type { ProvisionerDaemon } from "api/typesGenerated";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
import { Stack } from "components/Stack/Stack";
import { Provisioner } from "modules/provisioners/Provisioner";
import type { FC } from "react";
import { docs } from "utils/docs";
interface OrganizationProvisionersPageViewProps {
provisioners: ProvisionerDaemon[];
}
export const OrganizationProvisionersPageView: FC<
OrganizationProvisionersPageViewProps
> = ({ provisioners }) => {
return (
<div>
<PageHeader
// The deployment settings layout already has padding.
css={{ paddingTop: 0 }}
actions={
<Button
endIcon={<OpenInNewIcon />}
target="_blank"
href={docs("/admin/provisioners")}
>
Create a provisioner
</Button>
}
>
<PageHeaderTitle>Provisioners</PageHeaderTitle>
</PageHeader>
<Stack spacing={4.5}>
{provisioners.map((provisioner) => (
<Provisioner key={provisioner.id} provisioner={provisioner} />
))}
</Stack>
</div>
);
};
@@ -4,6 +4,7 @@ import {
updateOrganization,
} from "api/queries/organizations";
import type { Organization } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { EmptyState } from "components/EmptyState/EmptyState";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
@@ -42,11 +43,15 @@ const OrganizationSettingsPage: FC = () => {
organizationsPermissions(organizations?.map((o) => o.id)),
);
const permissions = permissionsQuery.data;
if (!organizations || !permissions) {
if (permissionsQuery.isLoading) {
return <Loader />;
}
const permissions = permissionsQuery.data;
if (permissionsQuery.error || !permissions) {
return <ErrorAlert error={permissionsQuery.error} />;
}
// Redirect /organizations => /organizations/default-org, or if they cannot edit
// the default org, then the first org they can edit, if any.
if (!organizationName) {
@@ -275,6 +275,13 @@ const OrganizationSettingsNavigation: FC<
Roles
</SidebarNavSubItem>
)}
{organization.permissions.viewProvisioners && (
<SidebarNavSubItem
href={urlForSubpage(organization.name, "provisioners")}
>
Provisioners
</SidebarNavSubItem>
)}
</Stack>
)}
</>
@@ -31,7 +31,7 @@ describe("ProvisionerTagsPopover", () => {
await userEvent.click(btn);
// Check for existing tags
const el = await screen.findByText(/scope: organization/i);
const el = await screen.findByText(/scope/i);
expect(el).toBeInTheDocument();
// Add key and value
@@ -62,8 +62,10 @@ describe("ProvisionerTagsPopover", () => {
);
// Check for new tag
const el4 = await screen.findByText(/foo: bar/i);
expect(el4).toBeInTheDocument();
const fooTag = await screen.findByText(/foo/i);
expect(fooTag).toBeInTheDocument();
const barValue = await screen.findByText(/bar/i);
expect(barValue).toBeInTheDocument();
});
it("can remove a tag", async () => {
const onSubmit = jest.fn().mockImplementation(({ key, value }) => {
@@ -87,7 +89,7 @@ describe("ProvisionerTagsPopover", () => {
await userEvent.click(btn);
// Check for existing tags
const el = await screen.findByText(/wowzers: whatatag/i);
const el = await screen.findByText(/wowzers/i);
expect(el).toBeInTheDocument();
// Find Delete button
@@ -110,7 +112,7 @@ describe("ProvisionerTagsPopover", () => {
);
// Expect deleted tag to be gone
const el2 = screen.queryByText(/wowzers: whatatag/i);
const el2 = screen.queryByText(/wowzers/i);
expect(el2).not.toBeInTheDocument();
});
});
@@ -13,7 +13,7 @@ import {
} from "components/Popover/Popover";
import { Stack } from "components/Stack/Stack";
import { useFormik } from "formik";
import { ProvisionerTag } from "pages/HealthPage/ProvisionerDaemonsPage";
import { ProvisionerTag } from "modules/provisioners/ProvisionerTag";
import { type FC, Fragment } from "react";
import { docs } from "utils/docs";
import { getFormHelpers, onChangeTrimmed } from "utils/formUtils";
+7
View File
@@ -251,6 +251,9 @@ const CreateEditRolePage = lazy(
() =>
import("./pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePage"),
);
const OrganizationProvisionersPage = lazy(
() => import("./pages/ManagementSettingsPage/OrganizationProvisionersPage"),
);
const TemplateEmbedPage = lazy(
() => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"),
);
@@ -399,6 +402,10 @@ export const router = createBrowserRouter(
<Route path="create" element={<CreateEditRolePage />} />
<Route path=":roleName" element={<CreateEditRolePage />} />
</Route>
<Route
path="provisioners"
element={<OrganizationProvisionersPage />}
/>
</Route>
</Route>
+15 -19
View File
@@ -542,19 +542,15 @@ export const MockProvisioner: TypesGen.ProvisionerDaemon = {
name: "Test Provisioner",
provisioners: ["echo"],
tags: { scope: "organization" },
version: "v2.34.5",
api_version: "1.0",
version: MockBuildInfo.version,
api_version: MockBuildInfo.provisioner_api_version,
};
export const MockUserProvisioner: TypesGen.ProvisionerDaemon = {
created_at: "2022-05-17T17:39:01.382927298Z",
...MockProvisioner,
id: "test-user-provisioner",
organization_id: MockOrganization.id,
name: "Test User Provisioner",
provisioners: ["echo"],
tags: { scope: "user", owner: "12345678-abcd-1234-abcd-1234567890abcd" },
version: "v2.34.5",
api_version: "1.0",
};
export const MockProvisionerJob: TypesGen.ProvisionerJob = {
@@ -826,7 +822,7 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = {
status: "connected",
updated_at: "",
version: MockBuildInfo.version,
api_version: "1.0",
api_version: MockBuildInfo.agent_api_version,
latency: {
"Coder Embedded DERP": {
latency_ms: 32.55,
@@ -3313,7 +3309,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
created_at: "2023-05-01T19:15:56.606593Z",
updated_at: "2023-12-05T14:13:36.647535Z",
deleted: false,
version: "v2.5.0-devel+5fad61102",
version: MockBuildInfo.version,
},
{
id: "9d786ce0-55b1-4ace-8acc-a4672ff8d41f",
@@ -3336,7 +3332,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
created_at: "2023-05-01T20:34:11.114005Z",
updated_at: "2023-12-05T14:13:45.941716Z",
deleted: false,
version: "v2.5.0-devel+5fad61102",
version: MockBuildInfo.version,
},
{
id: "2e209786-73b1-4838-ba78-e01c9334450a",
@@ -3359,7 +3355,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
created_at: "2023-05-01T20:41:02.76448Z",
updated_at: "2023-12-05T14:13:41.968568Z",
deleted: false,
version: "v2.5.0-devel+5fad61102",
version: MockBuildInfo.version,
},
{
id: "c272e80c-0cce-49d6-9782-1b5cf90398e8",
@@ -3430,7 +3426,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
created_at: "2023-12-01T09:21:15.996267Z",
updated_at: "2023-12-05T14:13:59.663174Z",
deleted: false,
version: "v2.5.0-devel+5fad61102",
version: MockBuildInfo.version,
},
{
id: "72649dc9-03c7-46a8-bc95-96775e93ddc1",
@@ -3453,7 +3449,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
created_at: "2023-12-01T09:23:44.505529Z",
updated_at: "2023-12-05T14:13:55.769058Z",
deleted: false,
version: "v2.5.0-devel+5fad61102",
version: MockBuildInfo.version,
},
{
id: "1f78398f-e5ae-4c38-aa89-30222181d443",
@@ -3476,7 +3472,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
created_at: "2023-12-01T09:36:00.231252Z",
updated_at: "2023-12-05T14:13:47.015031Z",
deleted: false,
version: "v2.5.0-devel+5fad61102",
version: MockBuildInfo.version,
},
],
},
@@ -3502,8 +3498,8 @@ export const MockHealth: TypesGen.HealthcheckReport = {
created_at: "2024-01-04T15:53:03.21563Z",
last_seen_at: "2024-01-04T16:05:03.967551Z",
name: "ok",
version: "v2.3.4-devel+abcd1234",
api_version: "1.0",
version: MockBuildInfo.version,
api_version: MockBuildInfo.provisioner_api_version,
provisioners: ["echo", "terraform"],
tags: {
owner: "",
@@ -3523,8 +3519,8 @@ export const MockHealth: TypesGen.HealthcheckReport = {
created_at: "2024-01-04T15:53:03.21563Z",
last_seen_at: "2024-01-04T16:05:03.967551Z",
name: "user-scoped",
version: "v2.34-devel+abcd1234",
api_version: "1.0",
version: MockBuildInfo.version,
api_version: MockBuildInfo.provisioner_api_version,
provisioners: ["echo", "terraform"],
tags: {
owner: "12345678-1234-1234-1234-12345678abcd",
@@ -3569,7 +3565,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
},
],
},
coder_version: "v2.5.0-devel+5fad61102",
coder_version: MockBuildInfo.version,
};
export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsResponse =