Skip to content

Commit f85aa44

Browse files
authored
feat: show version messages in version lists (coder#9708)
1 parent 6224422 commit f85aa44

File tree

5 files changed

+193
-108
lines changed

5 files changed

+193
-108
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { InfoTooltip } from "./InfoTooltip";
2+
import type { Meta, StoryObj } from "@storybook/react";
3+
4+
const meta: Meta<typeof InfoTooltip> = {
5+
title: "components/InfoTooltip",
6+
component: InfoTooltip,
7+
args: {
8+
type: "info",
9+
title: "Hello, friend!",
10+
message: "Today is a lovely day :^)",
11+
},
12+
};
13+
14+
export default meta;
15+
type Story = StoryObj<typeof InfoTooltip>;
16+
17+
export const Example: Story = {};
18+
export const Warning: Story = {
19+
args: {
20+
type: "warning",
21+
message: "Unfortunately, there's a radio connected to my brain",
22+
},
23+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { type FC, type ReactNode } from "react";
2+
import {
3+
HelpTooltip,
4+
HelpTooltipText,
5+
HelpTooltipTitle,
6+
} from "components/HelpTooltip/HelpTooltip";
7+
import InfoIcon from "@mui/icons-material/InfoOutlined";
8+
import { makeStyles } from "@mui/styles";
9+
import { colors } from "theme/colors";
10+
11+
interface InfoTooltipProps {
12+
type?: "warning" | "info";
13+
title: ReactNode;
14+
message: ReactNode;
15+
}
16+
17+
export const InfoTooltip: FC<InfoTooltipProps> = (props) => {
18+
const { title, message, type = "info" } = props;
19+
20+
const styles = useStyles({ type });
21+
22+
return (
23+
<HelpTooltip
24+
size="small"
25+
icon={InfoIcon}
26+
iconClassName={styles.icon}
27+
buttonClassName={styles.button}
28+
>
29+
<HelpTooltipTitle>{title}</HelpTooltipTitle>
30+
<HelpTooltipText>{message}</HelpTooltipText>
31+
</HelpTooltip>
32+
);
33+
};
34+
35+
const useStyles = makeStyles<unknown, Pick<InfoTooltipProps, "type">>(() => ({
36+
icon: ({ type }) => ({
37+
color: type === "info" ? colors.blue[5] : colors.yellow[5],
38+
}),
39+
40+
button: {
41+
opacity: 1,
42+
43+
"&:hover": {
44+
opacity: 1,
45+
},
46+
},
47+
}));

site/src/pages/TemplatePage/TemplateVersionsPage/VersionRow.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Pill } from "components/Pill/Pill";
66
import { Stack } from "components/Stack/Stack";
77
import { TimelineEntry } from "components/Timeline/TimelineEntry";
88
import { UserAvatar } from "components/UserAvatar/UserAvatar";
9+
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip";
910
import { useClickableTableRow } from "hooks/useClickableTableRow";
1011
import { useNavigate } from "react-router-dom";
1112
import { colors } from "theme/colors";
@@ -63,6 +64,10 @@ export const VersionRow: React.FC<VersionRowProps> = ({
6364
version <strong>{version.name}</strong>
6465
</span>
6566

67+
{version.message && (
68+
<InfoTooltip title="Message" message={version.message} />
69+
)}
70+
6671
<span className={styles.versionTime}>
6772
{new Date(version.created_at).toLocaleTimeString()}
6873
</span>

site/src/pages/WorkspacePage/ChangeVersionDialog.tsx

+110-68
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { DialogProps } from "components/Dialogs/Dialog";
22
import { FC, useRef, useState } from "react";
33
import { FormFields } from "components/Form/Form";
44
import TextField from "@mui/material/TextField";
5+
import { makeStyles } from "@mui/styles";
56
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
67
import { Stack } from "components/Stack/Stack";
78
import { Template, TemplateVersion } from "api/typesGenerated";
@@ -13,6 +14,9 @@ import { Pill } from "components/Pill/Pill";
1314
import { Avatar } from "components/Avatar/Avatar";
1415
import CircularProgress from "@mui/material/CircularProgress";
1516
import Box from "@mui/material/Box";
17+
import { Alert, AlertDetail } from "components/Alert/Alert";
18+
import AlertTitle from "@mui/material/AlertTitle";
19+
import InfoIcon from "@mui/icons-material/InfoOutlined";
1620

1721
export type ChangeVersionDialogProps = DialogProps & {
1822
template: Template | undefined;
@@ -32,6 +36,9 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({
3236
}) => {
3337
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
3438
const selectedTemplateVersion = useRef<TemplateVersion | undefined>();
39+
const version = selectedTemplateVersion.current;
40+
41+
const styles = useStyles();
3542

3643
return (
3744
<ConfirmDialog
@@ -51,74 +58,103 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({
5158
<Stack>
5259
<p>You are about to change the version of this workspace.</p>
5360
{templateVersions ? (
54-
<FormFields>
55-
<Autocomplete
56-
disableClearable
57-
options={templateVersions}
58-
defaultValue={defaultTemplateVersion}
59-
id="template-version-autocomplete"
60-
open={isAutocompleteOpen}
61-
onChange={(_, newTemplateVersion) => {
62-
selectedTemplateVersion.current =
63-
newTemplateVersion ?? undefined;
64-
}}
65-
onOpen={() => {
66-
setIsAutocompleteOpen(true);
67-
}}
68-
onClose={() => {
69-
setIsAutocompleteOpen(false);
70-
}}
71-
isOptionEqualToValue={(
72-
option: TemplateVersion,
73-
value: TemplateVersion,
74-
) => option.id === value.id}
75-
getOptionLabel={(option) => option.name}
76-
renderOption={(props, option: TemplateVersion) => (
77-
<Box component="li" {...props}>
78-
<AvatarData
79-
avatar={
80-
<Avatar src={option.created_by.avatar_url}>
81-
{option.name}
82-
</Avatar>
83-
}
84-
title={
85-
<Stack
86-
direction="row"
87-
justifyContent="space-between"
88-
style={{ width: "100%" }}
89-
>
90-
{option.name}
91-
{template?.active_version_id === option.id && (
92-
<Pill text="Active" type="success" />
93-
)}
94-
</Stack>
95-
}
96-
subtitle={createDayString(option.created_at)}
97-
/>
98-
</Box>
99-
)}
100-
renderInput={(params) => (
101-
<>
102-
<TextField
103-
{...params}
104-
fullWidth
105-
placeholder="Template version name"
106-
InputProps={{
107-
...params.InputProps,
108-
endAdornment: (
109-
<>
110-
{!templateVersions ? (
111-
<CircularProgress size={16} />
112-
) : null}
113-
{params.InputProps.endAdornment}
114-
</>
115-
),
116-
}}
117-
/>
118-
</>
119-
)}
120-
/>
121-
</FormFields>
61+
<>
62+
<FormFields>
63+
<Autocomplete
64+
disableClearable
65+
options={templateVersions}
66+
defaultValue={defaultTemplateVersion}
67+
id="template-version-autocomplete"
68+
open={isAutocompleteOpen}
69+
onChange={(_, newTemplateVersion) => {
70+
selectedTemplateVersion.current =
71+
newTemplateVersion ?? undefined;
72+
}}
73+
onOpen={() => {
74+
setIsAutocompleteOpen(true);
75+
}}
76+
onClose={() => {
77+
setIsAutocompleteOpen(false);
78+
}}
79+
isOptionEqualToValue={(
80+
option: TemplateVersion,
81+
value: TemplateVersion,
82+
) => option.id === value.id}
83+
getOptionLabel={(option) => option.name}
84+
renderOption={(props, option: TemplateVersion) => (
85+
<Box component="li" {...props}>
86+
<AvatarData
87+
avatar={
88+
<Avatar src={option.created_by.avatar_url}>
89+
{option.name}
90+
</Avatar>
91+
}
92+
title={
93+
<Stack
94+
direction="row"
95+
justifyContent="space-between"
96+
style={{ width: "100%" }}
97+
>
98+
<Stack
99+
direction="row"
100+
alignItems="center"
101+
spacing={1}
102+
>
103+
{option.name}
104+
{option.message && (
105+
<InfoIcon
106+
sx={(theme) => ({
107+
width: theme.spacing(1.5),
108+
height: theme.spacing(1.5),
109+
})}
110+
/>
111+
)}
112+
</Stack>
113+
{template?.active_version_id === option.id && (
114+
<Pill text="Active" type="success" />
115+
)}
116+
</Stack>
117+
}
118+
subtitle={createDayString(option.created_at)}
119+
/>
120+
</Box>
121+
)}
122+
renderInput={(params) => (
123+
<>
124+
<TextField
125+
{...params}
126+
fullWidth
127+
placeholder="Template version name"
128+
InputProps={{
129+
...params.InputProps,
130+
endAdornment: (
131+
<>
132+
{!templateVersions ? (
133+
<CircularProgress size={16} />
134+
) : null}
135+
{params.InputProps.endAdornment}
136+
</>
137+
),
138+
classes: {
139+
root: styles.inputRoot,
140+
},
141+
}}
142+
/>
143+
</>
144+
)}
145+
/>
146+
</FormFields>
147+
{version && (
148+
<Alert severity="info">
149+
<AlertTitle>
150+
Published by {version.created_by.username}
151+
</AlertTitle>
152+
{version.message && (
153+
<AlertDetail>{version.message}</AlertDetail>
154+
)}
155+
</Alert>
156+
)}
157+
</>
122158
) : (
123159
<Loader />
124160
)}
@@ -127,3 +163,9 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({
127163
/>
128164
);
129165
};
166+
167+
export const useStyles = makeStyles((theme) => ({
168+
inputRoot: {
169+
paddingLeft: `${theme.spacing(1.75)} !important`, // Same padding left as input
170+
},
171+
}));

site/src/pages/WorkspacesPage/WorkspacesTable.tsx

+8-40
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,6 @@ import Button from "@mui/material/Button";
1616
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
1717
import { Link as RouterLink, useNavigate } from "react-router-dom";
1818
import { makeStyles } from "@mui/styles";
19-
import {
20-
HelpTooltip,
21-
HelpTooltipText,
22-
HelpTooltipTitle,
23-
} from "components/HelpTooltip/HelpTooltip";
24-
import InfoIcon from "@mui/icons-material/InfoOutlined";
25-
import { colors } from "theme/colors";
2619
import { useClickableTableRow } from "hooks/useClickableTableRow";
2720
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
2821
import Box from "@mui/material/Box";
@@ -36,6 +29,7 @@ import { getDisplayWorkspaceTemplateName } from "utils/workspace";
3629
import Checkbox from "@mui/material/Checkbox";
3730
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton";
3831
import Skeleton from "@mui/material/Skeleton";
32+
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip";
3933

4034
export interface WorkspacesTableProps {
4135
workspaces?: Workspace[];
@@ -215,7 +209,13 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
215209
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
216210
<WorkspaceStatusBadge workspace={workspace} />
217211
{workspace.latest_build.status === "running" &&
218-
!workspace.health.healthy && <UnhealthyTooltip />}
212+
!workspace.health.healthy && (
213+
<InfoTooltip
214+
type="warning"
215+
title="Workspace is unhealthy"
216+
message="Your workspace is running but some agents are unhealthy."
217+
/>
218+
)}
219219
</Box>
220220
</TableCell>
221221

@@ -269,24 +269,6 @@ const WorkspacesRow: FC<{
269269
);
270270
};
271271

272-
export const UnhealthyTooltip = () => {
273-
const styles = useUnhealthyTooltipStyles();
274-
275-
return (
276-
<HelpTooltip
277-
size="small"
278-
icon={InfoIcon}
279-
iconClassName={styles.unhealthyIcon}
280-
buttonClassName={styles.unhealthyButton}
281-
>
282-
<HelpTooltipTitle>Workspace is unhealthy</HelpTooltipTitle>
283-
<HelpTooltipText>
284-
Your workspace is running but some agents are unhealthy.
285-
</HelpTooltipText>
286-
</HelpTooltip>
287-
);
288-
};
289-
290272
const TableLoader = ({
291273
canCheckWorkspaces,
292274
}: {
@@ -324,20 +306,6 @@ const cantBeChecked = (workspace: Workspace) => {
324306
return ["deleting", "pending"].includes(workspace.latest_build.status);
325307
};
326308

327-
const useUnhealthyTooltipStyles = makeStyles(() => ({
328-
unhealthyIcon: {
329-
color: colors.yellow[5],
330-
},
331-
332-
unhealthyButton: {
333-
opacity: 1,
334-
335-
"&:hover": {
336-
opacity: 1,
337-
},
338-
},
339-
}));
340-
341309
const useStyles = makeStyles((theme) => ({
342310
withImage: {
343311
paddingBottom: 0,

0 commit comments

Comments
 (0)