mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
fix: resolve <WorkspacePage /> colors (#23902)
This pull-request ensures that our borders and content are all inline with the design-system, whilst also ripping out the old Material UI based design system. Furthermore, we're enforcing the background gradient to always be showing regardless of if `<ResourceMetadata />` has content. | Old | New | | --- | --- | | <img width="1624" height="1061" alt="image" src="https://github.com/user-attachments/assets/0accc324-b012-43e4-bb13-ec3629fbc909" /> | <img width="1624" height="1061" alt="image" src="https://github.com/user-attachments/assets/e89c752a-057c-4256-9f8e-728d1f89a1fd" /> |
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
type FC,
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
import type { ProvisionerJobLog, WorkspaceBuild } from "#/api/typesGenerated";
|
||||
import type { Line } from "#/components/Logs/LogLine";
|
||||
import { DEFAULT_LOG_LINE_SIDE_PADDING, Logs } from "#/components/Logs/Logs";
|
||||
import { BODY_FONT_FAMILY } from "#/theme/constants";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
type Stage = ProvisionerJobLog["stage"];
|
||||
@@ -89,12 +87,23 @@ export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
|
||||
return (
|
||||
<Fragment key={stage}>
|
||||
<div
|
||||
css={[styles.header, sticky && styles.sticky]}
|
||||
className="logs-header"
|
||||
className={cn(
|
||||
"logs-header text-sm font-semibold font-sans leading-none",
|
||||
"border-solid border-0 border-b bg-surface-primary",
|
||||
"flex items-center",
|
||||
"[&:last-child]:border-b-0 [&:last-child]:rounded-b-lg",
|
||||
"[&:first-of-type]:rounded-t-lg",
|
||||
sticky && "sticky top-0",
|
||||
)}
|
||||
style={{
|
||||
padding: `12px var(--log-line-side-padding, ${DEFAULT_LOG_LINE_SIDE_PADDING}px)`,
|
||||
}}
|
||||
>
|
||||
<div>{stage}</div>
|
||||
{shouldDisplayDuration && (
|
||||
<div css={styles.duration}>{duration} seconds</div>
|
||||
<div className="ml-auto text-content-secondary text-xs">
|
||||
{duration} seconds
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isEmpty && <Logs hideTimestamps={hideTimestamps} lines={lines} />}
|
||||
@@ -104,37 +113,3 @@ export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
header: (theme) => ({
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
padding: `12px var(--log-line-side-padding, ${DEFAULT_LOG_LINE_SIDE_PADDING}px)`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontFamily: BODY_FONT_FAMILY,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
background: theme.palette.background.default,
|
||||
lineHeight: "1",
|
||||
|
||||
"&:last-child": {
|
||||
borderBottom: 0,
|
||||
borderRadius: "0 0 8px 8px",
|
||||
},
|
||||
|
||||
"&:first-of-type": {
|
||||
borderRadius: "8px 8px 0 0",
|
||||
},
|
||||
}),
|
||||
|
||||
sticky: {
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
},
|
||||
|
||||
duration: (theme) => ({
|
||||
marginLeft: "auto",
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 12,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { cn } from "#/utils/cn";
|
||||
export type BarColors = {
|
||||
stroke: string;
|
||||
fill: string;
|
||||
@@ -30,7 +30,17 @@ export const Bar: React.FC<BarProps> = ({
|
||||
offset,
|
||||
...htmlProps
|
||||
}) => {
|
||||
return <div css={barCSS({ colors, scale, value, offset })} {...htmlProps} />;
|
||||
return (
|
||||
<div
|
||||
{...htmlProps}
|
||||
className={cn(
|
||||
"relative flex h-[inherit] min-w-6 rounded-lg border border-border bg-surface-primary p-[7px]",
|
||||
"after:absolute after:inset-y-[-2px] after:left-[-8px] after:right-[-8px] after:content-['']",
|
||||
htmlProps.className,
|
||||
)}
|
||||
style={barStyle({ colors, scale, value, offset, style: htmlProps.style })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ClickableBarProps = BaseBarProps<React.ComponentPropsWithRef<"button">>;
|
||||
@@ -45,55 +55,31 @@ export const ClickableBar: React.FC<ClickableBarProps> = ({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
css={[...barCSS({ colors, scale, value, offset }), styles.clickable]}
|
||||
{...htmlProps}
|
||||
className={cn(
|
||||
"relative flex h-inherit min-w-[34px] cursor-pointer rounded-lg border border-border bg-surface-primary p-[7px]",
|
||||
"after:absolute after:inset-y-[-2px] after:left-[-8px] after:right-[-8px] after:content-['']",
|
||||
"outline-none hover:border-content-link focus-visible:border-content-link active:border-content-link",
|
||||
htmlProps.className,
|
||||
)}
|
||||
style={barStyle({ colors, scale, value, offset, style: htmlProps.style })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const barCSS = ({ scale, value, colors, offset }: BaseBarProps<unknown>) => {
|
||||
return [
|
||||
styles.bar,
|
||||
{
|
||||
width: `calc((var(--x-axis-width) * ${value}) / ${scale})`,
|
||||
backgroundColor: colors?.fill,
|
||||
borderColor: colors?.stroke,
|
||||
marginLeft: `calc((var(--x-axis-width) * ${offset}) / ${scale})`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const styles = {
|
||||
bar: (theme) => ({
|
||||
border: "1px solid",
|
||||
borderColor: theme.palette.divider,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
borderRadius: 8,
|
||||
// The bar should fill the row height.
|
||||
height: "inherit",
|
||||
display: "flex",
|
||||
padding: 7,
|
||||
minWidth: 24,
|
||||
// Increase hover area
|
||||
position: "relative",
|
||||
"&::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: -2,
|
||||
right: -8,
|
||||
bottom: -2,
|
||||
left: -8,
|
||||
},
|
||||
}),
|
||||
clickable: (theme) => ({
|
||||
cursor: "pointer",
|
||||
// We need to make the bar width at least 34px to allow the "..." icons to be displayed.
|
||||
// The calculation is border * 1 + side paddings * 2 + icon width (which is 18px)
|
||||
minWidth: 34,
|
||||
|
||||
"&:focus, &:hover, &:active": {
|
||||
outline: "none",
|
||||
borderColor: theme.roles.active.outline,
|
||||
},
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
const barStyle = ({
|
||||
scale,
|
||||
value,
|
||||
colors,
|
||||
offset,
|
||||
style,
|
||||
}: BaseBarProps<unknown> & {
|
||||
style?: React.CSSProperties;
|
||||
}): React.CSSProperties => ({
|
||||
width: `calc((var(--x-axis-width) * ${value}) / ${scale})`,
|
||||
marginLeft: `calc((var(--x-axis-width) * ${offset}) / ${scale})`,
|
||||
backgroundColor: colors?.fill,
|
||||
borderColor: colors?.stroke,
|
||||
borderStyle: "solid",
|
||||
...style,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { EllipsisIcon } from "lucide-react";
|
||||
import { type FC, useEffect, useRef, useState } from "react";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
const spaceBetweenBlocks = 4;
|
||||
const moreIconSize = 18;
|
||||
@@ -25,49 +25,30 @@ export const Blocks: FC<BlocksProps> = ({ count }) => {
|
||||
const totalSpaceBetweenBlocks = (count - 1) * spaceBetweenBlocks;
|
||||
const necessarySize = blockSize * count + totalSpaceBetweenBlocks;
|
||||
const hasSpacing = necessarySize <= availableWidth;
|
||||
const nOfPossibleBlocks = Math.floor(
|
||||
(availableWidth - moreIconSize) / (blockSize + spaceBetweenBlocks),
|
||||
const nOfPossibleBlocks = Math.max(
|
||||
0,
|
||||
Math.floor(
|
||||
(availableWidth - moreIconSize) / (blockSize + spaceBetweenBlocks),
|
||||
),
|
||||
);
|
||||
const nOfBlocks = hasSpacing ? count : nOfPossibleBlocks;
|
||||
|
||||
return (
|
||||
<div ref={blocksRef} css={styles.blocks}>
|
||||
<div ref={blocksRef} className="flex h-full w-full items-center gap-1">
|
||||
{Array.from({ length: nOfBlocks }, (_, i) => i + 1).map((n) => (
|
||||
<div key={n} css={styles.block} style={{ minWidth: blockSize }} />
|
||||
<div
|
||||
key={n}
|
||||
className={cn(
|
||||
"h-[18px] min-w-5 shrink-0 flex-1 rounded",
|
||||
"border border-solid border-content-link bg-content-link/15",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{!hasSpacing && (
|
||||
<div css={styles.more}>
|
||||
<EllipsisIcon className="size-icon-sm" />
|
||||
<div className="shrink-0 flex-1 leading-none text-content-link">
|
||||
<EllipsisIcon className="size-[18px]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
blocks: {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
gap: spaceBetweenBlocks,
|
||||
alignItems: "center",
|
||||
},
|
||||
block: (theme) => ({
|
||||
borderRadius: 4,
|
||||
height: 18,
|
||||
backgroundColor: theme.roles.active.background,
|
||||
border: `1px solid ${theme.roles.active.outline}`,
|
||||
flexShrink: 0,
|
||||
flex: 1,
|
||||
}),
|
||||
more: (theme) => ({
|
||||
color: theme.roles.active.outline,
|
||||
lineHeight: 0,
|
||||
flexShrink: 0,
|
||||
flex: 1,
|
||||
|
||||
"& svg": {
|
||||
fontSize: moreIconSize,
|
||||
},
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import type { FC, HTMLProps } from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
@@ -6,10 +5,25 @@ import {
|
||||
SearchField,
|
||||
type SearchFieldProps,
|
||||
} from "#/components/SearchField/SearchField";
|
||||
import { cn } from "#/utils/cn";
|
||||
import type { BarColors } from "./Bar";
|
||||
|
||||
export const Chart = (props: HTMLProps<HTMLDivElement>) => {
|
||||
return <div css={styles.chart} {...props} />;
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn("flex h-full flex-col", props.className)}
|
||||
style={
|
||||
{
|
||||
"--header-height": "40px",
|
||||
"--section-padding": "16px",
|
||||
"--x-axis-rows-gap": "20px",
|
||||
"--y-axis-width": "200px",
|
||||
...props.style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChartContent: FC<HTMLProps<HTMLDivElement>> = (props) => {
|
||||
@@ -41,11 +55,36 @@ export const ChartContent: FC<HTMLProps<HTMLDivElement>> = (props) => {
|
||||
return () => contentEl.removeEventListener("scroll", handler);
|
||||
}, []);
|
||||
|
||||
return <div css={styles.content} {...props} ref={contentRef} />;
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
ref={contentRef}
|
||||
className={cn(
|
||||
"relative flex flex-1 items-stretch overflow-auto text-xs font-medium",
|
||||
"[scrollbar-color:hsl(var(--border-default))_hsl(var(--surface-primary))]",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-0 z-[1] h-[100px] transition-opacity duration-200 [bottom:calc(-1*var(--scroll-top,0px))] [opacity:var(--scroll-mask-opacity)] [background:linear-gradient(180deg,rgba(0,0,0,0)_0%,var(--surface-primary)_81.93%)]"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChartToolbar = (props: HTMLProps<HTMLDivElement>) => {
|
||||
return <div css={styles.toolbar} {...props} />;
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
"flex items-stretch border-b border-border text-xs",
|
||||
"border-solid border-0 border-b",
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ChartBreadcrumb = {
|
||||
@@ -61,18 +100,18 @@ export const ChartBreadcrumbs: FC<ChartBreadcrumbsProps> = ({
|
||||
breadcrumbs,
|
||||
}) => {
|
||||
return (
|
||||
<ul css={styles.breadcrumbs}>
|
||||
<ul className="m-0 flex w-[var(--y-axis-width)] shrink-0 list-none items-center gap-1 p-[var(--section-padding)] leading-none">
|
||||
{breadcrumbs.map((b, i) => {
|
||||
const isLast = i === breadcrumbs.length - 1;
|
||||
return (
|
||||
<React.Fragment key={b.label}>
|
||||
<li>
|
||||
<li className={cn(i === 0 && "text-content-secondary")}>
|
||||
{isLast ? (
|
||||
b.label
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
css={styles.breadcrumbButton}
|
||||
className="cursor-pointer border-0 bg-transparent p-0 text-inherit hover:text-content-primary"
|
||||
onClick={b.onClick}
|
||||
>
|
||||
{b.label}
|
||||
@@ -80,8 +119,8 @@ export const ChartBreadcrumbs: FC<ChartBreadcrumbsProps> = ({
|
||||
)}
|
||||
</li>
|
||||
{!isLast && (
|
||||
<li role="presentation">
|
||||
<ChevronRightIcon />
|
||||
<li role="presentation" className="text-content-secondary">
|
||||
<ChevronRightIcon className="size-[14px]" />
|
||||
</li>
|
||||
)}
|
||||
</React.Fragment>
|
||||
@@ -111,17 +150,18 @@ type ChartLegendsProps = {
|
||||
|
||||
export const ChartLegends: FC<ChartLegendsProps> = ({ legends }) => {
|
||||
return (
|
||||
<ul css={styles.legends}>
|
||||
<ul className="m-0 flex list-none items-center gap-6 pr-[var(--section-padding)] p-0">
|
||||
{legends.map((l) => (
|
||||
<li key={l.label} css={styles.legend}>
|
||||
<li
|
||||
key={l.label}
|
||||
className="flex items-center gap-2 font-medium leading-none"
|
||||
>
|
||||
<div
|
||||
css={[
|
||||
styles.legendSquare,
|
||||
{
|
||||
borderColor: l.colors?.stroke,
|
||||
backgroundColor: l.colors?.fill,
|
||||
},
|
||||
]}
|
||||
className="size-[18px] rounded border border-solid bg-surface-primary"
|
||||
style={{
|
||||
borderColor: l.colors?.stroke,
|
||||
backgroundColor: l.colors?.fill,
|
||||
}}
|
||||
/>
|
||||
{l.label}
|
||||
</li>
|
||||
@@ -129,113 +169,3 @@ export const ChartLegends: FC<ChartLegendsProps> = ({ legends }) => {
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
chart: {
|
||||
"--header-height": "40px",
|
||||
"--section-padding": "16px",
|
||||
"--x-axis-rows-gap": "20px",
|
||||
"--y-axis-width": "200px",
|
||||
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
content: (theme) => ({
|
||||
display: "flex",
|
||||
alignItems: "stretch",
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
overflow: "auto",
|
||||
flex: 1,
|
||||
scrollbarColor: `${theme.palette.divider} ${theme.palette.background.default}`,
|
||||
scrollbarWidth: "thin",
|
||||
position: "relative",
|
||||
|
||||
"&:before": {
|
||||
content: "''",
|
||||
position: "absolute",
|
||||
bottom: "calc(-1 * var(--scroll-top, 0px))",
|
||||
width: "100%",
|
||||
height: 100,
|
||||
background: `linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, ${theme.palette.background.default} 81.93%)`,
|
||||
opacity: "var(--scroll-mask-opacity)",
|
||||
zIndex: 1,
|
||||
transition: "opacity 0.2s",
|
||||
pointerEvents: "none",
|
||||
},
|
||||
}),
|
||||
toolbar: (theme) => ({
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
fontSize: 12,
|
||||
display: "flex",
|
||||
flexAlign: "stretch",
|
||||
}),
|
||||
breadcrumbs: (theme) => ({
|
||||
listStyle: "none",
|
||||
margin: 0,
|
||||
width: "var(--y-axis-width)",
|
||||
padding: "var(--section-padding)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
lineHeight: 1,
|
||||
flexShrink: 0,
|
||||
|
||||
"& li": {
|
||||
display: "block",
|
||||
|
||||
"&[role=presentation]": {
|
||||
lineHeight: 0,
|
||||
},
|
||||
},
|
||||
|
||||
"& li:first-of-type": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
|
||||
"& li[role=presentation]": {
|
||||
color: theme.palette.text.secondary,
|
||||
|
||||
"& svg": {
|
||||
width: 14,
|
||||
height: 14,
|
||||
},
|
||||
},
|
||||
}),
|
||||
breadcrumbButton: (theme) => ({
|
||||
background: "none",
|
||||
padding: 0,
|
||||
border: "none",
|
||||
fontSize: "inherit",
|
||||
color: "inherit",
|
||||
cursor: "pointer",
|
||||
|
||||
"&:hover": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}),
|
||||
legends: {
|
||||
listStyle: "none",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 24,
|
||||
paddingRight: "var(--section-padding)",
|
||||
},
|
||||
legend: {
|
||||
fontWeight: 500,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
lineHeight: 1,
|
||||
},
|
||||
legendSquare: (theme) => ({
|
||||
width: 18,
|
||||
height: 18,
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { type FC, type HTMLProps, useLayoutEffect, useRef } from "react";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { formatTime } from "./utils";
|
||||
|
||||
const XAxisMinWidth = 130;
|
||||
@@ -28,7 +28,15 @@ export const XAxis: FC<XAxisProps> = ({ ticks, scale, ...htmlProps }) => {
|
||||
}, [ticks]);
|
||||
|
||||
return (
|
||||
<div css={styles.root} {...htmlProps} ref={rootRef}>
|
||||
<div
|
||||
{...htmlProps}
|
||||
className={cn(
|
||||
"flex flex-col flex-1 min-h-full relative h-fit",
|
||||
"border-solid border-0 border-l",
|
||||
htmlProps.className,
|
||||
)}
|
||||
ref={rootRef}
|
||||
>
|
||||
<XAxisLabels>
|
||||
{ticks.map((tick) => (
|
||||
<XAxisLabel key={tick}>{formatTime(tick)}</XAxisLabel>
|
||||
@@ -41,33 +49,63 @@ export const XAxis: FC<XAxisProps> = ({ ticks, scale, ...htmlProps }) => {
|
||||
};
|
||||
|
||||
const XAxisLabels: FC<HTMLProps<HTMLUListElement>> = (props) => {
|
||||
return <ul css={styles.labels} {...props} />;
|
||||
return (
|
||||
<ul
|
||||
{...props}
|
||||
className={cn(
|
||||
"sticky top-0 z-[2] bg-surface-primary",
|
||||
"flex items-center flex-shrink-0 list-none m-0",
|
||||
"w-fit p-0 min-w-full",
|
||||
"border-solid border-0 border-b",
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
minHeight: "var(--header-height)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const XAxisLabel: FC<HTMLProps<HTMLLIElement>> = (props) => {
|
||||
return (
|
||||
<li
|
||||
css={[
|
||||
styles.label,
|
||||
{
|
||||
// To centralize the labels between columns, we need to:
|
||||
// 1. Set the label width to twice the column width.
|
||||
// 2. Shift the label to the left by half of the column width.
|
||||
// Note: This adjustment is not applied to the first element,
|
||||
// as the 0 label/value is not displayed in the chart.
|
||||
width: "calc(var(--x-axis-width) * 2)",
|
||||
"&:not(:first-of-type)": {
|
||||
marginLeft: "calc(-1 * var(--x-axis-width))",
|
||||
},
|
||||
},
|
||||
]}
|
||||
{...props}
|
||||
className={cn(
|
||||
"flex justify-center flex-shrink-0 text-content-secondary",
|
||||
// To centralize the labels between columns, we need to:
|
||||
// 1. Set the label width to twice the column width.
|
||||
// 2. Shift the label to the left by half of the column width.
|
||||
// Note: This adjustment is not applied to the first element,
|
||||
// as the 0 label/value is not displayed in the chart.
|
||||
"w-[calc(var(--x-axis-width)_*_2)]",
|
||||
"[&:not(:first-of-type)]:ml-[calc(-1*var(--x-axis-width))]",
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const XAxisSection: FC<HTMLProps<HTMLDivElement>> = (props) => {
|
||||
return <section css={styles.section} {...props} />;
|
||||
return (
|
||||
<section
|
||||
{...props}
|
||||
className={cn(
|
||||
"flex flex-col",
|
||||
// Elevate this section to make it more prominent than the column dashes.
|
||||
"relative z-[1]",
|
||||
"[&:not(:first-of-type)]:pt-[calc(var(--section-padding)_+_var(--header-height))]",
|
||||
"[&:not(:first-of-type)]:border-solid",
|
||||
"[&:not(:first-of-type)]:border-0",
|
||||
"[&:not(:first-of-type)]:border-t",
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
...props.style,
|
||||
gap: "var(--x-axis-rows-gap)",
|
||||
padding: "var(--section-padding)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type XAxisRowProps = HTMLProps<HTMLDivElement> & {
|
||||
@@ -94,8 +132,8 @@ export const XAxisRow: FC<XAxisRowProps> = ({ yAxisLabelId, ...htmlProps }) => {
|
||||
|
||||
return (
|
||||
<section
|
||||
css={styles.row}
|
||||
{...htmlProps}
|
||||
className={cn("flex items-center w-fit gap-2 h-8", htmlProps.className)}
|
||||
aria-labelledby={yAxisLabelId}
|
||||
ref={syncYAxisLabelHeightToXAxisRow}
|
||||
/>
|
||||
@@ -107,12 +145,30 @@ type XGridProps = HTMLProps<HTMLDivElement> & {
|
||||
};
|
||||
|
||||
const XGrid: FC<XGridProps> = ({ columns, ...htmlProps }) => {
|
||||
const borderDefault =
|
||||
typeof document === "undefined"
|
||||
? "hsl(var(--border-default))"
|
||||
: `hsl(${getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--border-default")
|
||||
.trim()})`;
|
||||
|
||||
return (
|
||||
<div css={styles.grid} role="presentation" {...htmlProps}>
|
||||
<div
|
||||
role="presentation"
|
||||
{...htmlProps}
|
||||
className={cn(
|
||||
"flex w-full h-full absolute top-0 left-0",
|
||||
htmlProps.className,
|
||||
)}
|
||||
>
|
||||
{[...Array(columns).keys()].map((key) => (
|
||||
<div
|
||||
key={key}
|
||||
css={[styles.column, { width: "var(--x-axis-width)" }]}
|
||||
className="flex-shrink-0 bg-repeat-y bg-right"
|
||||
style={{
|
||||
width: "var(--x-axis-width)",
|
||||
backgroundImage: `url("${dashedLine(borderDefault)}")`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -121,78 +177,7 @@ const XGrid: FC<XGridProps> = ({ columns, ...htmlProps }) => {
|
||||
|
||||
// A dashed line is used as a background image to create the grid.
|
||||
// Using it as a background simplifies replication along the Y axis.
|
||||
const dashedLine = (
|
||||
color: string,
|
||||
) => `<svg width="2" height="446" viewBox="0 0 2 446" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
const dashedLine = (color: string) =>
|
||||
`data:image/svg+xml,${encodeURIComponent(`<svg width="2" height="446" viewBox="0 0 2 446" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.75 440.932L1.75 446L0.75 446L0.75 440.932L1.75 440.932ZM1.75 420.659L1.75 430.795L0.749999 430.795L0.749999 420.659L1.75 420.659ZM1.75 400.386L1.75 410.523L0.749998 410.523L0.749998 400.386L1.75 400.386ZM1.75 380.114L1.75 390.25L0.749998 390.25L0.749997 380.114L1.75 380.114ZM1.75 359.841L1.75 369.977L0.749997 369.977L0.749996 359.841L1.75 359.841ZM1.75 339.568L1.75 349.705L0.749996 349.705L0.749995 339.568L1.75 339.568ZM1.74999 319.295L1.74999 329.432L0.749995 329.432L0.749994 319.295L1.74999 319.295ZM1.74999 299.023L1.74999 309.159L0.749994 309.159L0.749994 299.023L1.74999 299.023ZM1.74999 278.75L1.74999 288.886L0.749993 288.886L0.749993 278.75L1.74999 278.75ZM1.74999 258.477L1.74999 268.614L0.749992 268.614L0.749992 258.477L1.74999 258.477ZM1.74999 238.204L1.74999 248.341L0.749991 248.341L0.749991 238.204L1.74999 238.204ZM1.74999 217.932L1.74999 228.068L0.74999 228.068L0.74999 217.932L1.74999 217.932ZM1.74999 197.659L1.74999 207.795L0.74999 207.795L0.749989 197.659L1.74999 197.659ZM1.74999 177.386L1.74999 187.523L0.749989 187.523L0.749988 177.386L1.74999 177.386ZM1.74999 157.114L1.74999 167.25L0.749988 167.25L0.749987 157.114L1.74999 157.114ZM1.74999 136.841L1.74999 146.977L0.749987 146.977L0.749986 136.841L1.74999 136.841ZM1.74999 116.568L1.74999 126.705L0.749986 126.705L0.749986 116.568L1.74999 116.568ZM1.74998 96.2955L1.74999 106.432L0.749985 106.432L0.749985 96.2955L1.74998 96.2955ZM1.74998 76.0228L1.74998 86.1591L0.749984 86.1591L0.749984 76.0228L1.74998 76.0228ZM1.74998 55.7501L1.74998 65.8864L0.749983 65.8864L0.749983 55.7501L1.74998 55.7501ZM1.74998 35.4774L1.74998 45.6137L0.749982 45.6137L0.749982 35.4774L1.74998 35.4774ZM1.74998 15.2047L1.74998 25.341L0.749982 25.341L0.749981 15.2047L1.74998 15.2047ZM1.74998 -4.37114e-08L1.74998 5.0683L0.749981 5.0683L0.749981 0L1.74998 -4.37114e-08Z" fill="${color}"/>
|
||||
</svg>`;
|
||||
|
||||
const styles = {
|
||||
root: (theme) => ({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
borderLeft: `1px solid ${theme.palette.divider}`,
|
||||
height: "fit-content",
|
||||
minHeight: "100%",
|
||||
position: "relative",
|
||||
}),
|
||||
labels: (theme) => ({
|
||||
margin: 0,
|
||||
listStyle: "none",
|
||||
display: "flex",
|
||||
width: "fit-content",
|
||||
alignItems: "center",
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
height: "var(--header-height)",
|
||||
padding: 0,
|
||||
minWidth: "100%",
|
||||
flexShrink: 0,
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: 2,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
}),
|
||||
label: (theme) => ({
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
color: theme.palette.text.secondary,
|
||||
}),
|
||||
|
||||
section: (theme) => ({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "var(--x-axis-rows-gap)",
|
||||
padding: "var(--section-padding)",
|
||||
// Elevate this section to make it more prominent than the column dashes.
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
|
||||
"&:not(:first-of-type)": {
|
||||
paddingTop: "calc(var(--section-padding) + var(--header-height))",
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
}),
|
||||
row: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "fit-content",
|
||||
gap: 8,
|
||||
height: 32,
|
||||
},
|
||||
grid: {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
},
|
||||
column: (theme) => ({
|
||||
flexShrink: 0,
|
||||
backgroundRepeat: "repeat-y",
|
||||
backgroundPosition: "right",
|
||||
backgroundImage: `url("data:image/svg+xml,${encodeURIComponent(dashedLine(theme.palette.divider))}");`,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
</svg>`)}`;
|
||||
|
||||
@@ -1,15 +1,50 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import type { FC, HTMLProps } from "react";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
export const YAxis: FC<HTMLProps<HTMLDivElement>> = (props) => {
|
||||
return <div css={styles.root} {...props} />;
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn("flex-shrink-0", props.className)}
|
||||
style={{
|
||||
...props.style,
|
||||
width: "var(--y-axis-width)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const YAxisSection: FC<HTMLProps<HTMLDivElement>> = (props) => {
|
||||
return <section {...props} css={styles.section} />;
|
||||
return (
|
||||
<section
|
||||
{...props}
|
||||
className={cn(
|
||||
"[&:not(:first-of-type)]:border-solid",
|
||||
"[&:not(:first-of-type)]:border-0",
|
||||
"[&:not(:first-of-type)]:border-t",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const YAxisHeader: FC<HTMLProps<HTMLSpanElement>> = (props) => {
|
||||
return <header css={styles.header} {...props} />;
|
||||
return (
|
||||
<header
|
||||
{...props}
|
||||
className={cn(
|
||||
"flex items-center",
|
||||
"sticky top-0 bg-surface-primary",
|
||||
"text-xs font-medium text-content-secondary",
|
||||
"border-solid border-0 border-b",
|
||||
)}
|
||||
style={{
|
||||
height: "var(--header-height)",
|
||||
paddingLeft: "var(--section-padding)",
|
||||
paddingRight: "var(--section-padding)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const YAxisLabels: FC<HTMLProps<HTMLUListElement>> = (props) => {
|
||||
@@ -29,29 +64,6 @@ export const YAxisLabel: FC<YAxisLabelProps> = ({ id, ...props }) => {
|
||||
};
|
||||
|
||||
const styles = {
|
||||
root: {
|
||||
width: "var(--y-axis-width)",
|
||||
flexShrink: 0,
|
||||
},
|
||||
section: (theme) => ({
|
||||
"&:not(:first-of-type)": {
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
}),
|
||||
header: (theme) => ({
|
||||
height: "var(--header-height)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
fontSize: 10,
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.secondary,
|
||||
paddingLeft: "var(--section-padding)",
|
||||
paddingRight: "var(--section-padding)",
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
background: theme.palette.background.default,
|
||||
}),
|
||||
labels: {
|
||||
margin: 0,
|
||||
listStyle: "none",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { CircleAlertIcon, InfoIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import type { TimingStage } from "#/api/typesGenerated";
|
||||
@@ -110,14 +109,11 @@ export const StagesChart: FC<StagesChartProps> = ({
|
||||
key={stage.name}
|
||||
id={encodeURIComponent(stage.name)}
|
||||
>
|
||||
<span css={styles.stageLabel}>
|
||||
<span className="flex items-center justify-end gap-0.5">
|
||||
{stage.label}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon
|
||||
className="size-icon-xs"
|
||||
css={styles.info}
|
||||
/>
|
||||
<InfoIcon className="size-icon-xs cursor-pointer text-content-secondary" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="bottom"
|
||||
@@ -187,11 +183,7 @@ export const StagesChart: FC<StagesChartProps> = ({
|
||||
{validDuration ? (
|
||||
<span>{formatTime(value)}</span>
|
||||
) : (
|
||||
<span
|
||||
css={(theme) => ({
|
||||
color: theme.palette.error.main,
|
||||
})}
|
||||
>
|
||||
<span className="text-content-destructive">
|
||||
Invalid
|
||||
</span>
|
||||
)}
|
||||
@@ -207,24 +199,6 @@ export const StagesChart: FC<StagesChartProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
stageLabel: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
stageDescription: {
|
||||
maxWidth: 300,
|
||||
},
|
||||
info: (theme) => ({
|
||||
width: 12,
|
||||
height: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
cursor: "pointer",
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export const provisioningStages: Stage[] = [
|
||||
{
|
||||
name: "init",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import Collapse from "@mui/material/Collapse";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import sortBy from "lodash/sortBy";
|
||||
@@ -100,21 +99,16 @@ export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div css={styles.collapse}>
|
||||
<div className="rounded-lg border-solid bg-surface-primary">
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
variant="subtle"
|
||||
css={styles.collapseTrigger}
|
||||
className="w-full flex items-center"
|
||||
onClick={() => setIsOpen((o) => !o)}
|
||||
>
|
||||
<ChevronDownIcon open={isOpen} className="size-4 mr-4" />
|
||||
<span>Build timeline</span>
|
||||
<span
|
||||
css={(theme) => ({
|
||||
marginLeft: "auto",
|
||||
color: theme.palette.text.secondary,
|
||||
})}
|
||||
>
|
||||
<span className="ml-auto text-content-secondary">
|
||||
{isLoading ? (
|
||||
<Skeleton variant="text" width={40} height={16} />
|
||||
) : (
|
||||
@@ -124,7 +118,12 @@ export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
|
||||
</Button>
|
||||
{!isLoading && (
|
||||
<Collapse in={isOpen}>
|
||||
<div css={styles.collapseBody}>
|
||||
<div
|
||||
className="border-solid border-0 border-t flex flex-col"
|
||||
style={{
|
||||
height: "var(--collapse-body-height, 420px)",
|
||||
}}
|
||||
>
|
||||
{view.name === "default" && (
|
||||
<StagesChart
|
||||
timings={stages.map((s) => {
|
||||
@@ -175,7 +174,6 @@ export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{view.name === "detailed" && (
|
||||
<>
|
||||
{view.stage.section === "provisioning" && (
|
||||
@@ -235,46 +233,3 @@ const toTimeRange = (timing: {
|
||||
endedAt: new Date(timing.ended_at),
|
||||
};
|
||||
};
|
||||
|
||||
const _humanizeDuration = (durationMs: number): string => {
|
||||
const seconds = Math.floor(durationMs / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours.toLocaleString()}h ${(minutes % 60).toLocaleString()}m`;
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
return `${minutes.toLocaleString()}m ${(seconds % 60).toLocaleString()}s`;
|
||||
}
|
||||
|
||||
return `${seconds.toLocaleString()}s`;
|
||||
};
|
||||
|
||||
const styles = {
|
||||
collapse: (theme) => ({
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
}),
|
||||
collapseTrigger: {
|
||||
background: "none",
|
||||
border: 0,
|
||||
padding: 16,
|
||||
color: "inherit",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: 57,
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
},
|
||||
collapseBody: (theme) => ({
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "var(--collapse-body-height, 420px)",
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -68,7 +68,7 @@ export const ResourceMetadata: FC<ResourceMetadataProps> = ({
|
||||
};
|
||||
|
||||
const styles = {
|
||||
root: (theme) => ({
|
||||
root: () => ({
|
||||
padding: 24,
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
@@ -76,7 +76,6 @@ const styles = {
|
||||
rowGap: 24,
|
||||
marginBottom: 24,
|
||||
fontSize: 14,
|
||||
background: `linear-gradient(180deg, ${theme.palette.background.default} 25%, rgba(0, 0, 0, 0) 100%)`,
|
||||
}),
|
||||
|
||||
item: {
|
||||
|
||||
@@ -157,98 +157,102 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative w-full overflow-y-auto bg-[radial-gradient(circle_at_1px_1px,hsl(var(--surface-invert-secondary))_0,transparent_1px)] bg-[-2px_-2px] bg-[length:16px_16px] p-8">
|
||||
{selectedResource && (
|
||||
<ResourceMetadata
|
||||
resource={selectedResource}
|
||||
className="-mx-8 -mt-8 mb-6"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col gap-6 max-w-[1200px] m-auto">
|
||||
{workspace.latest_build.status === "deleted" && (
|
||||
<WorkspaceDeletedBanner
|
||||
handleClick={() => navigate("/templates")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldShowProvisionerAlert && (
|
||||
<ProvisionerStatusAlert
|
||||
matchingProvisioners={
|
||||
workspace.latest_build.matched_provisioners?.count
|
||||
}
|
||||
availableProvisioners={
|
||||
workspace.latest_build.matched_provisioners?.available ?? 0
|
||||
}
|
||||
tags={workspace.latest_build.job.tags}
|
||||
/>
|
||||
)}
|
||||
|
||||
{workspace.latest_build.job.error && (
|
||||
<Alert severity="error" prominent>
|
||||
<AlertTitle>Workspace build failed</AlertTitle>
|
||||
<AlertDescription>
|
||||
{workspace.latest_build.job.error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!workspace.health.healthy && (
|
||||
<WorkspaceAlert
|
||||
{...getAgentHealthIssue(workspace)}
|
||||
troubleshootingURL={troubleshootingURL}
|
||||
/>
|
||||
)}
|
||||
|
||||
{transitionStats !== undefined && (
|
||||
<WorkspaceBuildProgress
|
||||
workspace={workspace}
|
||||
transitionStats={transitionStats}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldShowBuildLogs && (
|
||||
<WorkspaceBuildLogsSection logs={buildLogs} />
|
||||
)}
|
||||
<div className="relative w-full overflow-y-auto bg-[radial-gradient(circle_at_1px_1px,hsl(var(--content-disabled))_0,transparent_1px)] bg-[-2px_-2px] bg-[length:16px_16px] p-8">
|
||||
<div className="absolute top-0 left-0 right-0 h-32 bg-gradient-to-b from-surface-primary to-transparent"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
{selectedResource && (
|
||||
<section className="flex flex-col gap-6 flex-grow min-w-0">
|
||||
{selectedResource.agents
|
||||
// If an agent has a `parent_id`, that means it is
|
||||
// child of another agent. We do not want these agents
|
||||
// to be displayed at the top-level on this page. We
|
||||
// want them to display _as children_ of their parents.
|
||||
?.filter((agent) => agent.parent_id === null)
|
||||
.map((agent) => (
|
||||
<AgentRow
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
subAgents={selectedResource.agents?.filter(
|
||||
(a) => a.parent_id === agent.id,
|
||||
)}
|
||||
workspace={workspace}
|
||||
template={template}
|
||||
onUpdateAgent={handleUpdate} // On updating the workspace the agent version is also updated
|
||||
/>
|
||||
))}
|
||||
|
||||
{(!selectedResource.agents ||
|
||||
selectedResource.agents?.length === 0) && (
|
||||
<div className="flex justify-center items-center w-full h-full">
|
||||
<div>
|
||||
<h4 className="text-base font-medium">
|
||||
No agents are currently assigned to this resource.
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<ResourceMetadata
|
||||
resource={selectedResource}
|
||||
className="-mx-8 -mt-8 mb-6"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col gap-6 max-w-[1200px] m-auto">
|
||||
{workspace.latest_build.status === "deleted" && (
|
||||
<WorkspaceDeletedBanner
|
||||
handleClick={() => navigate("/templates")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<WorkspaceTimings
|
||||
provisionerTimings={timings?.provisioner_timings}
|
||||
agentScriptTimings={timings?.agent_script_timings}
|
||||
agentConnectionTimings={timings?.agent_connection_timings}
|
||||
/>
|
||||
{shouldShowProvisionerAlert && (
|
||||
<ProvisionerStatusAlert
|
||||
matchingProvisioners={
|
||||
workspace.latest_build.matched_provisioners?.count
|
||||
}
|
||||
availableProvisioners={
|
||||
workspace.latest_build.matched_provisioners?.available ?? 0
|
||||
}
|
||||
tags={workspace.latest_build.job.tags}
|
||||
/>
|
||||
)}
|
||||
|
||||
{workspace.latest_build.job.error && (
|
||||
<Alert severity="error" prominent>
|
||||
<AlertTitle>Workspace build failed</AlertTitle>
|
||||
<AlertDescription>
|
||||
{workspace.latest_build.job.error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!workspace.health.healthy && (
|
||||
<WorkspaceAlert
|
||||
{...getAgentHealthIssue(workspace)}
|
||||
troubleshootingURL={troubleshootingURL}
|
||||
/>
|
||||
)}
|
||||
|
||||
{transitionStats !== undefined && (
|
||||
<WorkspaceBuildProgress
|
||||
workspace={workspace}
|
||||
transitionStats={transitionStats}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldShowBuildLogs && (
|
||||
<WorkspaceBuildLogsSection logs={buildLogs} />
|
||||
)}
|
||||
|
||||
{selectedResource && (
|
||||
<section className="flex flex-col gap-6 flex-grow min-w-0">
|
||||
{selectedResource.agents
|
||||
// If an agent has a `parent_id`, that means it is
|
||||
// child of another agent. We do not want these agents
|
||||
// to be displayed at the top-level on this page. We
|
||||
// want them to display _as children_ of their parents.
|
||||
?.filter((agent) => agent.parent_id === null)
|
||||
.map((agent) => (
|
||||
<AgentRow
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
subAgents={selectedResource.agents?.filter(
|
||||
(a) => a.parent_id === agent.id,
|
||||
)}
|
||||
workspace={workspace}
|
||||
template={template}
|
||||
onUpdateAgent={handleUpdate} // On updating the workspace the agent version is also updated
|
||||
/>
|
||||
))}
|
||||
|
||||
{(!selectedResource.agents ||
|
||||
selectedResource.agents?.length === 0) && (
|
||||
<div className="flex justify-center items-center w-full h-full">
|
||||
<div>
|
||||
<h4 className="text-base font-medium">
|
||||
No agents are currently assigned to this resource.
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<WorkspaceTimings
|
||||
provisionerTimings={timings?.provisioner_timings}
|
||||
agentScriptTimings={timings?.agent_script_timings}
|
||||
agentConnectionTimings={timings?.agent_connection_timings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import type { FC } from "react";
|
||||
import type { ProvisionerJobLog } from "#/api/typesGenerated";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { WorkspaceBuildLogs } from "#/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
interface WorkspaceBuildLogsSectionProps {
|
||||
logs?: ProvisionerJobLog[];
|
||||
@@ -11,28 +11,14 @@ interface WorkspaceBuildLogsSectionProps {
|
||||
export const WorkspaceBuildLogsSection: FC<WorkspaceBuildLogsSectionProps> = ({
|
||||
logs,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
overflow: "hidden",
|
||||
background: theme.palette.background.default,
|
||||
}}
|
||||
>
|
||||
<div className="rounded-lg border-solid overflow-hidden bg-surface-primary">
|
||||
<header
|
||||
css={{
|
||||
background: theme.palette.background.paper,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
padding: "8px 8px 8px 24px",
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderRadius: "8px 8px 0 0",
|
||||
}}
|
||||
className={cn(
|
||||
"bg-surface-secondary border-solid border-0 border-b",
|
||||
"px-2 py-2 pl-6 flex items-center rounded-t-lg",
|
||||
"text-sm",
|
||||
)}
|
||||
>
|
||||
Build logs
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user