Skip to content

Commit e2020b2

Browse files
committed
move components/Resources to modules/resources
1 parent c2b6e20 commit e2020b2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+351
-75
lines changed

site/src/components/TemplateResourcesTable/TemplateResourcesTable.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { AgentRowPreview } from "components/Resources/AgentRowPreview";
2-
import { Resources } from "components/Resources/Resources";
3-
import { FC } from "react";
4-
import { WorkspaceResource } from "api/typesGenerated";
1+
import { type FC } from "react";
2+
import type { WorkspaceResource } from "api/typesGenerated";
3+
import { AgentRowPreview } from "modules/resources/AgentRowPreview";
4+
import { Resources } from "modules/resources/Resources";
55

66
export interface TemplateResourcesProps {
77
resources: WorkspaceResource[];
88
}
99

10-
export const TemplateResourcesTable: FC<
11-
React.PropsWithChildren<TemplateResourcesProps>
12-
> = ({ resources }) => {
10+
export const TemplateResourcesTable: FC<TemplateResourcesProps> = ({
11+
resources,
12+
}) => {
1313
return (
1414
<Resources
1515
resources={resources}

site/src/modules/dashboard/DeploymentBanner/DeploymentBannerView.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
import { DeploymentBannerView } from "./DeploymentBannerView";
77

88
const meta: Meta<typeof DeploymentBannerView> = {
9-
title: "components/DeploymentBannerView",
9+
title: "modules/dashboard/DeploymentBannerView",
1010
component: DeploymentBannerView,
1111
args: {
1212
stats: MockDeploymentStats,

site/src/modules/dashboard/Navbar/NavbarView.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { MockUser, MockUser2 } from "testHelpers/entities";
44
import { NavbarView } from "./NavbarView";
55

66
const meta: Meta<typeof NavbarView> = {
7-
title: "components/NavbarView",
7+
title: "modules/dashboard/NavbarView",
88
parameters: { chromatic: chromaticWithTablet, layout: "fullscreen" },
99
component: NavbarView,
1010
args: {

site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { UserDropdown } from "./UserDropdown";
33
import type { Meta, StoryObj } from "@storybook/react";
44

55
const meta: Meta<typeof UserDropdown> = {
6-
title: "components/UserDropdown",
6+
title: "modules/dashboard/UserDropdown",
77
component: UserDropdown,
88
args: {
99
user: MockUser,

site/src/modules/dashboard/ServiceBanner/ServiceBannerView.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
22
import { ServiceBannerView } from "./ServiceBannerView";
33

44
const meta: Meta<typeof ServiceBannerView> = {
5-
title: "components/ServiceBannerView",
5+
title: "modules/dashboard/ServiceBannerView",
66
component: ServiceBannerView,
77
};
88

site/src/components/Resources/AgentMetadata.stories.tsx renamed to site/src/modules/resources/AgentMetadata.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { AgentMetadataView } from "./AgentMetadata";
66
import type { Meta, StoryObj } from "@storybook/react";
77

88
const meta: Meta<typeof AgentMetadataView> = {
9-
title: "components/AgentMetadataView",
9+
title: "modules/resources/AgentMetadataView",
1010
component: AgentMetadataView,
1111
};
1212

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import dayjs from "dayjs";
2+
import Skeleton from "@mui/material/Skeleton";
3+
import Tooltip from "@mui/material/Tooltip";
4+
import { type Interpolation, type Theme } from "@emotion/react";
5+
import {
6+
createContext,
7+
type FC,
8+
type HTMLAttributes,
9+
useContext,
10+
useEffect,
11+
useLayoutEffect,
12+
useRef,
13+
useState,
14+
} from "react";
15+
import { watchAgentMetadata } from "api/api";
16+
import type {
17+
WorkspaceAgent,
18+
WorkspaceAgentMetadata,
19+
} from "api/typesGenerated";
20+
import { Stack } from "components/Stack/Stack";
21+
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
22+
23+
type ItemStatus = "stale" | "valid" | "loading";
24+
25+
export const WatchAgentMetadataContext = createContext(watchAgentMetadata);
26+
27+
export interface AgentMetadataViewProps {
28+
metadata: WorkspaceAgentMetadata[];
29+
}
30+
31+
export const AgentMetadataView: FC<AgentMetadataViewProps> = ({ metadata }) => {
32+
if (metadata.length === 0) {
33+
return null;
34+
}
35+
return (
36+
<section css={styles.root}>
37+
{metadata.map((m) => (
38+
<MetadataItem key={m.description.key} item={m} />
39+
))}
40+
</section>
41+
);
42+
};
43+
44+
interface AgentMetadataProps {
45+
agent: WorkspaceAgent;
46+
storybookMetadata?: WorkspaceAgentMetadata[];
47+
}
48+
49+
export const AgentMetadata: FC<AgentMetadataProps> = ({
50+
agent,
51+
storybookMetadata,
52+
}) => {
53+
const [metadata, setMetadata] = useState<
54+
WorkspaceAgentMetadata[] | undefined
55+
>(undefined);
56+
const watchAgentMetadata = useContext(WatchAgentMetadataContext);
57+
58+
useEffect(() => {
59+
if (storybookMetadata !== undefined) {
60+
setMetadata(storybookMetadata);
61+
return;
62+
}
63+
64+
let timeout: ReturnType<typeof setTimeout> | undefined = undefined;
65+
66+
const connect = (): (() => void) => {
67+
const source = watchAgentMetadata(agent.id);
68+
69+
source.onerror = (e) => {
70+
console.error("received error in watch stream", e);
71+
setMetadata(undefined);
72+
source.close();
73+
74+
timeout = setTimeout(() => {
75+
connect();
76+
}, 3000);
77+
};
78+
79+
source.addEventListener("data", (e) => {
80+
const data = JSON.parse(e.data);
81+
setMetadata(data);
82+
});
83+
return () => {
84+
if (timeout !== undefined) {
85+
clearTimeout(timeout);
86+
}
87+
source.close();
88+
};
89+
};
90+
return connect();
91+
}, [agent.id, watchAgentMetadata, storybookMetadata]);
92+
93+
if (metadata === undefined) {
94+
return (
95+
<section css={styles.root}>
96+
<AgentMetadataSkeleton />
97+
</section>
98+
);
99+
}
100+
101+
return (
102+
<AgentMetadataView
103+
metadata={[...metadata].sort((a, b) =>
104+
a.description.display_name.localeCompare(b.description.display_name),
105+
)}
106+
/>
107+
);
108+
};
109+
110+
export const AgentMetadataSkeleton: FC = () => {
111+
return (
112+
<Stack alignItems="baseline" direction="row" spacing={6}>
113+
<div css={styles.metadata}>
114+
<Skeleton width={40} height={12} variant="text" />
115+
<Skeleton width={65} height={14} variant="text" />
116+
</div>
117+
118+
<div css={styles.metadata}>
119+
<Skeleton width={40} height={12} variant="text" />
120+
<Skeleton width={65} height={14} variant="text" />
121+
</div>
122+
123+
<div css={styles.metadata}>
124+
<Skeleton width={40} height={12} variant="text" />
125+
<Skeleton width={65} height={14} variant="text" />
126+
</div>
127+
</Stack>
128+
);
129+
};
130+
131+
interface MetadataItemProps {
132+
item: WorkspaceAgentMetadata;
133+
}
134+
135+
const MetadataItem: FC<MetadataItemProps> = ({ item }) => {
136+
const staleThreshold = Math.max(
137+
item.description.interval + item.description.timeout * 2,
138+
// In case there is intense backpressure, we give a little bit of slack.
139+
5,
140+
);
141+
142+
const status: ItemStatus = (() => {
143+
const year = dayjs(item.result.collected_at).year();
144+
if (year <= 1970 || isNaN(year)) {
145+
return "loading";
146+
}
147+
// There is a special circumstance for metadata with `interval: 0`. It is
148+
// expected that they run once and never again, so never display them as
149+
// stale.
150+
if (item.result.age > staleThreshold && item.description.interval > 0) {
151+
return "stale";
152+
}
153+
return "valid";
154+
})();
155+
156+
// Stale data is as good as no data. Plus, we want to build confidence in our
157+
// users that what's shown is real. If times aren't correctly synced this
158+
// could be buggy. But, how common is that anyways?
159+
const value =
160+
status === "loading" ? (
161+
<Skeleton width={65} height={12} variant="text" css={styles.skeleton} />
162+
) : status === "stale" ? (
163+
<Tooltip title="This data is stale and no longer up to date">
164+
<StaticWidth css={[styles.metadataValue, styles.metadataStale]}>
165+
{item.result.value}
166+
</StaticWidth>
167+
</Tooltip>
168+
) : (
169+
<StaticWidth
170+
css={[
171+
styles.metadataValue,
172+
item.result.error.length === 0
173+
? styles.metadataValueSuccess
174+
: styles.metadataValueError,
175+
]}
176+
>
177+
{item.result.value}
178+
</StaticWidth>
179+
);
180+
181+
return (
182+
<div css={styles.metadata}>
183+
<div css={styles.metadataLabel}>{item.description.display_name}</div>
184+
<div>{value}</div>
185+
</div>
186+
);
187+
};
188+
189+
const StaticWidth: FC<HTMLAttributes<HTMLDivElement>> = ({
190+
children,
191+
...attrs
192+
}) => {
193+
const ref = useRef<HTMLDivElement>(null);
194+
195+
useLayoutEffect(() => {
196+
// Ignore this in storybook
197+
if (!ref.current || process.env.STORYBOOK === "true") {
198+
return;
199+
}
200+
201+
const currentWidth = ref.current.getBoundingClientRect().width;
202+
ref.current.style.width = "auto";
203+
const autoWidth = ref.current.getBoundingClientRect().width;
204+
ref.current.style.width =
205+
autoWidth > currentWidth ? `${autoWidth}px` : `${currentWidth}px`;
206+
}, [children]);
207+
208+
return (
209+
<div ref={ref} {...attrs}>
210+
{children}
211+
</div>
212+
);
213+
};
214+
215+
// These are more or less copied from
216+
// site/src/components/Resources/ResourceCard.tsx
217+
const styles = {
218+
root: {
219+
display: "flex",
220+
alignItems: "baseline",
221+
flexWrap: "wrap",
222+
gap: 32,
223+
rowGap: 16,
224+
},
225+
226+
metadata: {
227+
lineHeight: "1.6",
228+
display: "flex",
229+
flexDirection: "column",
230+
overflow: "visible",
231+
flexShrink: 0,
232+
},
233+
234+
metadataLabel: (theme) => ({
235+
color: theme.palette.text.secondary,
236+
textOverflow: "ellipsis",
237+
overflow: "hidden",
238+
whiteSpace: "nowrap",
239+
fontSize: 13,
240+
}),
241+
242+
metadataValue: {
243+
textOverflow: "ellipsis",
244+
overflow: "hidden",
245+
whiteSpace: "nowrap",
246+
maxWidth: "16em",
247+
fontSize: 14,
248+
},
249+
250+
metadataValueSuccess: (theme) => ({
251+
color: theme.experimental.roles.success.fill.outline,
252+
}),
253+
254+
metadataValueError: (theme) => ({
255+
color: theme.palette.error.main,
256+
}),
257+
258+
metadataStale: (theme) => ({
259+
color: theme.palette.text.disabled,
260+
cursor: "pointer",
261+
}),
262+
263+
skeleton: {
264+
marginTop: 4,
265+
},
266+
267+
inlineCommand: (theme) => ({
268+
fontFamily: MONOSPACE_FONT_FAMILY,
269+
display: "inline-block",
270+
fontWeight: 600,
271+
margin: 0,
272+
borderRadius: 4,
273+
color: theme.palette.text.primary,
274+
}),
275+
} satisfies Record<string, Interpolation<Theme>>;

site/src/components/Resources/AgentRow.tsx renamed to site/src/modules/resources/AgentRow.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@ import Collapse from "@mui/material/Collapse";
22
import Skeleton from "@mui/material/Skeleton";
33
import Tooltip from "@mui/material/Tooltip";
44
import { type Interpolation, type Theme } from "@emotion/react";
5+
import {
6+
type FC,
7+
useCallback,
8+
useEffect,
9+
useLayoutEffect,
10+
useMemo,
11+
useRef,
12+
useState,
13+
} from "react";
14+
import AutoSizer from "react-virtualized-auto-sizer";
15+
import { FixedSizeList as List, ListOnScrollProps } from "react-window";
516
import * as API from "api/api";
617
import type {
718
Workspace,
@@ -10,33 +21,22 @@ import type {
1021
WorkspaceAgentMetadata,
1122
} from "api/typesGenerated";
1223
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
13-
import { VSCodeDesktopButton } from "components/Resources/VSCodeDesktopButton/VSCodeDesktopButton";
1424
import {
1525
Line,
1626
LogLine,
1727
logLineHeight,
1828
} from "components/WorkspaceBuildLogs/Logs";
1929
import { useProxy } from "contexts/ProxyContext";
20-
import {
21-
type FC,
22-
useCallback,
23-
useEffect,
24-
useLayoutEffect,
25-
useMemo,
26-
useRef,
27-
useState,
28-
} from "react";
29-
import AutoSizer from "react-virtualized-auto-sizer";
30-
import { FixedSizeList as List, ListOnScrollProps } from "react-window";
31-
import { Stack } from "../Stack/Stack";
30+
import { Stack } from "components/Stack/Stack";
3231
import { AgentLatency } from "./AgentLatency";
3332
import { AgentMetadata } from "./AgentMetadata";
33+
import { AgentStatus } from "./AgentStatus";
3434
import { AgentVersion } from "./AgentVersion";
3535
import { AppLink } from "./AppLink/AppLink";
3636
import { PortForwardButton } from "./PortForwardButton";
3737
import { SSHButton } from "./SSHButton/SSHButton";
3838
import { TerminalLink } from "./TerminalLink/TerminalLink";
39-
import { AgentStatus } from "./AgentStatus";
39+
import { VSCodeDesktopButton } from "./VSCodeDesktopButton/VSCodeDesktopButton";
4040

4141
// Logs are stored as the Line interface to make rendering
4242
// much more efficient. Instead of mapping objects each time, we're

0 commit comments

Comments
 (0)