mirror of
https://github.com/coder/coder.git
synced 2026-10-29 17:44:33 +08:00
feat: show version messages in version lists (#9708)
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user