feat: show version messages in version lists (#9708)

This commit is contained in:
Kayla Washburn
2023-09-18 11:17:07 -06:00
committed by GitHub
parent 622442203d
commit f85aa443dd
5 changed files with 193 additions and 108 deletions
@@ -0,0 +1,23 @@
import { InfoTooltip } from "./InfoTooltip";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof InfoTooltip> = {
title: "components/InfoTooltip",
component: InfoTooltip,
args: {
type: "info",
title: "Hello, friend!",
message: "Today is a lovely day :^)",
},
};
export default meta;
type Story = StoryObj<typeof InfoTooltip>;
export const Example: Story = {};
export const Warning: Story = {
args: {
type: "warning",
message: "Unfortunately, there's a radio connected to my brain",
},
};
@@ -0,0 +1,47 @@
import { type FC, type ReactNode } from "react";
import {
HelpTooltip,
HelpTooltipText,
HelpTooltipTitle,
} from "components/HelpTooltip/HelpTooltip";
import InfoIcon from "@mui/icons-material/InfoOutlined";
import { makeStyles } from "@mui/styles";
import { colors } from "theme/colors";
interface InfoTooltipProps {
type?: "warning" | "info";
title: ReactNode;
message: ReactNode;
}
export const InfoTooltip: FC<InfoTooltipProps> = (props) => {
const { title, message, type = "info" } = props;
const styles = useStyles({ type });
return (
<HelpTooltip
size="small"
icon={InfoIcon}
iconClassName={styles.icon}
buttonClassName={styles.button}
>
<HelpTooltipTitle>{title}</HelpTooltipTitle>
<HelpTooltipText>{message}</HelpTooltipText>
</HelpTooltip>
);
};
const useStyles = makeStyles<unknown, Pick<InfoTooltipProps, "type">>(() => ({
icon: ({ type }) => ({
color: type === "info" ? colors.blue[5] : colors.yellow[5],
}),
button: {
opacity: 1,
"&:hover": {
opacity: 1,
},
},
}));
@@ -6,6 +6,7 @@ import { Pill } from "components/Pill/Pill";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { TimelineEntry } from "components/Timeline/TimelineEntry"; import { TimelineEntry } from "components/Timeline/TimelineEntry";
import { UserAvatar } from "components/UserAvatar/UserAvatar"; import { UserAvatar } from "components/UserAvatar/UserAvatar";
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip";
import { useClickableTableRow } from "hooks/useClickableTableRow"; import { useClickableTableRow } from "hooks/useClickableTableRow";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { colors } from "theme/colors"; import { colors } from "theme/colors";
@@ -63,6 +64,10 @@ export const VersionRow: React.FC<VersionRowProps> = ({
version <strong>{version.name}</strong> version <strong>{version.name}</strong>
</span> </span>
{version.message && (
<InfoTooltip title="Message" message={version.message} />
)}
<span className={styles.versionTime}> <span className={styles.versionTime}>
{new Date(version.created_at).toLocaleTimeString()} {new Date(version.created_at).toLocaleTimeString()}
</span> </span>
@@ -2,6 +2,7 @@ import { DialogProps } from "components/Dialogs/Dialog";
import { FC, useRef, useState } from "react"; import { FC, useRef, useState } from "react";
import { FormFields } from "components/Form/Form"; import { FormFields } from "components/Form/Form";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import { makeStyles } from "@mui/styles";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { Template, TemplateVersion } from "api/typesGenerated"; import { Template, TemplateVersion } from "api/typesGenerated";
@@ -13,6 +14,9 @@ import { Pill } from "components/Pill/Pill";
import { Avatar } from "components/Avatar/Avatar"; import { Avatar } from "components/Avatar/Avatar";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { Alert, AlertDetail } from "components/Alert/Alert";
import AlertTitle from "@mui/material/AlertTitle";
import InfoIcon from "@mui/icons-material/InfoOutlined";
export type ChangeVersionDialogProps = DialogProps & { export type ChangeVersionDialogProps = DialogProps & {
template: Template | undefined; template: Template | undefined;
@@ -32,6 +36,9 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({
}) => { }) => {
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false); const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
const selectedTemplateVersion = useRef<TemplateVersion | undefined>(); const selectedTemplateVersion = useRef<TemplateVersion | undefined>();
const version = selectedTemplateVersion.current;
const styles = useStyles();
return ( return (
<ConfirmDialog <ConfirmDialog
@@ -51,74 +58,103 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({
<Stack> <Stack>
<p>You are about to change the version of this workspace.</p> <p>You are about to change the version of this workspace.</p>
{templateVersions ? ( {templateVersions ? (
<FormFields> <>
<Autocomplete <FormFields>
disableClearable <Autocomplete
options={templateVersions} disableClearable
defaultValue={defaultTemplateVersion} options={templateVersions}
id="template-version-autocomplete" defaultValue={defaultTemplateVersion}
open={isAutocompleteOpen} id="template-version-autocomplete"
onChange={(_, newTemplateVersion) => { open={isAutocompleteOpen}
selectedTemplateVersion.current = onChange={(_, newTemplateVersion) => {
newTemplateVersion ?? undefined; selectedTemplateVersion.current =
}} newTemplateVersion ?? undefined;
onOpen={() => { }}
setIsAutocompleteOpen(true); onOpen={() => {
}} setIsAutocompleteOpen(true);
onClose={() => { }}
setIsAutocompleteOpen(false); onClose={() => {
}} setIsAutocompleteOpen(false);
isOptionEqualToValue={( }}
option: TemplateVersion, isOptionEqualToValue={(
value: TemplateVersion, option: TemplateVersion,
) => option.id === value.id} value: TemplateVersion,
getOptionLabel={(option) => option.name} ) => option.id === value.id}
renderOption={(props, option: TemplateVersion) => ( getOptionLabel={(option) => option.name}
<Box component="li" {...props}> renderOption={(props, option: TemplateVersion) => (
<AvatarData <Box component="li" {...props}>
avatar={ <AvatarData
<Avatar src={option.created_by.avatar_url}> avatar={
{option.name} <Avatar src={option.created_by.avatar_url}>
</Avatar> {option.name}
} </Avatar>
title={ }
<Stack title={
direction="row" <Stack
justifyContent="space-between" direction="row"
style={{ width: "100%" }} justifyContent="space-between"
> style={{ width: "100%" }}
{option.name} >
{template?.active_version_id === option.id && ( <Stack
<Pill text="Active" type="success" /> direction="row"
)} alignItems="center"
</Stack> spacing={1}
} >
subtitle={createDayString(option.created_at)} {option.name}
/> {option.message && (
</Box> <InfoIcon
)} sx={(theme) => ({
renderInput={(params) => ( width: theme.spacing(1.5),
<> height: theme.spacing(1.5),
<TextField })}
{...params} />
fullWidth )}
placeholder="Template version name" </Stack>
InputProps={{ {template?.active_version_id === option.id && (
...params.InputProps, <Pill text="Active" type="success" />
endAdornment: ( )}
<> </Stack>
{!templateVersions ? ( }
<CircularProgress size={16} /> subtitle={createDayString(option.created_at)}
) : null} />
{params.InputProps.endAdornment} </Box>
</> )}
), renderInput={(params) => (
}} <>
/> <TextField
</> {...params}
)} fullWidth
/> placeholder="Template version name"
</FormFields> InputProps={{
...params.InputProps,
endAdornment: (
<>
{!templateVersions ? (
<CircularProgress size={16} />
) : null}
{params.InputProps.endAdornment}
</>
),
classes: {
root: styles.inputRoot,
},
}}
/>
</>
)}
/>
</FormFields>
{version && (
<Alert severity="info">
<AlertTitle>
Published by {version.created_by.username}
</AlertTitle>
{version.message && (
<AlertDetail>{version.message}</AlertDetail>
)}
</Alert>
)}
</>
) : ( ) : (
<Loader /> <Loader />
)} )}
@@ -127,3 +163,9 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({
/> />
); );
}; };
export const useStyles = makeStyles((theme) => ({
inputRoot: {
paddingLeft: `${theme.spacing(1.75)} !important`, // Same padding left as input
},
}));
@@ -16,13 +16,6 @@ import Button from "@mui/material/Button";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { Link as RouterLink, useNavigate } from "react-router-dom"; import { Link as RouterLink, useNavigate } from "react-router-dom";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import {
HelpTooltip,
HelpTooltipText,
HelpTooltipTitle,
} from "components/HelpTooltip/HelpTooltip";
import InfoIcon from "@mui/icons-material/InfoOutlined";
import { colors } from "theme/colors";
import { useClickableTableRow } from "hooks/useClickableTableRow"; import { useClickableTableRow } from "hooks/useClickableTableRow";
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
@@ -36,6 +29,7 @@ import { getDisplayWorkspaceTemplateName } from "utils/workspace";
import Checkbox from "@mui/material/Checkbox"; import Checkbox from "@mui/material/Checkbox";
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"; import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton";
import Skeleton from "@mui/material/Skeleton"; import Skeleton from "@mui/material/Skeleton";
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip";
export interface WorkspacesTableProps { export interface WorkspacesTableProps {
workspaces?: Workspace[]; workspaces?: Workspace[];
@@ -215,7 +209,13 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<WorkspaceStatusBadge workspace={workspace} /> <WorkspaceStatusBadge workspace={workspace} />
{workspace.latest_build.status === "running" && {workspace.latest_build.status === "running" &&
!workspace.health.healthy && <UnhealthyTooltip />} !workspace.health.healthy && (
<InfoTooltip
type="warning"
title="Workspace is unhealthy"
message="Your workspace is running but some agents are unhealthy."
/>
)}
</Box> </Box>
</TableCell> </TableCell>
@@ -269,24 +269,6 @@ const WorkspacesRow: FC<{
); );
}; };
export const UnhealthyTooltip = () => {
const styles = useUnhealthyTooltipStyles();
return (
<HelpTooltip
size="small"
icon={InfoIcon}
iconClassName={styles.unhealthyIcon}
buttonClassName={styles.unhealthyButton}
>
<HelpTooltipTitle>Workspace is unhealthy</HelpTooltipTitle>
<HelpTooltipText>
Your workspace is running but some agents are unhealthy.
</HelpTooltipText>
</HelpTooltip>
);
};
const TableLoader = ({ const TableLoader = ({
canCheckWorkspaces, canCheckWorkspaces,
}: { }: {
@@ -324,20 +306,6 @@ const cantBeChecked = (workspace: Workspace) => {
return ["deleting", "pending"].includes(workspace.latest_build.status); return ["deleting", "pending"].includes(workspace.latest_build.status);
}; };
const useUnhealthyTooltipStyles = makeStyles(() => ({
unhealthyIcon: {
color: colors.yellow[5],
},
unhealthyButton: {
opacity: 1,
"&:hover": {
opacity: 1,
},
},
}));
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
withImage: { withImage: {
paddingBottom: 0, paddingBottom: 0,