mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: remove usage of styled and withStyles (#10806)
This commit is contained in:
@@ -1,29 +1,13 @@
|
||||
import Badge from "@mui/material/Badge";
|
||||
import { withStyles } from "@mui/styles";
|
||||
import { type FC } from "react";
|
||||
import { WorkspaceBuild } from "api/typesGenerated";
|
||||
import { getDisplayWorkspaceBuildStatus } from "utils/workspace";
|
||||
import { Avatar, AvatarProps } from "components/Avatar/Avatar";
|
||||
import type { PaletteIndex } from "theme/mui";
|
||||
import { css, cx } from "@emotion/css";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { type FC } from "react";
|
||||
import type { WorkspaceBuild } from "api/typesGenerated";
|
||||
import { getDisplayWorkspaceBuildStatus } from "utils/workspace";
|
||||
import { useClassName } from "hooks/useClassName";
|
||||
import { Avatar, AvatarProps } from "components/Avatar/Avatar";
|
||||
import { BuildIcon } from "components/BuildIcon/BuildIcon";
|
||||
|
||||
interface StylesBadgeProps {
|
||||
type: PaletteIndex;
|
||||
}
|
||||
|
||||
const StyledBadge = withStyles((theme) => ({
|
||||
badge: {
|
||||
backgroundColor: ({ type }: StylesBadgeProps) => theme.palette[type].light,
|
||||
borderRadius: "100%",
|
||||
width: 8,
|
||||
minWidth: 8,
|
||||
height: 8,
|
||||
display: "block",
|
||||
padding: 0,
|
||||
},
|
||||
}))(Badge);
|
||||
|
||||
export interface BuildAvatarProps {
|
||||
build: WorkspaceBuild;
|
||||
size?: AvatarProps["size"];
|
||||
@@ -31,24 +15,38 @@ export interface BuildAvatarProps {
|
||||
|
||||
export const BuildAvatar: FC<BuildAvatarProps> = ({ build, size }) => {
|
||||
const theme = useTheme();
|
||||
const displayBuildStatus = getDisplayWorkspaceBuildStatus(theme, build);
|
||||
const { status, type } = getDisplayWorkspaceBuildStatus(theme, build);
|
||||
const badgeType = useClassName(
|
||||
(css, theme) => css`
|
||||
background-color: ${theme.palette[type].light};
|
||||
`,
|
||||
[type],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledBadge
|
||||
<Badge
|
||||
role="status"
|
||||
type={displayBuildStatus.type}
|
||||
arial-label={displayBuildStatus.status}
|
||||
title={displayBuildStatus.status}
|
||||
aria-label={status}
|
||||
title={status}
|
||||
overlap="circular"
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
badgeContent={<div></div>}
|
||||
classes={{ badge: cx(classNames.badge, badgeType) }}
|
||||
>
|
||||
<Avatar background size={size}>
|
||||
<BuildIcon transition={build.transition} />
|
||||
</Avatar>
|
||||
</StyledBadge>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const classNames = {
|
||||
badge: css({
|
||||
borderRadius: "100%",
|
||||
width: 8,
|
||||
minWidth: 8,
|
||||
height: 8,
|
||||
display: "block",
|
||||
padding: 0,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,45 +1,46 @@
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import Badge from "@mui/material/Badge";
|
||||
import { withStyles } from "@mui/styles";
|
||||
import Group from "@mui/icons-material/Group";
|
||||
import { FC } from "react";
|
||||
import { type FC } from "react";
|
||||
import { type ClassName, useClassName } from "hooks/useClassName";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
|
||||
const StyledBadge = withStyles((theme) => ({
|
||||
badge: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: "100%",
|
||||
width: 24,
|
||||
height: 24,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
"& svg": {
|
||||
width: 14,
|
||||
height: 14,
|
||||
},
|
||||
},
|
||||
}))(Badge);
|
||||
|
||||
export type GroupAvatarProps = {
|
||||
export interface GroupAvatarProps {
|
||||
name: string;
|
||||
avatarURL?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const GroupAvatar: FC<GroupAvatarProps> = ({ name, avatarURL }) => {
|
||||
const badge = useClassName(classNames.badge, []);
|
||||
|
||||
return (
|
||||
<StyledBadge
|
||||
<Badge
|
||||
overlap="circular"
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
badgeContent={<Group />}
|
||||
classes={{ badge }}
|
||||
>
|
||||
<Avatar src={avatarURL} background>
|
||||
{name}
|
||||
</Avatar>
|
||||
</StyledBadge>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const classNames = {
|
||||
badge: (css, theme) =>
|
||||
css({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: "100%",
|
||||
width: 24,
|
||||
height: 24,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
"& svg": {
|
||||
width: 14,
|
||||
height: 14,
|
||||
},
|
||||
}),
|
||||
} satisfies Record<string, ClassName>;
|
||||
|
||||
@@ -1,50 +1,59 @@
|
||||
import { styled } from "@mui/material/styles";
|
||||
import Box from "@mui/material/Box";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { type FC, type PropsWithChildren } from "react";
|
||||
|
||||
export const TableToolbar = styled(Box)(({ theme }) => ({
|
||||
fontSize: 13,
|
||||
marginBottom: "8px",
|
||||
marginTop: 0,
|
||||
height: "36px", // The size of a small button
|
||||
color: theme.palette.text.secondary,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
"& strong": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}));
|
||||
|
||||
type BasePaginationStatusProps = {
|
||||
label: string;
|
||||
isLoading: boolean;
|
||||
showing?: number;
|
||||
total?: number;
|
||||
export const TableToolbar: FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<div
|
||||
css={(theme) => ({
|
||||
fontSize: 13,
|
||||
marginBottom: "8px",
|
||||
marginTop: 0,
|
||||
height: "36px", // The size of a small button
|
||||
color: theme.palette.text.secondary,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
"& strong": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type LoadedPaginationStatusProps = BasePaginationStatusProps & {
|
||||
type PaginationStatusProps =
|
||||
| BasePaginationStatusProps
|
||||
| LoadedPaginationStatusProps;
|
||||
|
||||
type BasePaginationStatusProps = {
|
||||
isLoading: true;
|
||||
};
|
||||
|
||||
type LoadedPaginationStatusProps = {
|
||||
isLoading: false;
|
||||
label: string;
|
||||
showing: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
export const PaginationStatus = ({
|
||||
isLoading,
|
||||
showing,
|
||||
total,
|
||||
label,
|
||||
}: BasePaginationStatusProps | LoadedPaginationStatusProps) => {
|
||||
export const PaginationStatus: FC<PaginationStatusProps> = (props) => {
|
||||
const { isLoading } = props;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box sx={{ height: 24, display: "flex", alignItems: "center" }}>
|
||||
<div css={{ height: 24, display: "flex", alignItems: "center" }}>
|
||||
<Skeleton variant="text" width={160} height={16} />
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { showing, total, label } = props;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<div>
|
||||
Showing <strong>{showing}</strong> of{" "}
|
||||
<strong>{total?.toLocaleString()}</strong> {label}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { StatsItem } from "components/Stats/Stats";
|
||||
import Link from "@mui/material/Link";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import styled from "@emotion/styled";
|
||||
import { Workspace } from "api/typesGenerated";
|
||||
import { displayDormantDeletion } from "./utils";
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||
import { type FC } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import type { Workspace } from "api/typesGenerated";
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||
import { displayDormantDeletion } from "./utils";
|
||||
|
||||
interface DormantDeletionStatProps {
|
||||
workspace: Workspace;
|
||||
@@ -32,7 +31,7 @@ export const DormantDeletionStat: FC<DormantDeletionStatProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledStatsItem
|
||||
<StatsItem
|
||||
label="Deletion on"
|
||||
className="containerClass"
|
||||
value={
|
||||
@@ -45,19 +44,18 @@ export const DormantDeletionStat: FC<DormantDeletionStatProps> = ({
|
||||
{new Date(workspace.deleting_at!).toLocaleString()}
|
||||
</Link>
|
||||
}
|
||||
css={{
|
||||
"&.containerClass": {
|
||||
flexDirection: "column",
|
||||
gap: 0,
|
||||
padding: 0,
|
||||
|
||||
"& > span:first-of-type": {
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledStatsItem = styled(StatsItem)(() => ({
|
||||
"&.containerClass": {
|
||||
flexDirection: "column",
|
||||
gap: 0,
|
||||
padding: 0,
|
||||
|
||||
"& > span:first-of-type": {
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Workspace } from "api/typesGenerated";
|
||||
import { type FC } from "react";
|
||||
import type { Workspace } from "api/typesGenerated";
|
||||
import { displayDormantDeletion } from "./utils";
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||
import styled from "@emotion/styled";
|
||||
import { Theme as MaterialUITheme } from "@mui/material/styles";
|
||||
|
||||
export const DormantDeletionText = ({
|
||||
workspace,
|
||||
}: {
|
||||
interface DormantDeletionTextProps {
|
||||
workspace: Workspace;
|
||||
}): JSX.Element | null => {
|
||||
}
|
||||
|
||||
export const DormantDeletionText: FC<DormantDeletionTextProps> = ({
|
||||
workspace,
|
||||
}) => {
|
||||
const { entitlements, experiments } = useDashboard();
|
||||
const allowAdvancedScheduling =
|
||||
entitlements.features["advanced_template_scheduling"].enabled;
|
||||
@@ -25,10 +26,16 @@ export const DormantDeletionText = ({
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return <StyledSpan role="status">Impending deletion</StyledSpan>;
|
||||
};
|
||||
|
||||
const StyledSpan = styled.span<{ theme?: MaterialUITheme }>`
|
||||
color: ${(props) => props.theme.palette.warning.light};
|
||||
font-weight: 600;
|
||||
`;
|
||||
return (
|
||||
<span
|
||||
role="status"
|
||||
css={(theme) => ({
|
||||
color: theme.palette.warning.light,
|
||||
fontWeight: 600,
|
||||
})}
|
||||
>
|
||||
Impending deletion
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,45 +1,46 @@
|
||||
import type { Workspace } from "api/typesGenerated";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import { type FC, type PropsWithChildren } from "react";
|
||||
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
|
||||
import { DormantDeletionText } from "components/WorkspaceDeletion";
|
||||
import { getDisplayWorkspaceStatus } from "utils/workspace";
|
||||
import Tooltip, {
|
||||
type TooltipProps,
|
||||
tooltipClasses,
|
||||
} from "@mui/material/Tooltip";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import Box from "@mui/material/Box";
|
||||
import ErrorOutline from "@mui/icons-material/ErrorOutline";
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
import { type FC, type ReactNode } from "react";
|
||||
import type { Workspace } from "api/typesGenerated";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
|
||||
import { DormantDeletionText } from "components/WorkspaceDeletion";
|
||||
import { getDisplayWorkspaceStatus } from "utils/workspace";
|
||||
import { useClassName } from "hooks/useClassName";
|
||||
|
||||
export type WorkspaceStatusBadgeProps = {
|
||||
workspace: Workspace;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const WorkspaceStatusBadge: FC<
|
||||
PropsWithChildren<WorkspaceStatusBadgeProps>
|
||||
> = ({ workspace, className }) => {
|
||||
export const WorkspaceStatusBadge: FC<WorkspaceStatusBadgeProps> = ({
|
||||
workspace,
|
||||
className,
|
||||
}) => {
|
||||
const { text, icon, type } = getDisplayWorkspaceStatus(
|
||||
workspace.latest_build.status,
|
||||
workspace.latest_build.job,
|
||||
);
|
||||
|
||||
return (
|
||||
<ChooseOne>
|
||||
<Cond condition={workspace.latest_build.status === "failed"}>
|
||||
<FailureTooltip
|
||||
title={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.25 }}>
|
||||
<div css={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<ErrorOutline
|
||||
sx={{
|
||||
css={(theme) => ({
|
||||
width: 14,
|
||||
height: 14,
|
||||
color: (theme) => theme.palette.error.light,
|
||||
}}
|
||||
color: theme.palette.error.light,
|
||||
})}
|
||||
/>
|
||||
<Box>{workspace.latest_build.job.error}</Box>
|
||||
</Box>
|
||||
<div>{workspace.latest_build.job.error}</div>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
@@ -55,16 +56,16 @@ export const WorkspaceStatusBadge: FC<
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceStatusText: FC<
|
||||
PropsWithChildren<WorkspaceStatusBadgeProps>
|
||||
> = ({ workspace, className }) => {
|
||||
export const WorkspaceStatusText: FC<WorkspaceStatusBadgeProps> = ({
|
||||
workspace,
|
||||
className,
|
||||
}) => {
|
||||
const { text, type } = getDisplayWorkspaceStatus(
|
||||
workspace.latest_build.status,
|
||||
);
|
||||
|
||||
return (
|
||||
<ChooseOne>
|
||||
{/* <DormantDeletionText/> determines its own visibility */}
|
||||
<Cond condition={Boolean(DormantDeletionText({ workspace }))}>
|
||||
<DormantDeletionText workspace={workspace} />
|
||||
</Cond>
|
||||
@@ -73,14 +74,12 @@ export const WorkspaceStatusText: FC<
|
||||
role="status"
|
||||
data-testid="build-status"
|
||||
className={className}
|
||||
css={[
|
||||
styles.root,
|
||||
(theme) => ({
|
||||
color: type
|
||||
? theme.experimental.roles[type].fill
|
||||
: theme.experimental.l1.text,
|
||||
}),
|
||||
]}
|
||||
css={(theme) => ({
|
||||
fontWeight: 600,
|
||||
color: type
|
||||
? theme.experimental.roles[type].fill
|
||||
: theme.experimental.l1.text,
|
||||
})}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
@@ -89,33 +88,22 @@ export const WorkspaceStatusText: FC<
|
||||
);
|
||||
};
|
||||
|
||||
const FailureTooltip = styled(({ className, ...props }: TooltipProps) => (
|
||||
<Tooltip {...props} classes={{ popper: className }} />
|
||||
))(({ theme }) => ({
|
||||
[`& .${tooltipClasses.tooltip}`]: {
|
||||
backgroundColor: theme.palette.background.paperLight,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
fontSize: 12,
|
||||
padding: "8px 10px",
|
||||
},
|
||||
}));
|
||||
const FailureTooltip: FC<TooltipProps> = ({ children, ...tooltipProps }) => {
|
||||
const popper = useClassName(
|
||||
(css, theme) => css`
|
||||
& .${tooltipClasses.tooltip} {
|
||||
background-color: ${theme.palette.background.paperLight};
|
||||
border: 1px solid ${theme.palette.divider};
|
||||
font-size: 12px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
`,
|
||||
[],
|
||||
);
|
||||
|
||||
const styles = {
|
||||
root: { fontWeight: 600 },
|
||||
|
||||
"type-error": (theme) => ({
|
||||
color: theme.palette.error.light,
|
||||
}),
|
||||
"type-warning": (theme) => ({
|
||||
color: theme.palette.warning.light,
|
||||
}),
|
||||
"type-success": (theme) => ({
|
||||
color: theme.palette.success.light,
|
||||
}),
|
||||
"type-info": (theme) => ({
|
||||
color: theme.palette.info.light,
|
||||
}),
|
||||
"type-undefined": (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
return (
|
||||
<Tooltip {...tooltipProps} classes={{ popper }}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { ComponentProps, useRef, useState } from "react";
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
import { type ComponentProps, type FC, useRef, useState } from "react";
|
||||
import "react-date-range/dist/styles.css";
|
||||
import "react-date-range/dist/theme/default.css";
|
||||
import Button from "@mui/material/Button";
|
||||
@@ -37,13 +36,12 @@ type RangesState = NonNullable<
|
||||
ComponentProps<typeof DateRangePicker>["ranges"]
|
||||
>;
|
||||
|
||||
export const DateRange = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
interface DateRangeProps {
|
||||
value: DateRangeValue;
|
||||
onChange: (value: DateRangeValue) => void;
|
||||
}) => {
|
||||
}
|
||||
|
||||
export const DateRange: FC<DateRangeProps> = ({ value, onChange }) => {
|
||||
const selectionStatusRef = useRef<"idle" | "selecting">("idle");
|
||||
const [ranges, setRanges] = useState<RangesState>([
|
||||
{
|
||||
@@ -63,13 +61,15 @@ export const DateRange = ({
|
||||
<PopoverTrigger>
|
||||
<Button>
|
||||
<span>{format(currentRange.startDate, "MMM d, Y")}</span>
|
||||
<ArrowRightAltOutlined sx={{ width: 16, height: 16, mx: 1 }} />
|
||||
<ArrowRightAltOutlined
|
||||
css={{ width: 16, height: 16, marginLeft: 8, marginRight: 8 }}
|
||||
/>
|
||||
<span>{format(currentRange.endDate, "MMM d, Y")}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<DateRangePickerWrapper
|
||||
component={DateRangePicker}
|
||||
<DateRangePicker
|
||||
css={styles.wrapper}
|
||||
onChange={(item) => {
|
||||
const range = item.selection;
|
||||
setRanges([range]);
|
||||
@@ -143,93 +143,95 @@ export const DateRange = ({
|
||||
);
|
||||
};
|
||||
|
||||
const DateRangePickerWrapper: typeof Box = styled(Box)(({ theme }) => ({
|
||||
"& .rdrDefinedRangesWrapper": {
|
||||
background: theme.palette.background.paper,
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
const styles = {
|
||||
wrapper: (theme) => ({
|
||||
"& .rdrDefinedRangesWrapper": {
|
||||
background: theme.palette.background.paper,
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
|
||||
"& .rdrStaticRange": {
|
||||
background: theme.palette.background.paper,
|
||||
border: 0,
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
"& .rdrStaticRange": {
|
||||
background: theme.palette.background.paper,
|
||||
border: 0,
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
|
||||
"&:hover .rdrStaticRangeLabel": {
|
||||
background: theme.palette.background.paperLight,
|
||||
"&:hover .rdrStaticRangeLabel": {
|
||||
background: theme.palette.background.paperLight,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
|
||||
"&.rdrStaticRangeSelected": {
|
||||
color: `${theme.palette.text.primary} !important`,
|
||||
},
|
||||
},
|
||||
|
||||
"& .rdrInputRanges": {
|
||||
display: "none",
|
||||
},
|
||||
|
||||
"& .rdrDateDisplayWrapper": {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
|
||||
"& .rdrCalendarWrapper": {
|
||||
backgroundColor: theme.palette.background.paperLight,
|
||||
},
|
||||
|
||||
"& .rdrDateDisplayItem": {
|
||||
background: "transparent",
|
||||
borderColor: theme.palette.divider,
|
||||
|
||||
"& input": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
|
||||
"&.rdrDateDisplayItemActive": {
|
||||
borderColor: theme.palette.text.primary,
|
||||
backgroundColor: theme.palette.background.paperLight,
|
||||
|
||||
"& input": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"& .rdrMonthPicker select, & .rdrYearPicker select": {
|
||||
color: theme.palette.text.primary,
|
||||
appearance: "auto",
|
||||
background: "transparent",
|
||||
},
|
||||
|
||||
"&.rdrStaticRangeSelected": {
|
||||
color: `${theme.palette.text.primary} !important`,
|
||||
},
|
||||
},
|
||||
|
||||
"& .rdrInputRanges": {
|
||||
display: "none",
|
||||
},
|
||||
|
||||
"& .rdrDateDisplayWrapper": {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
|
||||
"& .rdrCalendarWrapper": {
|
||||
backgroundColor: theme.palette.background.paperLight,
|
||||
},
|
||||
|
||||
"& .rdrDateDisplayItem": {
|
||||
background: "transparent",
|
||||
borderColor: theme.palette.divider,
|
||||
|
||||
"& input": {
|
||||
"& .rdrMonthName, & .rdrWeekDay": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
|
||||
"&.rdrDateDisplayItemActive": {
|
||||
borderColor: theme.palette.text.primary,
|
||||
backgroundColor: theme.palette.background.paperLight,
|
||||
|
||||
"& input": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"& .rdrMonthPicker select, & .rdrYearPicker select": {
|
||||
color: theme.palette.text.primary,
|
||||
appearance: "auto",
|
||||
background: "transparent",
|
||||
},
|
||||
|
||||
"& .rdrMonthName, & .rdrWeekDay": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
|
||||
"& .rdrDayPassive .rdrDayNumber span": {
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
|
||||
"& .rdrDayNumber span": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
|
||||
"& .rdrDayToday .rdrDayNumber span": {
|
||||
fontWeight: 900,
|
||||
|
||||
"&:after": {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
|
||||
"& .rdrInRange, & .rdrEndEdge, & .rdrStartEdge": {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
|
||||
"& .rdrDayDisabled": {
|
||||
backgroundColor: "transparent",
|
||||
|
||||
"& .rdrDayNumber span": {
|
||||
"& .rdrDayPassive .rdrDayNumber span": {
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
"& .rdrDayNumber span": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
|
||||
"& .rdrDayToday .rdrDayNumber span": {
|
||||
fontWeight: 900,
|
||||
|
||||
"&:after": {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
|
||||
"& .rdrInRange, & .rdrEndEdge, & .rdrStartEdge": {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
|
||||
"& .rdrDayDisabled": {
|
||||
backgroundColor: "transparent",
|
||||
|
||||
"& .rdrDayNumber span": {
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
},
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,8 +1,35 @@
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
import Box from "@mui/material/Box";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { BoxProps } from "@mui/system";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Link from "@mui/material/Link";
|
||||
import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined";
|
||||
import CancelOutlined from "@mui/icons-material/CancelOutlined";
|
||||
import LinkOutlined from "@mui/icons-material/LinkOutlined";
|
||||
import { useQuery } from "react-query";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import type {
|
||||
Entitlements,
|
||||
Template,
|
||||
TemplateAppUsage,
|
||||
TemplateInsightsResponse,
|
||||
TemplateParameterUsage,
|
||||
TemplateParameterValue,
|
||||
UserActivityInsightsResponse,
|
||||
UserLatencyInsightsResponse,
|
||||
} from "api/typesGenerated";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import {
|
||||
PropsWithChildren,
|
||||
type FC,
|
||||
type ReactNode,
|
||||
HTMLAttributes,
|
||||
useId,
|
||||
} from "react";
|
||||
import chroma from "chroma-js";
|
||||
import { subDays, addWeeks, format } from "date-fns";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import "react-date-range/dist/styles.css";
|
||||
import "react-date-range/dist/theme/default.css";
|
||||
|
||||
import {
|
||||
ActiveUsersTitle,
|
||||
ActiveUserChart,
|
||||
@@ -15,42 +42,19 @@ import {
|
||||
} from "components/HelpTooltip/HelpTooltip";
|
||||
import { UserAvatar } from "components/UserAvatar/UserAvatar";
|
||||
import { getLatencyColor } from "utils/latency";
|
||||
import chroma from "chroma-js";
|
||||
import { colors } from "theme/colors";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { getTemplatePageTitle } from "../utils";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import type {
|
||||
Entitlements,
|
||||
Template,
|
||||
TemplateAppUsage,
|
||||
TemplateInsightsResponse,
|
||||
TemplateParameterUsage,
|
||||
TemplateParameterValue,
|
||||
UserActivityInsightsResponse,
|
||||
UserLatencyInsightsResponse,
|
||||
} from "api/typesGenerated";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { type ComponentProps, type ReactNode } from "react";
|
||||
import { subDays, addWeeks, format } from "date-fns";
|
||||
import "react-date-range/dist/styles.css";
|
||||
import "react-date-range/dist/theme/default.css";
|
||||
import { DateRange as DailyPicker, DateRangeValue } from "./DateRange";
|
||||
import Link from "@mui/material/Link";
|
||||
import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined";
|
||||
import CancelOutlined from "@mui/icons-material/CancelOutlined";
|
||||
import { lastWeeks } from "./utils";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import LinkOutlined from "@mui/icons-material/LinkOutlined";
|
||||
import { InsightsInterval, IntervalMenu } from "./IntervalMenu";
|
||||
import { WeekPicker, numberOfWeeksOptions } from "./WeekPicker";
|
||||
import {
|
||||
insightsTemplate,
|
||||
insightsUserActivity,
|
||||
insightsUserLatency,
|
||||
} from "api/queries/insights";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { entitlements } from "api/queries/entitlements";
|
||||
import { getTemplatePageTitle } from "../utils";
|
||||
import { DateRange as DailyPicker, DateRangeValue } from "./DateRange";
|
||||
import { lastWeeks } from "./utils";
|
||||
import { InsightsInterval, IntervalMenu } from "./IntervalMenu";
|
||||
import { WeekPicker, numberOfWeeksOptions } from "./WeekPicker";
|
||||
|
||||
const DEFAULT_NUMBER_OF_WEEKS = numberOfWeeksOptions[0];
|
||||
|
||||
@@ -151,24 +155,26 @@ const getDateRange = (
|
||||
return lastWeeks(DEFAULT_NUMBER_OF_WEEKS);
|
||||
};
|
||||
|
||||
export const TemplateInsightsPageView = ({
|
||||
templateInsights,
|
||||
userLatency,
|
||||
userActivity,
|
||||
entitlements,
|
||||
controls,
|
||||
interval,
|
||||
}: {
|
||||
interface TemplateInsightsPageViewProps {
|
||||
templateInsights: TemplateInsightsResponse | undefined;
|
||||
userLatency: UserLatencyInsightsResponse | undefined;
|
||||
userActivity: UserActivityInsightsResponse | undefined;
|
||||
entitlements: Entitlements | undefined;
|
||||
controls: ReactNode;
|
||||
interval: InsightsInterval;
|
||||
}
|
||||
|
||||
export const TemplateInsightsPageView: FC<TemplateInsightsPageViewProps> = ({
|
||||
templateInsights,
|
||||
userLatency,
|
||||
userActivity,
|
||||
entitlements,
|
||||
controls,
|
||||
interval,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
<div
|
||||
css={{
|
||||
marginBottom: 32,
|
||||
display: "flex",
|
||||
@@ -177,8 +183,8 @@ export const TemplateInsightsPageView = ({
|
||||
}}
|
||||
>
|
||||
{controls}
|
||||
</Box>
|
||||
<Box
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
|
||||
@@ -187,7 +193,7 @@ export const TemplateInsightsPageView = ({
|
||||
}}
|
||||
>
|
||||
<ActiveUsersPanel
|
||||
sx={{ gridColumn: "span 2" }}
|
||||
css={{ gridColumn: "span 2" }}
|
||||
interval={interval}
|
||||
userLimit={
|
||||
entitlements?.features.user_limit.enabled
|
||||
@@ -198,28 +204,30 @@ export const TemplateInsightsPageView = ({
|
||||
/>
|
||||
<UsersLatencyPanel data={userLatency} />
|
||||
<TemplateUsagePanel
|
||||
sx={{ gridColumn: "span 2" }}
|
||||
css={{ gridColumn: "span 2" }}
|
||||
data={templateInsights?.report?.apps_usage}
|
||||
/>
|
||||
<UsersActivityPanel data={userActivity} />
|
||||
<TemplateParametersUsagePanel
|
||||
sx={{ gridColumn: "span 3" }}
|
||||
css={{ gridColumn: "span 3" }}
|
||||
data={templateInsights?.report?.parameters_usage}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ActiveUsersPanel = ({
|
||||
interface ActiveUsersPanelProps extends PanelProps {
|
||||
data: TemplateInsightsResponse["interval_reports"] | undefined;
|
||||
interval: InsightsInterval;
|
||||
userLimit: number | undefined;
|
||||
}
|
||||
|
||||
const ActiveUsersPanel: FC<ActiveUsersPanelProps> = ({
|
||||
data,
|
||||
interval,
|
||||
userLimit,
|
||||
...panelProps
|
||||
}: PanelProps & {
|
||||
data: TemplateInsightsResponse["interval_reports"] | undefined;
|
||||
interval: InsightsInterval;
|
||||
userLimit: number | undefined;
|
||||
}) => {
|
||||
return (
|
||||
<Panel {...panelProps}>
|
||||
@@ -229,7 +237,7 @@ const ActiveUsersPanel = ({
|
||||
</PanelTitle>
|
||||
</PanelHeader>
|
||||
<PanelContent>
|
||||
{!data && <Loader sx={{ height: "100%" }} />}
|
||||
{!data && <Loader css={{ height: "100%" }} />}
|
||||
{data && data.length === 0 && <NoDataAvailable />}
|
||||
{data && data.length > 0 && (
|
||||
<ActiveUserChart
|
||||
@@ -246,17 +254,21 @@ const ActiveUsersPanel = ({
|
||||
);
|
||||
};
|
||||
|
||||
const UsersLatencyPanel = ({
|
||||
interface UsersLatencyPanelProps extends PanelProps {
|
||||
data: UserLatencyInsightsResponse | undefined;
|
||||
}
|
||||
|
||||
const UsersLatencyPanel: FC<UsersLatencyPanelProps> = ({
|
||||
data,
|
||||
...panelProps
|
||||
}: PanelProps & { data: UserLatencyInsightsResponse | undefined }) => {
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const users = data?.report.users;
|
||||
|
||||
return (
|
||||
<Panel {...panelProps} sx={{ overflowY: "auto", ...panelProps.sx }}>
|
||||
<Panel {...panelProps} css={{ overflowY: "auto" }}>
|
||||
<PanelHeader>
|
||||
<PanelTitle sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<PanelTitle css={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
Latency by user
|
||||
<HelpTooltip size="small">
|
||||
<HelpTooltipTitle>How is latency calculated?</HelpTooltipTitle>
|
||||
@@ -267,31 +279,32 @@ const UsersLatencyPanel = ({
|
||||
</PanelTitle>
|
||||
</PanelHeader>
|
||||
<PanelContent>
|
||||
{!data && <Loader sx={{ height: "100%" }} />}
|
||||
{!data && <Loader css={{ height: "100%" }} />}
|
||||
{users && users.length === 0 && <NoDataAvailable />}
|
||||
{users &&
|
||||
users
|
||||
.sort((a, b) => b.latency_ms.p50 - a.latency_ms.p50)
|
||||
.map((row) => (
|
||||
<Box
|
||||
<div
|
||||
key={row.user_id}
|
||||
sx={{
|
||||
css={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
fontSize: 14,
|
||||
py: 1,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
|
||||
<div css={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<UserAvatar
|
||||
username={row.username}
|
||||
avatarURL={row.avatar_url}
|
||||
/>
|
||||
<Box sx={{ fontWeight: 500 }}>{row.username}</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
<div css={{ fontWeight: 500 }}>{row.username}</div>
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
color: getLatencyColor(theme, row.latency_ms.p50),
|
||||
fontWeight: 500,
|
||||
fontSize: 13,
|
||||
@@ -299,24 +312,30 @@ const UsersLatencyPanel = ({
|
||||
}}
|
||||
>
|
||||
{row.latency_ms.p50.toFixed(0)}ms
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</PanelContent>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
const UsersActivityPanel = ({
|
||||
interface UsersActivityPanelProps extends PanelProps {
|
||||
data: UserActivityInsightsResponse | undefined;
|
||||
}
|
||||
|
||||
const UsersActivityPanel: FC<UsersActivityPanelProps> = ({
|
||||
data,
|
||||
...panelProps
|
||||
}: PanelProps & { data: UserActivityInsightsResponse | undefined }) => {
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const users = data?.report.users;
|
||||
|
||||
return (
|
||||
<Panel {...panelProps} sx={{ overflowY: "auto", ...panelProps.sx }}>
|
||||
<Panel {...panelProps} css={{ overflowY: "auto" }}>
|
||||
<PanelHeader>
|
||||
<PanelTitle sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<PanelTitle css={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
Activity by user
|
||||
<HelpTooltip size="small">
|
||||
<HelpTooltipTitle>How is activity calculated?</HelpTooltipTitle>
|
||||
@@ -328,51 +347,55 @@ const UsersActivityPanel = ({
|
||||
</PanelTitle>
|
||||
</PanelHeader>
|
||||
<PanelContent>
|
||||
{!data && <Loader sx={{ height: "100%" }} />}
|
||||
{!data && <Loader css={{ height: "100%" }} />}
|
||||
{users && users.length === 0 && <NoDataAvailable />}
|
||||
{users &&
|
||||
users
|
||||
.sort((a, b) => b.seconds - a.seconds)
|
||||
.map((row) => (
|
||||
<Box
|
||||
<div
|
||||
key={row.user_id}
|
||||
sx={{
|
||||
css={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
fontSize: 14,
|
||||
py: 1,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
|
||||
<div css={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<UserAvatar
|
||||
username={row.username}
|
||||
avatarURL={row.avatar_url}
|
||||
/>
|
||||
<Box sx={{ fontWeight: 500 }}>{row.username}</Box>
|
||||
</Box>
|
||||
<Box
|
||||
css={(theme) => ({
|
||||
<div css={{ fontWeight: 500 }}>{row.username}</div>
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 13,
|
||||
textAlign: "right",
|
||||
})}
|
||||
}}
|
||||
>
|
||||
{formatTime(row.seconds)}
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</PanelContent>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
const TemplateUsagePanel = ({
|
||||
interface TemplateUsagePanelProps extends PanelProps {
|
||||
data: TemplateAppUsage[] | undefined;
|
||||
}
|
||||
|
||||
const TemplateUsagePanel: FC<TemplateUsagePanelProps> = ({
|
||||
data,
|
||||
...panelProps
|
||||
}: PanelProps & {
|
||||
data: TemplateAppUsage[] | undefined;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const validUsage = data?.filter((u) => u.seconds > 0);
|
||||
const totalInSeconds =
|
||||
validUsage?.reduce((total, usage) => total + usage.seconds, 0) ?? 1;
|
||||
@@ -382,20 +405,21 @@ const TemplateUsagePanel = ({
|
||||
.colors(validUsage?.length ?? 0);
|
||||
// The API returns a row for each app, even if the user didn't use it.
|
||||
const hasDataAvailable = validUsage && validUsage.length > 0;
|
||||
|
||||
return (
|
||||
<Panel {...panelProps} css={{ overflowY: "auto" }}>
|
||||
<PanelHeader>
|
||||
<PanelTitle>App & IDE Usage</PanelTitle>
|
||||
</PanelHeader>
|
||||
<PanelContent>
|
||||
{!data && <Loader sx={{ height: "100%" }} />}
|
||||
{!data && <Loader css={{ height: "100%" }} />}
|
||||
{data && !hasDataAvailable && <NoDataAvailable />}
|
||||
{data && hasDataAvailable && (
|
||||
<Box
|
||||
sx={{
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 3,
|
||||
gap: 24,
|
||||
}}
|
||||
>
|
||||
{validUsage
|
||||
@@ -403,13 +427,15 @@ const TemplateUsagePanel = ({
|
||||
.map((usage, i) => {
|
||||
const percentage = (usage.seconds / totalInSeconds) * 100;
|
||||
return (
|
||||
<Box
|
||||
<div
|
||||
key={usage.slug}
|
||||
sx={{ display: "flex", gap: 2, alignItems: "center" }}
|
||||
css={{ display: "flex", gap: 16, alignItems: "center" }}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
<div
|
||||
css={{ display: "flex", alignItems: "center", gap: 8 }}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
display: "flex",
|
||||
@@ -426,58 +452,62 @@ const TemplateUsagePanel = ({
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ fontSize: 13, fontWeight: 500, width: 200 }}>
|
||||
</div>
|
||||
<div css={{ fontSize: 13, fontWeight: 500, width: 200 }}>
|
||||
{usage.display_name}
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
<LinearProgress
|
||||
value={percentage}
|
||||
variant="determinate"
|
||||
sx={{
|
||||
css={{
|
||||
width: "100%",
|
||||
height: 8,
|
||||
backgroundColor: (theme) => theme.palette.divider,
|
||||
backgroundColor: theme.palette.divider,
|
||||
"& .MuiLinearProgress-bar": {
|
||||
backgroundColor: usageColors[i],
|
||||
borderRadius: 999,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
<div
|
||||
css={{
|
||||
fontSize: 13,
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
color: theme.palette.text.secondary,
|
||||
width: 120,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{formatTime(usage.seconds)}
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</div>
|
||||
)}
|
||||
</PanelContent>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
const TemplateParametersUsagePanel = ({
|
||||
interface TemplateParametersUsagePanelProps extends PanelProps {
|
||||
data: TemplateParameterUsage[] | undefined;
|
||||
}
|
||||
|
||||
const TemplateParametersUsagePanel: FC<TemplateParametersUsagePanelProps> = ({
|
||||
data,
|
||||
...panelProps
|
||||
}: PanelProps & {
|
||||
data: TemplateParameterUsage[] | undefined;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Panel {...panelProps}>
|
||||
<PanelHeader>
|
||||
<PanelTitle>Parameters usage</PanelTitle>
|
||||
</PanelHeader>
|
||||
<PanelContent>
|
||||
{!data && <Loader sx={{ height: 200 }} />}
|
||||
{data && data.length === 0 && <NoDataAvailable sx={{ height: 200 }} />}
|
||||
{!data && <Loader css={{ height: 200 }} />}
|
||||
{data && data.length === 0 && <NoDataAvailable css={{ height: 200 }} />}
|
||||
{data &&
|
||||
data.length > 0 &&
|
||||
data.map((parameter, parameterIndex) => {
|
||||
@@ -486,49 +516,50 @@ const TemplateParametersUsagePanel = ({
|
||||
? parameter.display_name
|
||||
: parameter.name;
|
||||
return (
|
||||
<Box
|
||||
<div
|
||||
key={parameter.name}
|
||||
sx={{
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "start",
|
||||
p: 3,
|
||||
marginX: -3,
|
||||
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
padding: 24,
|
||||
marginLeft: -24,
|
||||
marginRight: -24,
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
width: "calc(100% + 48px)",
|
||||
"&:first-child": {
|
||||
borderTop: 0,
|
||||
},
|
||||
gap: 24,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box sx={{ fontWeight: 500 }}>{label}</Box>
|
||||
<Box
|
||||
component="p"
|
||||
sx={{
|
||||
<div css={{ flex: 1 }}>
|
||||
<div css={{ fontWeight: 500 }}>{label}</div>
|
||||
<p
|
||||
css={{
|
||||
fontSize: 14,
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
color: theme.palette.text.secondary,
|
||||
maxWidth: 400,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{parameter.description}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, fontSize: 14 }}>
|
||||
</p>
|
||||
</div>
|
||||
<div css={{ flex: 1, fontSize: 14, flexGrow: 2 }}>
|
||||
<ParameterUsageRow
|
||||
sx={{
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
fontWeight: 500,
|
||||
fontSize: 13,
|
||||
cursor: "default",
|
||||
}}
|
||||
>
|
||||
<Box>Value</Box>
|
||||
<div>Value</div>
|
||||
<Tooltip
|
||||
title="The number of workspaces using this value"
|
||||
placement="top"
|
||||
>
|
||||
<Box>Count</Box>
|
||||
<div>Count</div>
|
||||
</Tooltip>
|
||||
</ParameterUsageRow>
|
||||
{parameter.values
|
||||
@@ -542,11 +573,11 @@ const TemplateParametersUsagePanel = ({
|
||||
usage={usage}
|
||||
parameter={parameter}
|
||||
/>
|
||||
<Box sx={{ textAlign: "right" }}>{usage.count}</Box>
|
||||
<div css={{ textAlign: "right" }}>{usage.count}</div>
|
||||
</ParameterUsageRow>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</PanelContent>
|
||||
@@ -564,49 +595,66 @@ const filterOrphanValues = (
|
||||
return true;
|
||||
};
|
||||
|
||||
const ParameterUsageRow = styled(Box)(() => ({
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
padding: "4px 0",
|
||||
gap: 40,
|
||||
}));
|
||||
const ParameterUsageRow: FC<HTMLAttributes<HTMLDivElement>> = ({
|
||||
children,
|
||||
...attrs
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
padding: "4px 0",
|
||||
}}
|
||||
{...attrs}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ParameterUsageLabel = ({
|
||||
usage,
|
||||
parameter,
|
||||
}: {
|
||||
interface ParameterUsageLabelProps {
|
||||
usage: TemplateParameterValue;
|
||||
parameter: TemplateParameterUsage;
|
||||
}
|
||||
|
||||
const ParameterUsageLabel: FC<ParameterUsageLabelProps> = ({
|
||||
usage,
|
||||
parameter,
|
||||
}) => {
|
||||
const ariaId = useId();
|
||||
const theme = useTheme();
|
||||
|
||||
if (parameter.options) {
|
||||
const option = parameter.options.find((o) => o.value === usage.value)!;
|
||||
const icon = option.icon;
|
||||
const label = option.name;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{icon && (
|
||||
<Box sx={{ width: 16, height: 16, lineHeight: 1 }}>
|
||||
<Box
|
||||
component="img"
|
||||
<div css={{ width: 16, height: 16, lineHeight: 1 }}>
|
||||
<img
|
||||
alt=""
|
||||
src={icon}
|
||||
sx={{
|
||||
css={{
|
||||
objectFit: "contain",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
aria-labelledby={ariaId}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
)}
|
||||
{label}
|
||||
</Box>
|
||||
<span id={ariaId}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -616,19 +664,19 @@ const ParameterUsageLabel = ({
|
||||
href={usage.value}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
sx={{
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
color: (theme) => theme.palette.text.primary,
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
<TextValue>{usage.value}</TextValue>
|
||||
<LinkOutlined
|
||||
sx={{
|
||||
css={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
color: (theme) => theme.palette.primary.light,
|
||||
color: theme.palette.primary.light,
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
@@ -638,42 +686,40 @@ const ParameterUsageLabel = ({
|
||||
if (parameter.type === "list(string)") {
|
||||
const values = JSON.parse(usage.value) as string[];
|
||||
return (
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
{values.map((v, i) => {
|
||||
return (
|
||||
<Box
|
||||
key={i}
|
||||
sx={{
|
||||
p: "2px 12px",
|
||||
borderRadius: 999,
|
||||
background: (theme) => theme.palette.divider,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{v}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<div css={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
{values.map((v, i) => (
|
||||
<div
|
||||
key={i}
|
||||
css={{
|
||||
padding: "2px 12px",
|
||||
borderRadius: 999,
|
||||
background: theme.palette.divider,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{v}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (parameter.type === "bool") {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{usage.value === "false" ? (
|
||||
<>
|
||||
<CancelOutlined
|
||||
sx={{
|
||||
css={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
color: (theme) => theme.palette.error.light,
|
||||
color: theme.palette.error.light,
|
||||
}}
|
||||
/>
|
||||
False
|
||||
@@ -681,91 +727,122 @@ const ParameterUsageLabel = ({
|
||||
) : (
|
||||
<>
|
||||
<CheckCircleOutlined
|
||||
sx={{
|
||||
css={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
color: (theme) => theme.palette.success.light,
|
||||
color: theme.palette.success.light,
|
||||
}}
|
||||
/>
|
||||
True
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <TextValue>{usage.value}</TextValue>;
|
||||
};
|
||||
|
||||
const Panel = styled(Box)(({ theme }) => ({
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}));
|
||||
interface PanelProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
type PanelProps = ComponentProps<typeof Panel>;
|
||||
const Panel: FC<PanelProps> = ({ children, ...attrs }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const PanelHeader = styled(Box)(() => ({
|
||||
padding: "20px 24px 24px",
|
||||
}));
|
||||
|
||||
const PanelTitle = styled(Box)(() => ({
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
}));
|
||||
|
||||
const PanelContent = styled(Box)(() => ({
|
||||
padding: "0 24px 24px",
|
||||
flex: 1,
|
||||
}));
|
||||
|
||||
const NoDataAvailable = (props: BoxProps) => {
|
||||
return (
|
||||
<Box
|
||||
<div
|
||||
css={{
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
{...attrs}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PanelHeader: FC<HTMLAttributes<HTMLDivElement>> = ({
|
||||
children,
|
||||
...attrs
|
||||
}) => {
|
||||
return (
|
||||
<div css={{ padding: "20px 24px 24px" }} {...attrs}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PanelTitle: FC<HTMLAttributes<HTMLDivElement>> = ({
|
||||
children,
|
||||
...attrs
|
||||
}) => {
|
||||
return (
|
||||
<div css={{ fontSize: 14, fontWeight: 500 }} {...attrs}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PanelContent: FC<HTMLAttributes<HTMLDivElement>> = ({
|
||||
children,
|
||||
...attrs
|
||||
}) => {
|
||||
return (
|
||||
<div css={{ padding: "0 24px 24px", flex: 1 }} {...attrs}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NoDataAvailable = (props: HTMLAttributes<HTMLDivElement>) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
sx={{
|
||||
css={{
|
||||
fontSize: 13,
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
color: theme.palette.text.secondary,
|
||||
textAlign: "center",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
No data available
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TextValue = ({ children }: { children: ReactNode }) => {
|
||||
const TextValue: FC<PropsWithChildren> = ({ children }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box component="span">
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
<span>
|
||||
<span
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
weight: 600,
|
||||
mr: 0.25,
|
||||
marginRight: 2,
|
||||
}}
|
||||
>
|
||||
"
|
||||
</Box>
|
||||
</span>
|
||||
{children}
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
<span
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
weight: 600,
|
||||
ml: 0.25,
|
||||
marginLeft: 2,
|
||||
}}
|
||||
>
|
||||
"
|
||||
</Box>
|
||||
</Box>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user