refactor: remove usage of styled and withStyles (#10806)

This commit is contained in:
Kayla Washburn
2023-11-21 10:43:01 -07:00
committed by GitHub
parent ad3eb4bb75
commit 26b5390f4b
8 changed files with 580 additions and 500 deletions
+30 -32
View File
@@ -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,
}),
};
+30 -29
View File
@@ -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,
}}
>
&quot;
</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,
}}
>
&quot;
</Box>
</Box>
</span>
</span>
);
};