mirror of
https://github.com/coder/coder.git
synced 2025-07-18 14:17:22 +00:00
feat(site): warn on provisioner health during builds (#15589)
This PR adds warning alerts to log drawers for templates and template versions. warning alerts for workspace builds to follow in a subsequent PR. Phrasing to be finalised. Stories added and manually verified. See screenshots below. Updating a template version with no provisioners: <img width="1250" alt="Screenshot 2024-11-27 at 11 06 28" src="https://github.com/user-attachments/assets/47aa0940-57a8-44e1-b9a3-25a638fa2c8d"> Build Errors for template versions now show tags as well: <img width="1250" alt="Screenshot 2024-11-27 at 11 07 01" src="https://github.com/user-attachments/assets/566e5339-0fe1-4cf7-8eab-9bf4892ed28a"> Updating a template version with provisioners that are busy or unresponsive: <img width="1250" alt="Screenshot 2024-11-27 at 11 06 40" src="https://github.com/user-attachments/assets/71977c8c-e4ed-457f-8587-2154850e7567"> Creating a new template with provisioners that are busy or unresponsive: <img width="819" alt="Screenshot 2024-11-27 at 11 08 55" src="https://github.com/user-attachments/assets/bda11501-b482-4046-95c5-feabcd1ad7f5"> Creating a new template when there are no provisioners to do the build: <img width="819" alt="Screenshot 2024-11-27 at 11 08 45" src="https://github.com/user-attachments/assets/e4279ebb-399e-4c6e-86e2-ead8f3ac7605">
This commit is contained in:
@ -682,12 +682,20 @@ class ApiMethods {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param organization Can be the organization's ID or name
|
* @param organization Can be the organization's ID or name
|
||||||
|
* @param tags to filter provisioner daemons by.
|
||||||
*/
|
*/
|
||||||
getProvisionerDaemonsByOrganization = async (
|
getProvisionerDaemonsByOrganization = async (
|
||||||
organization: string,
|
organization: string,
|
||||||
|
tags?: Record<string, string>,
|
||||||
): Promise<TypesGen.ProvisionerDaemon[]> => {
|
): Promise<TypesGen.ProvisionerDaemon[]> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
params.append("tags", JSON.stringify(tags));
|
||||||
|
}
|
||||||
|
|
||||||
const response = await this.axios.get<TypesGen.ProvisionerDaemon[]>(
|
const response = await this.axios.get<TypesGen.ProvisionerDaemon[]>(
|
||||||
`/api/v2/organizations/${organization}/provisionerdaemons`,
|
`/api/v2/organizations/${organization}/provisionerdaemons?${params.toString()}`,
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
@ -115,16 +115,18 @@ export const organizations = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProvisionerDaemonsKey = (organization: string) => [
|
export const getProvisionerDaemonsKey = (
|
||||||
"organization",
|
organization: string,
|
||||||
organization,
|
tags?: Record<string, string>,
|
||||||
"provisionerDaemons",
|
) => ["organization", organization, tags, "provisionerDaemons"];
|
||||||
];
|
|
||||||
|
|
||||||
export const provisionerDaemons = (organization: string) => {
|
export const provisionerDaemons = (
|
||||||
|
organization: string,
|
||||||
|
tags?: Record<string, string>,
|
||||||
|
) => {
|
||||||
return {
|
return {
|
||||||
queryKey: getProvisionerDaemonsKey(organization),
|
queryKey: getProvisionerDaemonsKey(organization, tags),
|
||||||
queryFn: () => API.getProvisionerDaemonsByOrganization(organization),
|
queryFn: () => API.getProvisionerDaemonsByOrganization(organization, tags),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import MuiAlert, {
|
import MuiAlert, {
|
||||||
|
type AlertColor as MuiAlertColor,
|
||||||
type AlertProps as MuiAlertProps,
|
type AlertProps as MuiAlertProps,
|
||||||
// biome-ignore lint/nursery/noRestrictedImports: Used as base component
|
// biome-ignore lint/nursery/noRestrictedImports: Used as base component
|
||||||
} from "@mui/material/Alert";
|
} from "@mui/material/Alert";
|
||||||
@ -11,6 +12,8 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
|
export type AlertColor = MuiAlertColor;
|
||||||
|
|
||||||
export type AlertProps = MuiAlertProps & {
|
export type AlertProps = MuiAlertProps & {
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
dismissible?: boolean;
|
dismissible?: boolean;
|
||||||
|
28
site/src/modules/provisioners/ProvisionerAlert.stories.tsx
Normal file
28
site/src/modules/provisioners/ProvisionerAlert.stories.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import { chromatic } from "testHelpers/chromatic";
|
||||||
|
import { ProvisionerAlert } from "./ProvisionerAlert";
|
||||||
|
|
||||||
|
const meta: Meta<typeof ProvisionerAlert> = {
|
||||||
|
title: "modules/provisioners/ProvisionerAlert",
|
||||||
|
parameters: {
|
||||||
|
chromatic,
|
||||||
|
layout: "centered",
|
||||||
|
},
|
||||||
|
component: ProvisionerAlert,
|
||||||
|
args: {
|
||||||
|
title: "Title",
|
||||||
|
detail: "Detail",
|
||||||
|
severity: "info",
|
||||||
|
tags: { tag: "tagValue" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof ProvisionerAlert>;
|
||||||
|
|
||||||
|
export const Info: Story = {};
|
||||||
|
export const NullTags: Story = {
|
||||||
|
args: {
|
||||||
|
tags: undefined,
|
||||||
|
},
|
||||||
|
};
|
45
site/src/modules/provisioners/ProvisionerAlert.tsx
Normal file
45
site/src/modules/provisioners/ProvisionerAlert.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import AlertTitle from "@mui/material/AlertTitle";
|
||||||
|
import { Alert, type AlertColor } from "components/Alert/Alert";
|
||||||
|
import { AlertDetail } from "components/Alert/Alert";
|
||||||
|
import { Stack } from "components/Stack/Stack";
|
||||||
|
import { ProvisionerTag } from "modules/provisioners/ProvisionerTag";
|
||||||
|
import type { FC } from "react";
|
||||||
|
interface ProvisionerAlertProps {
|
||||||
|
title: string;
|
||||||
|
detail: string;
|
||||||
|
severity: AlertColor;
|
||||||
|
tags: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProvisionerAlert: FC<ProvisionerAlertProps> = ({
|
||||||
|
title,
|
||||||
|
detail,
|
||||||
|
severity,
|
||||||
|
tags,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
severity={severity}
|
||||||
|
css={(theme) => {
|
||||||
|
return {
|
||||||
|
borderRadius: 0,
|
||||||
|
border: 0,
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
borderLeft: `2px solid ${theme.palette[severity].main}`,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertTitle>{title}</AlertTitle>
|
||||||
|
<AlertDetail>
|
||||||
|
<div>{detail}</div>
|
||||||
|
<Stack direction="row" spacing={1} wrap="wrap">
|
||||||
|
{Object.entries(tags ?? {})
|
||||||
|
.filter(([key]) => key !== "owner")
|
||||||
|
.map(([key, value]) => (
|
||||||
|
<ProvisionerTag key={key} tagName={key} tagValue={value} />
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</AlertDetail>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,55 @@
|
|||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import { chromatic } from "testHelpers/chromatic";
|
||||||
|
import { MockTemplateVersion } from "testHelpers/entities";
|
||||||
|
import { ProvisionerStatusAlert } from "./ProvisionerStatusAlert";
|
||||||
|
|
||||||
|
const meta: Meta<typeof ProvisionerStatusAlert> = {
|
||||||
|
title: "modules/provisioners/ProvisionerStatusAlert",
|
||||||
|
parameters: {
|
||||||
|
chromatic,
|
||||||
|
layout: "centered",
|
||||||
|
},
|
||||||
|
component: ProvisionerStatusAlert,
|
||||||
|
args: {
|
||||||
|
matchingProvisioners: 0,
|
||||||
|
availableProvisioners: 0,
|
||||||
|
tags: MockTemplateVersion.job.tags,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof ProvisionerStatusAlert>;
|
||||||
|
|
||||||
|
export const HealthyProvisioners: Story = {
|
||||||
|
args: {
|
||||||
|
matchingProvisioners: 1,
|
||||||
|
availableProvisioners: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UndefinedMatchingProvisioners: Story = {
|
||||||
|
args: {
|
||||||
|
matchingProvisioners: undefined,
|
||||||
|
availableProvisioners: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UndefinedAvailableProvisioners: Story = {
|
||||||
|
args: {
|
||||||
|
matchingProvisioners: 1,
|
||||||
|
availableProvisioners: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NoMatchingProvisioners: Story = {
|
||||||
|
args: {
|
||||||
|
matchingProvisioners: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NoAvailableProvisioners: Story = {
|
||||||
|
args: {
|
||||||
|
matchingProvisioners: 1,
|
||||||
|
availableProvisioners: 0,
|
||||||
|
},
|
||||||
|
};
|
47
site/src/modules/provisioners/ProvisionerStatusAlert.tsx
Normal file
47
site/src/modules/provisioners/ProvisionerStatusAlert.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import type { AlertColor } from "components/Alert/Alert";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import { ProvisionerAlert } from "./ProvisionerAlert";
|
||||||
|
|
||||||
|
interface ProvisionerStatusAlertProps {
|
||||||
|
matchingProvisioners: number | undefined;
|
||||||
|
availableProvisioners: number | undefined;
|
||||||
|
tags: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProvisionerStatusAlert: FC<ProvisionerStatusAlertProps> = ({
|
||||||
|
matchingProvisioners,
|
||||||
|
availableProvisioners,
|
||||||
|
tags,
|
||||||
|
}) => {
|
||||||
|
let title: string;
|
||||||
|
let detail: string;
|
||||||
|
let severity: AlertColor;
|
||||||
|
switch (true) {
|
||||||
|
case matchingProvisioners === 0:
|
||||||
|
title = "Build pending provisioner deployment";
|
||||||
|
detail =
|
||||||
|
"Your build has been enqueued, but there are no provisioners that accept the required tags. Once a compatible provisioner becomes available, your build will continue. Please contact your administrator.";
|
||||||
|
severity = "warning";
|
||||||
|
break;
|
||||||
|
case availableProvisioners === 0:
|
||||||
|
title = "Build delayed";
|
||||||
|
detail =
|
||||||
|
"Provisioners that accept the required tags have not responded for longer than expected. This may delay your build. Please contact your administrator if your build does not complete.";
|
||||||
|
severity = "warning";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
title = "Build enqueued";
|
||||||
|
detail =
|
||||||
|
"Your build has been enqueued and will begin once a provisioner becomes available to process it.";
|
||||||
|
severity = "info";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProvisionerAlert
|
||||||
|
title={title}
|
||||||
|
detail={detail}
|
||||||
|
severity={severity}
|
||||||
|
tags={tags}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -34,6 +34,42 @@ export const MissingVariables: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const NoProvisioners: Story = {
|
||||||
|
args: {
|
||||||
|
templateVersion: {
|
||||||
|
...MockTemplateVersion,
|
||||||
|
matched_provisioners: {
|
||||||
|
count: 0,
|
||||||
|
available: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProvisionersUnhealthy: Story = {
|
||||||
|
args: {
|
||||||
|
templateVersion: {
|
||||||
|
...MockTemplateVersion,
|
||||||
|
matched_provisioners: {
|
||||||
|
count: 1,
|
||||||
|
available: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProvisionersHealthy: Story = {
|
||||||
|
args: {
|
||||||
|
templateVersion: {
|
||||||
|
...MockTemplateVersion,
|
||||||
|
matched_provisioners: {
|
||||||
|
count: 1,
|
||||||
|
available: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const Logs: Story = {
|
export const Logs: Story = {
|
||||||
args: {
|
args: {
|
||||||
templateVersion: {
|
templateVersion: {
|
||||||
|
@ -8,6 +8,7 @@ import { visuallyHidden } from "@mui/utils";
|
|||||||
import { JobError } from "api/queries/templates";
|
import { JobError } from "api/queries/templates";
|
||||||
import type { TemplateVersion } from "api/typesGenerated";
|
import type { TemplateVersion } from "api/typesGenerated";
|
||||||
import { Loader } from "components/Loader/Loader";
|
import { Loader } from "components/Loader/Loader";
|
||||||
|
import { ProvisionerStatusAlert } from "modules/provisioners/ProvisionerStatusAlert";
|
||||||
import { useWatchVersionLogs } from "modules/templates/useWatchVersionLogs";
|
import { useWatchVersionLogs } from "modules/templates/useWatchVersionLogs";
|
||||||
import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs";
|
import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs";
|
||||||
import { type FC, useLayoutEffect, useRef } from "react";
|
import { type FC, useLayoutEffect, useRef } from "react";
|
||||||
@ -27,6 +28,10 @@ export const BuildLogsDrawer: FC<BuildLogsDrawerProps> = ({
|
|||||||
variablesSectionRef,
|
variablesSectionRef,
|
||||||
...drawerProps
|
...drawerProps
|
||||||
}) => {
|
}) => {
|
||||||
|
const matchingProvisioners = templateVersion?.matched_provisioners?.count;
|
||||||
|
const availableProvisioners =
|
||||||
|
templateVersion?.matched_provisioners?.available;
|
||||||
|
|
||||||
const logs = useWatchVersionLogs(templateVersion);
|
const logs = useWatchVersionLogs(templateVersion);
|
||||||
const logsContainer = useRef<HTMLDivElement>(null);
|
const logsContainer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -65,6 +70,8 @@ export const BuildLogsDrawer: FC<BuildLogsDrawerProps> = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
{isMissingVariables ? (
|
{isMissingVariables ? (
|
||||||
<MissingVariablesBanner
|
<MissingVariablesBanner
|
||||||
onFillVariables={() => {
|
onFillVariables={() => {
|
||||||
@ -82,7 +89,14 @@ export const BuildLogsDrawer: FC<BuildLogsDrawerProps> = ({
|
|||||||
<WorkspaceBuildLogs logs={logs} css={{ border: 0 }} />
|
<WorkspaceBuildLogs logs={logs} css={{ border: 0 }} />
|
||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<Loader />
|
<>
|
||||||
|
<ProvisionerStatusAlert
|
||||||
|
matchingProvisioners={matchingProvisioners}
|
||||||
|
availableProvisioners={availableProvisioners}
|
||||||
|
tags={templateVersion?.job.tags ?? {}}
|
||||||
|
/>
|
||||||
|
<Loader />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
@ -49,6 +49,73 @@ type Story = StoryObj<typeof TemplateVersionEditor>;
|
|||||||
|
|
||||||
export const Example: Story = {};
|
export const Example: Story = {};
|
||||||
|
|
||||||
|
export const UndefinedLogs: Story = {
|
||||||
|
args: {
|
||||||
|
defaultTab: "logs",
|
||||||
|
buildLogs: undefined,
|
||||||
|
templateVersion: {
|
||||||
|
...MockTemplateVersion,
|
||||||
|
job: MockRunningProvisionerJob,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmptyLogs: Story = {
|
||||||
|
args: {
|
||||||
|
defaultTab: "logs",
|
||||||
|
buildLogs: [],
|
||||||
|
templateVersion: {
|
||||||
|
...MockTemplateVersion,
|
||||||
|
job: MockRunningProvisionerJob,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NoProvisioners: Story = {
|
||||||
|
args: {
|
||||||
|
defaultTab: "logs",
|
||||||
|
buildLogs: [],
|
||||||
|
templateVersion: {
|
||||||
|
...MockTemplateVersion,
|
||||||
|
job: MockRunningProvisionerJob,
|
||||||
|
matched_provisioners: {
|
||||||
|
count: 0,
|
||||||
|
available: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UnavailableProvisioners: Story = {
|
||||||
|
args: {
|
||||||
|
defaultTab: "logs",
|
||||||
|
buildLogs: [],
|
||||||
|
templateVersion: {
|
||||||
|
...MockTemplateVersion,
|
||||||
|
job: MockRunningProvisionerJob,
|
||||||
|
matched_provisioners: {
|
||||||
|
count: 1,
|
||||||
|
available: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HealthyProvisioners: Story = {
|
||||||
|
args: {
|
||||||
|
defaultTab: "logs",
|
||||||
|
buildLogs: [],
|
||||||
|
templateVersion: {
|
||||||
|
...MockTemplateVersion,
|
||||||
|
job: MockRunningProvisionerJob,
|
||||||
|
matched_provisioners: {
|
||||||
|
count: 1,
|
||||||
|
available: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const Logs: Story = {
|
export const Logs: Story = {
|
||||||
args: {
|
args: {
|
||||||
defaultTab: "logs",
|
defaultTab: "logs",
|
||||||
|
@ -4,7 +4,6 @@ import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined";
|
|||||||
import CloseOutlined from "@mui/icons-material/CloseOutlined";
|
import CloseOutlined from "@mui/icons-material/CloseOutlined";
|
||||||
import PlayArrowOutlined from "@mui/icons-material/PlayArrowOutlined";
|
import PlayArrowOutlined from "@mui/icons-material/PlayArrowOutlined";
|
||||||
import WarningOutlined from "@mui/icons-material/WarningOutlined";
|
import WarningOutlined from "@mui/icons-material/WarningOutlined";
|
||||||
import AlertTitle from "@mui/material/AlertTitle";
|
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@ -17,7 +16,7 @@ import type {
|
|||||||
VariableValue,
|
VariableValue,
|
||||||
WorkspaceResource,
|
WorkspaceResource,
|
||||||
} from "api/typesGenerated";
|
} from "api/typesGenerated";
|
||||||
import { Alert, AlertDetail } from "components/Alert/Alert";
|
import { Alert } from "components/Alert/Alert";
|
||||||
import { Sidebar } from "components/FullPageLayout/Sidebar";
|
import { Sidebar } from "components/FullPageLayout/Sidebar";
|
||||||
import {
|
import {
|
||||||
Topbar,
|
Topbar,
|
||||||
@ -29,6 +28,8 @@ import {
|
|||||||
} from "components/FullPageLayout/Topbar";
|
} from "components/FullPageLayout/Topbar";
|
||||||
import { Loader } from "components/Loader/Loader";
|
import { Loader } from "components/Loader/Loader";
|
||||||
import { linkToTemplate, useLinks } from "modules/navigation";
|
import { linkToTemplate, useLinks } from "modules/navigation";
|
||||||
|
import { ProvisionerAlert } from "modules/provisioners/ProvisionerAlert";
|
||||||
|
import { ProvisionerStatusAlert } from "modules/provisioners/ProvisionerStatusAlert";
|
||||||
import { TemplateFileTree } from "modules/templates/TemplateFiles/TemplateFileTree";
|
import { TemplateFileTree } from "modules/templates/TemplateFiles/TemplateFileTree";
|
||||||
import { isBinaryData } from "modules/templates/TemplateFiles/isBinaryData";
|
import { isBinaryData } from "modules/templates/TemplateFiles/isBinaryData";
|
||||||
import { TemplateResourcesTable } from "modules/templates/TemplateResourcesTable/TemplateResourcesTable";
|
import { TemplateResourcesTable } from "modules/templates/TemplateResourcesTable/TemplateResourcesTable";
|
||||||
@ -126,6 +127,8 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||||||
const [deleteFileOpen, setDeleteFileOpen] = useState<string>();
|
const [deleteFileOpen, setDeleteFileOpen] = useState<string>();
|
||||||
const [renameFileOpen, setRenameFileOpen] = useState<string>();
|
const [renameFileOpen, setRenameFileOpen] = useState<string>();
|
||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
|
const matchingProvisioners = templateVersion.matched_provisioners?.count;
|
||||||
|
const availableProvisioners = templateVersion.matched_provisioners?.available;
|
||||||
|
|
||||||
const triggerPreview = useCallback(async () => {
|
const triggerPreview = useCallback(async () => {
|
||||||
await onPreview(fileTree);
|
await onPreview(fileTree);
|
||||||
@ -192,6 +195,8 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||||||
linkToTemplate(template.organization_name, template.name),
|
linkToTemplate(template.organization_name, template.name),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const gotBuildLogs = buildLogs && buildLogs.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div css={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
<div css={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||||
@ -581,31 +586,34 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||||||
css={[styles.logs, styles.tabContent]}
|
css={[styles.logs, styles.tabContent]}
|
||||||
ref={logsContentRef}
|
ref={logsContentRef}
|
||||||
>
|
>
|
||||||
{templateVersion.job.error && (
|
{templateVersion.job.error ? (
|
||||||
<div>
|
<div>
|
||||||
<Alert
|
<ProvisionerAlert
|
||||||
|
title="Error during the build"
|
||||||
|
detail={templateVersion.job.error}
|
||||||
severity="error"
|
severity="error"
|
||||||
css={{
|
tags={templateVersion.job.tags}
|
||||||
borderRadius: 0,
|
/>
|
||||||
border: 0,
|
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
||||||
borderLeft: `2px solid ${theme.palette.error.main}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AlertTitle>Error during the build</AlertTitle>
|
|
||||||
<AlertDetail>{templateVersion.job.error}</AlertDetail>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
!gotBuildLogs && (
|
||||||
|
<>
|
||||||
|
<ProvisionerStatusAlert
|
||||||
|
matchingProvisioners={matchingProvisioners}
|
||||||
|
availableProvisioners={availableProvisioners}
|
||||||
|
tags={templateVersion.job.tags}
|
||||||
|
/>
|
||||||
|
<Loader css={{ height: "100%" }} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{buildLogs && buildLogs.length > 0 ? (
|
{gotBuildLogs && (
|
||||||
<WorkspaceBuildLogs
|
<WorkspaceBuildLogs
|
||||||
css={styles.buildLogs}
|
css={styles.buildLogs}
|
||||||
hideTimestamps
|
hideTimestamps
|
||||||
logs={buildLogs}
|
logs={buildLogs}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<Loader css={{ height: "100%" }} />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { StoryContext } from "@storybook/react";
|
import type { StoryContext } from "@storybook/react";
|
||||||
import { withDefaultFeatures } from "api/api";
|
import { withDefaultFeatures } from "api/api";
|
||||||
import { getAuthorizationKey } from "api/queries/authCheck";
|
import { getAuthorizationKey } from "api/queries/authCheck";
|
||||||
|
import { getProvisionerDaemonsKey } from "api/queries/organizations";
|
||||||
import { hasFirstUserKey, meKey } from "api/queries/users";
|
import { hasFirstUserKey, meKey } from "api/queries/users";
|
||||||
import type { Entitlements } from "api/typesGenerated";
|
import type { Entitlements } from "api/typesGenerated";
|
||||||
import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar";
|
import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar";
|
||||||
@ -121,6 +122,30 @@ export const withAuthProvider = (Story: FC, { parameters }: StoryContext) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const withProvisioners = (Story: FC, { parameters }: StoryContext) => {
|
||||||
|
if (!parameters.organization_id) {
|
||||||
|
throw new Error(
|
||||||
|
"You forgot to add `parameters.organization_id` to your story",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!parameters.provisioners) {
|
||||||
|
throw new Error(
|
||||||
|
"You forgot to add `parameters.provisioners` to your story",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!parameters.tags) {
|
||||||
|
throw new Error("You forgot to add `parameters.tags` to your story");
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
queryClient.setQueryData(
|
||||||
|
getProvisionerDaemonsKey(parameters.organization_id, parameters.tags),
|
||||||
|
parameters.provisioners,
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Story />;
|
||||||
|
};
|
||||||
|
|
||||||
export const withGlobalSnackbar = (Story: FC) => (
|
export const withGlobalSnackbar = (Story: FC) => (
|
||||||
<>
|
<>
|
||||||
<Story />
|
<Story />
|
||||||
|
Reference in New Issue
Block a user