Skip to content

Commit d89eceb

Browse files
1 parent cd92220 commit d89eceb

File tree

17 files changed

+2204
-0
lines changed

17 files changed

+2204
-0
lines changed

site/src/api/api.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2179,6 +2179,13 @@ class ApiMethods {
21792179
) => {
21802180
await this.axios.post<void>("/api/v2/users/otp/change-password", req);
21812181
};
2182+
2183+
workspaceBuildTimings = async (workspaceBuildId: string) => {
2184+
const res = await this.axios.get<TypesGen.WorkspaceBuildTimings>(
2185+
`/api/v2/workspacebuilds/${workspaceBuildId}/timings`,
2186+
);
2187+
return res.data;
2188+
};
21822189
}
21832190

21842191
// This is a hard coded CSRF token/cookie pair for local development. In prod,

site/src/api/queries/workspaceBuilds.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,10 @@ export const infiniteWorkspaceBuilds = (
5656
},
5757
};
5858
};
59+
60+
export const workspaceBuildTimings = (workspaceBuildId: string) => {
61+
return {
62+
queryKey: ["workspaceBuilds", workspaceBuildId, "timings"],
63+
queryFn: () => API.workspaceBuildTimings(workspaceBuildId),
64+
};
65+
};
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type { Interpolation, Theme } from "@emotion/react";
2+
import { type ButtonHTMLAttributes, type HTMLProps, forwardRef } from "react";
3+
4+
export type BarColors = {
5+
stroke: string;
6+
fill: string;
7+
};
8+
9+
type BaseBarProps<T> = Omit<T, "size" | "color"> & {
10+
/**
11+
* Scale used to determine the width based on the given value.
12+
*/
13+
scale: number;
14+
value: number;
15+
/**
16+
* The X position of the bar component.
17+
*/
18+
offset: number;
19+
/**
20+
* Color scheme for the bar. If not passed the default gray color will be
21+
* used.
22+
*/
23+
colors?: BarColors;
24+
};
25+
26+
type BarProps = BaseBarProps<HTMLProps<HTMLDivElement>>;
27+
28+
export const Bar = forwardRef<HTMLDivElement, BarProps>(
29+
({ colors, scale, value, offset, ...htmlProps }, ref) => {
30+
return (
31+
<div
32+
css={barCSS({ colors, scale, value, offset })}
33+
{...htmlProps}
34+
ref={ref}
35+
/>
36+
);
37+
},
38+
);
39+
40+
type ClickableBarProps = BaseBarProps<ButtonHTMLAttributes<HTMLButtonElement>>;
41+
42+
export const ClickableBar = forwardRef<HTMLButtonElement, ClickableBarProps>(
43+
({ colors, scale, value, offset, ...htmlProps }, ref) => {
44+
return (
45+
<button
46+
type="button"
47+
css={[...barCSS({ colors, scale, value, offset }), styles.clickable]}
48+
{...htmlProps}
49+
ref={ref}
50+
/>
51+
);
52+
},
53+
);
54+
55+
export const barCSS = ({
56+
scale,
57+
value,
58+
colors,
59+
offset,
60+
}: BaseBarProps<unknown>) => {
61+
return [
62+
styles.bar,
63+
{
64+
width: `calc((var(--x-axis-width) * ${value}) / ${scale})`,
65+
backgroundColor: colors?.fill,
66+
borderColor: colors?.stroke,
67+
marginLeft: `calc((var(--x-axis-width) * ${offset}) / ${scale})`,
68+
},
69+
];
70+
};
71+
72+
const styles = {
73+
bar: (theme) => ({
74+
border: "1px solid",
75+
borderColor: theme.palette.divider,
76+
backgroundColor: theme.palette.background.default,
77+
borderRadius: 8,
78+
// The bar should fill the row height.
79+
height: "inherit",
80+
display: "flex",
81+
padding: 7,
82+
minWidth: 24,
83+
// Increase hover area
84+
position: "relative",
85+
"&::after": {
86+
content: '""',
87+
position: "absolute",
88+
top: -2,
89+
right: -8,
90+
bottom: -2,
91+
left: -8,
92+
},
93+
}),
94+
clickable: {
95+
cursor: "pointer",
96+
// We need to make the bar width at least 34px to allow the "..." icons to be displayed.
97+
// The calculation is border * 1 + side paddings * 2 + icon width (which is 18px)
98+
minWidth: 34,
99+
100+
"&:focus, &:hover, &:active": {
101+
outline: "none",
102+
borderColor: "#38BDF8",
103+
},
104+
},
105+
} satisfies Record<string, Interpolation<Theme>>;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { Interpolation, Theme } from "@emotion/react";
2+
import MoreHorizOutlined from "@mui/icons-material/MoreHorizOutlined";
3+
import { type FC, useEffect, useRef, useState } from "react";
4+
5+
const spaceBetweenBlocks = 4;
6+
const moreIconSize = 18;
7+
const blockSize = 20;
8+
9+
type BlocksProps = {
10+
count: number;
11+
};
12+
13+
export const Blocks: FC<BlocksProps> = ({ count }) => {
14+
const [availableWidth, setAvailableWidth] = useState<number>(0);
15+
const blocksRef = useRef<HTMLDivElement>(null);
16+
17+
// Fix: When using useLayoutEffect, Chromatic fails to calculate the right width.
18+
useEffect(() => {
19+
if (availableWidth || !blocksRef.current) {
20+
return;
21+
}
22+
setAvailableWidth(blocksRef.current.clientWidth);
23+
}, [availableWidth]);
24+
25+
const totalSpaceBetweenBlocks = (count - 1) * spaceBetweenBlocks;
26+
const necessarySize = blockSize * count + totalSpaceBetweenBlocks;
27+
const hasSpacing = necessarySize <= availableWidth;
28+
const nOfPossibleBlocks = Math.floor(
29+
(availableWidth - moreIconSize) / (blockSize + spaceBetweenBlocks),
30+
);
31+
const nOfBlocks = hasSpacing ? count : nOfPossibleBlocks;
32+
33+
return (
34+
<div ref={blocksRef} css={styles.blocks}>
35+
{Array.from({ length: nOfBlocks }, (_, i) => i + 1).map((n) => (
36+
<div key={n} css={styles.block} style={{ minWidth: blockSize }} />
37+
))}
38+
{!hasSpacing && (
39+
<div css={styles.more}>
40+
<MoreHorizOutlined />
41+
</div>
42+
)}
43+
</div>
44+
);
45+
};
46+
47+
const styles = {
48+
blocks: {
49+
display: "flex",
50+
width: "100%",
51+
height: "100%",
52+
gap: spaceBetweenBlocks,
53+
alignItems: "center",
54+
},
55+
block: {
56+
borderRadius: 4,
57+
height: 18,
58+
backgroundColor: "#082F49",
59+
border: "1px solid #38BDF8",
60+
flexShrink: 0,
61+
flex: 1,
62+
},
63+
more: {
64+
color: "#38BDF8",
65+
lineHeight: 0,
66+
flexShrink: 0,
67+
flex: 1,
68+
69+
"& svg": {
70+
fontSize: moreIconSize,
71+
},
72+
},
73+
} satisfies Record<string, Interpolation<Theme>>;

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy