From 59a92640c040cfa227fa8cb105003aea73b4536a Mon Sep 17 00:00:00 2001
From: McKayla Washburn
Date: Mon, 20 Nov 2023 21:31:26 +0000
Subject: [PATCH 1/4] refactor: remove usage of `styled` and `withStyles`
---
.../components/BuildAvatar/BuildAvatar.tsx | 38 +-
.../components/GroupAvatar/GroupAvatar.tsx | 49 +-
.../components/TableToolbar/TableToolbar.tsx | 67 ++-
.../WorkspaceDeletion/DormantDeletionStat.tsx | 34 +-
.../WorkspaceDeletion/DormantDeletionText.tsx | 33 +-
.../WorkspaceStatusBadge.tsx | 106 ++--
site/src/hooks/useClassName.ts | 21 +
.../TemplateInsightsPage/DateRange.tsx | 156 +++---
.../TemplateInsightsPage.tsx | 519 ++++++++++--------
9 files changed, 557 insertions(+), 466 deletions(-)
create mode 100644 site/src/hooks/useClassName.ts
diff --git a/site/src/components/BuildAvatar/BuildAvatar.tsx b/site/src/components/BuildAvatar/BuildAvatar.tsx
index e0edfdc634179..a762a0d70c8cc 100644
--- a/site/src/components/BuildAvatar/BuildAvatar.tsx
+++ b/site/src/components/BuildAvatar/BuildAvatar.tsx
@@ -1,29 +1,11 @@
import Badge from "@mui/material/Badge";
-import { withStyles } from "@mui/styles";
+import { useTheme } from "@emotion/react";
import { type FC } from "react";
-import { WorkspaceBuild } from "api/typesGenerated";
+import type { WorkspaceBuild } from "api/typesGenerated";
import { getDisplayWorkspaceBuildStatus } from "utils/workspace";
import { Avatar, AvatarProps } from "components/Avatar/Avatar";
-import type { PaletteIndex } from "theme/mui";
-import { useTheme } from "@emotion/react";
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"];
@@ -34,10 +16,9 @@ export const BuildAvatar: FC = ({ build, size }) => {
const displayBuildStatus = getDisplayWorkspaceBuildStatus(theme, build);
return (
- = ({ build, size }) => {
horizontal: "right",
}}
badgeContent={}
+ css={{
+ backgroundColor: theme.palette[displayBuildStatus.type].light,
+ borderRadius: "100%",
+ width: 8,
+ minWidth: 8,
+ height: 8,
+ display: "block",
+ padding: 0,
+ }}
>
-
+
);
};
diff --git a/site/src/components/GroupAvatar/GroupAvatar.tsx b/site/src/components/GroupAvatar/GroupAvatar.tsx
index caf6c7c23f709..1b4c56f0d0230 100644
--- a/site/src/components/GroupAvatar/GroupAvatar.tsx
+++ b/site/src/components/GroupAvatar/GroupAvatar.tsx
@@ -1,45 +1,44 @@
-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";
-
-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);
+import { useTheme } from "@emotion/react";
+import { type FC } from "react";
+import { Avatar } from "components/Avatar/Avatar";
-export type GroupAvatarProps = {
+export interface GroupAvatarProps {
name: string;
avatarURL?: string;
-};
+}
export const GroupAvatar: FC = ({ name, avatarURL }) => {
+ const theme = useTheme();
+
return (
- }
+ 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,
+ },
+ }}
>
{name}
-
+
);
};
diff --git a/site/src/components/TableToolbar/TableToolbar.tsx b/site/src/components/TableToolbar/TableToolbar.tsx
index 080350ddc2234..bee49cac87fd7 100644
--- a/site/src/components/TableToolbar/TableToolbar.tsx
+++ b/site/src/components/TableToolbar/TableToolbar.tsx
@@ -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,
- },
-}));
+export const TableToolbar: FC = ({ children }) => {
+ return (
+ ({
+ 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}
+
+ );
+};
+
+type PaginationStatusProps =
+ | BasePaginationStatusProps
+ | LoadedPaginationStatusProps;
type BasePaginationStatusProps = {
- label: string;
- isLoading: boolean;
- showing?: number;
- total?: number;
+ isLoading: true;
};
-type LoadedPaginationStatusProps = BasePaginationStatusProps & {
+type LoadedPaginationStatusProps = {
isLoading: false;
+ label: string;
showing: number;
total: number;
};
-export const PaginationStatus = ({
- isLoading,
- showing,
- total,
- label,
-}: BasePaginationStatusProps | LoadedPaginationStatusProps) => {
+export const PaginationStatus: FC = (props) => {
+ const { isLoading } = props;
+
if (isLoading) {
return (
-
+
-
+
);
}
+
+ const { showing, total, label } = props;
+
return (
-
+
Showing {showing} of{" "}
{total?.toLocaleString()} {label}
-
+
);
};
diff --git a/site/src/components/WorkspaceDeletion/DormantDeletionStat.tsx b/site/src/components/WorkspaceDeletion/DormantDeletionStat.tsx
index 8dc7a289d21f9..7d881f8ca67a3 100644
--- a/site/src/components/WorkspaceDeletion/DormantDeletionStat.tsx
+++ b/site/src/components/WorkspaceDeletion/DormantDeletionStat.tsx
@@ -1,11 +1,10 @@
import { StatsItem } from "components/Stats/Stats";
import Link from "@mui/material/Link";
+import { type FC } from "react";
import { Link as RouterLink } from "react-router-dom";
-import styled from "@emotion/styled";
-import { Workspace } from "api/typesGenerated";
-import { displayDormantDeletion } from "./utils";
+import type { Workspace } from "api/typesGenerated";
import { useDashboard } from "components/Dashboard/DashboardProvider";
-import { type FC } from "react";
+import { displayDormantDeletion } from "./utils";
interface DormantDeletionStatProps {
workspace: Workspace;
@@ -32,7 +31,7 @@ export const DormantDeletionStat: FC = ({
}
return (
- = ({
{new Date(workspace.deleting_at!).toLocaleString()}
}
+ 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,
- },
- },
-}));
diff --git a/site/src/components/WorkspaceDeletion/DormantDeletionText.tsx b/site/src/components/WorkspaceDeletion/DormantDeletionText.tsx
index 0d1b8692674fa..a6747ca9ce458 100644
--- a/site/src/components/WorkspaceDeletion/DormantDeletionText.tsx
+++ b/site/src/components/WorkspaceDeletion/DormantDeletionText.tsx
@@ -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 = ({
+ workspace,
+}) => {
const { entitlements, experiments } = useDashboard();
const allowAdvancedScheduling =
entitlements.features["advanced_template_scheduling"].enabled;
@@ -25,10 +26,16 @@ export const DormantDeletionText = ({
) {
return null;
}
- return Impending deletion;
-};
-const StyledSpan = styled.span<{ theme?: MaterialUITheme }>`
- color: ${(props) => props.theme.palette.warning.light};
- font-weight: 600;
-`;
+ return (
+ ({
+ color: theme.palette.warning.light,
+ fontWeight: 600,
+ })}
+ >
+ Impending deletion
+
+ );
+};
diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx
index fd71afb16c465..d707694d6ad8b 100644
--- a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx
+++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx
@@ -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
-> = ({ workspace, className }) => {
+export const WorkspaceStatusBadge: FC = ({
+ workspace,
+ className,
+}) => {
const { text, icon, type } = getDisplayWorkspaceStatus(
workspace.latest_build.status,
workspace.latest_build.job,
);
+
return (
+
({
width: 14,
height: 14,
- color: (theme) => theme.palette.error.light,
- }}
+ color: theme.palette.error.light,
+ })}
/>
- {workspace.latest_build.job.error}
-
+ {workspace.latest_build.job.error}
+
}
placement="top"
>
@@ -55,16 +56,16 @@ export const WorkspaceStatusBadge: FC<
);
};
-export const WorkspaceStatusText: FC<
- PropsWithChildren
-> = ({ workspace, className }) => {
+export const WorkspaceStatusText: FC = ({
+ workspace,
+ className,
+}) => {
const { text, type } = getDisplayWorkspaceStatus(
workspace.latest_build.status,
);
return (
- {/* determines its own visibility */}
@@ -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}
@@ -89,33 +88,22 @@ export const WorkspaceStatusText: FC<
);
};
-const FailureTooltip = styled(({ className, ...props }: TooltipProps) => (
-
-))(({ theme }) => ({
- [`& .${tooltipClasses.tooltip}`]: {
- backgroundColor: theme.palette.background.paperLight,
- border: `1px solid ${theme.palette.divider}`,
- fontSize: 12,
- padding: "8px 10px",
- },
-}));
-
-const styles = {
- root: { fontWeight: 600 },
+const FailureTooltip: FC = ({ 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;
+ }
+ `,
+ [],
+ );
- "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>;
+ return (
+
+ {children}
+
+ );
+};
diff --git a/site/src/hooks/useClassName.ts b/site/src/hooks/useClassName.ts
new file mode 100644
index 0000000000000..595308dead9ca
--- /dev/null
+++ b/site/src/hooks/useClassName.ts
@@ -0,0 +1,21 @@
+/* eslint-disable react-hooks/exhaustive-deps -- false positives */
+
+import { css } from "@emotion/css";
+import { type DependencyList, useMemo } from "react";
+import { type Theme, useTheme } from "@emotion/react";
+
+export type ClassName = (cssFn: typeof css, theme: Theme) => string;
+
+/**
+ * An escape hatch for when you really need to manually pass around a
+ * `className`. Prefer using the `css` prop whenever possible. If you
+ * can't use that, then this might be helpful for you.
+ */
+export function useClassName(styles: ClassName, deps: DependencyList): string {
+ const theme = useTheme();
+ const className = useMemo(() => {
+ return styles(css, theme);
+ }, [...deps, theme]);
+
+ return className;
+}
diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx
index aa6d43bc9a298..6ec3543283d94 100644
--- a/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx
+++ b/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx
@@ -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["ranges"]
>;
-export const DateRange = ({
- value,
- onChange,
-}: {
+interface DateRangeProps {
value: DateRangeValue;
onChange: (value: DateRangeValue) => void;
-}) => {
+}
+
+export const DateRange: FC = ({ value, onChange }) => {
const selectionStatusRef = useRef<"idle" | "selecting">("idle");
const [ranges, setRanges] = useState([
{
@@ -63,13 +61,15 @@ export const DateRange = ({
- {
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,
- },
-
- "& .rdrStaticRange": {
- background: theme.palette.background.paper,
- border: 0,
- fontSize: 14,
- color: theme.palette.text.secondary,
-
- "&:hover .rdrStaticRangeLabel": {
- background: theme.palette.background.paperLight,
- color: theme.palette.text.primary,
+const styles = {
+ wrapper: (theme) => ({
+ "& .rdrDefinedRangesWrapper": {
+ background: theme.palette.background.paper,
+ borderColor: theme.palette.divider,
},
- "&.rdrStaticRangeSelected": {
- color: `${theme.palette.text.primary} !important`,
- },
- },
-
- "& .rdrInputRanges": {
- display: "none",
- },
+ "& .rdrStaticRange": {
+ background: theme.palette.background.paper,
+ border: 0,
+ fontSize: 14,
+ color: theme.palette.text.secondary,
- "& .rdrDateDisplayWrapper": {
- backgroundColor: theme.palette.background.paper,
- },
+ "&:hover .rdrStaticRangeLabel": {
+ background: theme.palette.background.paperLight,
+ color: theme.palette.text.primary,
+ },
- "& .rdrCalendarWrapper": {
- backgroundColor: theme.palette.background.paperLight,
- },
+ "&.rdrStaticRangeSelected": {
+ color: `${theme.palette.text.primary} !important`,
+ },
+ },
- "& .rdrDateDisplayItem": {
- background: "transparent",
- borderColor: theme.palette.divider,
+ "& .rdrInputRanges": {
+ display: "none",
+ },
- "& input": {
- color: theme.palette.text.secondary,
+ "& .rdrDateDisplayWrapper": {
+ backgroundColor: theme.palette.background.paper,
},
- "&.rdrDateDisplayItemActive": {
- borderColor: theme.palette.text.primary,
+ "& .rdrCalendarWrapper": {
backgroundColor: theme.palette.background.paperLight,
+ },
+
+ "& .rdrDateDisplayItem": {
+ background: "transparent",
+ borderColor: theme.palette.divider,
"& input": {
- color: theme.palette.text.primary,
+ 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",
- },
+ "& .rdrMonthPicker select, & .rdrYearPicker select": {
+ color: theme.palette.text.primary,
+ appearance: "auto",
+ background: "transparent",
+ },
- "& .rdrMonthName, & .rdrWeekDay": {
- color: theme.palette.text.secondary,
- },
+ "& .rdrMonthName, & .rdrWeekDay": {
+ color: theme.palette.text.secondary,
+ },
- "& .rdrDayPassive .rdrDayNumber span": {
- color: theme.palette.text.disabled,
- },
+ "& .rdrDayPassive .rdrDayNumber span": {
+ color: theme.palette.text.disabled,
+ },
- "& .rdrDayNumber span": {
- color: theme.palette.text.primary,
- },
+ "& .rdrDayNumber span": {
+ color: theme.palette.text.primary,
+ },
- "& .rdrDayToday .rdrDayNumber span": {
- fontWeight: 900,
+ "& .rdrDayToday .rdrDayNumber span": {
+ fontWeight: 900,
- "&:after": {
- display: "none",
+ "&:after": {
+ display: "none",
+ },
},
- },
- "& .rdrInRange, & .rdrEndEdge, & .rdrStartEdge": {
- color: theme.palette.primary.main,
- },
+ "& .rdrInRange, & .rdrEndEdge, & .rdrStartEdge": {
+ color: theme.palette.primary.main,
+ },
- "& .rdrDayDisabled": {
- backgroundColor: "transparent",
+ "& .rdrDayDisabled": {
+ backgroundColor: "transparent",
- "& .rdrDayNumber span": {
- color: theme.palette.text.disabled,
+ "& .rdrDayNumber span": {
+ color: theme.palette.text.disabled,
+ },
},
- },
-}));
+ }),
+} satisfies Record>;
diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx
index f5f6c4ca38e32..71a75222feeda 100644
--- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx
+++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx
@@ -1,25 +1,11 @@
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 {
- ActiveUsersTitle,
- ActiveUserChart,
-} from "components/ActiveUserChart/ActiveUserChart";
-import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
-import {
- HelpTooltip,
- HelpTooltipTitle,
- HelpTooltipText,
-} 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,
@@ -31,26 +17,44 @@ import type {
UserLatencyInsightsResponse,
} from "api/typesGenerated";
import { useTheme } from "@emotion/react";
-import { type ComponentProps, type ReactNode } from "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 { 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 {
+ ActiveUsersTitle,
+ ActiveUserChart,
+} from "components/ActiveUserChart/ActiveUserChart";
+import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
+import {
+ HelpTooltip,
+ HelpTooltipTitle,
+ HelpTooltipText,
+} from "components/HelpTooltip/HelpTooltip";
+import { UserAvatar } from "components/UserAvatar/UserAvatar";
+import { getLatencyColor } from "utils/latency";
+import { colors } from "theme/colors";
+import { Loader } from "components/Loader/Loader";
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 = ({
+ templateInsights,
+ userLatency,
+ userActivity,
+ entitlements,
+ controls,
+ interval,
}) => {
return (
<>
-
{controls}
-
-
+
>
);
};
-const ActiveUsersPanel = ({
+interface ActiveUsersPanelProps extends PanelProps {
+ data: TemplateInsightsResponse["interval_reports"] | undefined;
+ interval: InsightsInterval;
+ userLimit: number | undefined;
+}
+
+const ActiveUsersPanel: FC = ({
data,
interval,
userLimit,
...panelProps
-}: PanelProps & {
- data: TemplateInsightsResponse["interval_reports"] | undefined;
- interval: InsightsInterval;
- userLimit: number | undefined;
}) => {
return (
@@ -229,7 +237,7 @@ const ActiveUsersPanel = ({
- {!data && }
+ {!data && }
{data && data.length === 0 && }
{data && data.length > 0 && (
= ({
data,
...panelProps
-}: PanelProps & { data: UserLatencyInsightsResponse | undefined }) => {
+}) => {
const theme = useTheme();
const users = data?.report.users;
return (
-
+
-
+
Latency by user
How is latency calculated?
@@ -267,31 +279,32 @@ const UsersLatencyPanel = ({
- {!data && }
+ {!data && }
{users && users.length === 0 && }
{users &&
users
.sort((a, b) => b.latency_ms.p50 - a.latency_ms.p50)
.map((row) => (
-
-
+
- {row.username}
-
- {row.username}
+
+
{row.latency_ms.p50.toFixed(0)}ms
-
-
+
+
))}
);
};
-const UsersActivityPanel = ({
+interface UsersActivityPanelProps extends PanelProps {
+ data: UserActivityInsightsResponse | undefined;
+}
+
+const UsersActivityPanel: FC = ({
data,
...panelProps
-}: PanelProps & { data: UserActivityInsightsResponse | undefined }) => {
+}) => {
+ const theme = useTheme();
+
const users = data?.report.users;
return (
-
+
-
+
Activity by user
How is activity calculated?
@@ -328,51 +347,55 @@ const UsersActivityPanel = ({
- {!data && }
+ {!data && }
{users && users.length === 0 && }
{users &&
users
.sort((a, b) => b.seconds - a.seconds)
.map((row) => (
-
-
+
-
{row.username}
-
-
({
+ {row.username}
+
+
{formatTime(row.seconds)}
-
-
+
+
))}
);
};
-const TemplateUsagePanel = ({
+interface TemplateUsagePanelProps extends PanelProps {
+ data: TemplateAppUsage[] | undefined;
+}
+
+const TemplateUsagePanel: FC = ({
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 (
App & IDE Usage
- {!data && }
+ {!data && }
{data && !hasDataAvailable && }
{data && hasDataAvailable && (
-
{validUsage
@@ -403,13 +427,15 @@ const TemplateUsagePanel = ({
.map((usage, i) => {
const percentage = (usage.seconds / totalInSeconds) * 100;
return (
-
-
-
+
-
-
+
+
{usage.display_name}
-
-
+
+
theme.palette.divider,
+ backgroundColor: theme.palette.divider,
"& .MuiLinearProgress-bar": {
backgroundColor: usageColors[i],
borderRadius: 999,
},
}}
/>
- theme.palette.text.secondary,
+ color: theme.palette.text.secondary,
width: 120,
flexShrink: 0,
}}
>
{formatTime(usage.seconds)}
-
-
+
+
);
})}
-
+
)}
);
};
-const TemplateParametersUsagePanel = ({
+interface TemplateParametersUsagePanelProps extends PanelProps {
+ data: TemplateParameterUsage[] | undefined;
+}
+
+const TemplateParametersUsagePanel: FC = ({
data,
...panelProps
-}: PanelProps & {
- data: TemplateParameterUsage[] | undefined;
}) => {
+ const theme = useTheme();
+
return (
Parameters usage
- {!data && }
- {data && data.length === 0 && }
+ {!data && }
+ {data && data.length === 0 && }
{data &&
data.length > 0 &&
data.map((parameter, parameterIndex) => {
@@ -486,49 +516,49 @@ const TemplateParametersUsagePanel = ({
? parameter.display_name
: parameter.name;
return (
- `1px solid ${theme.palette.divider}`,
+ padding: 24,
+ marginLeft: -3,
+ marginRight: -3,
+ borderTop: `1px solid ${theme.palette.divider}`,
width: "calc(100% + 48px)",
"&:first-child": {
borderTop: 0,
},
}}
>
-
- {label}
-
+ {label}
+ theme.palette.text.secondary,
+ color: theme.palette.text.secondary,
maxWidth: 400,
margin: 0,
}}
>
{parameter.description}
-
-
-
+
+
+
theme.palette.text.secondary,
+ css={{
+ color: theme.palette.text.secondary,
fontWeight: 500,
fontSize: 13,
cursor: "default",
}}
>
- Value
+ Value
- Count
+ Count
{parameter.values
@@ -542,11 +572,11 @@ const TemplateParametersUsagePanel = ({
usage={usage}
parameter={parameter}
/>
-
{usage.count}
+
{usage.count}
))}
-
-
+
+
);
})}
@@ -564,49 +594,67 @@ const filterOrphanValues = (
return true;
};
-const ParameterUsageRow = styled(Box)(() => ({
- display: "flex",
- alignItems: "baseline",
- justifyContent: "space-between",
- padding: "4px 0",
- gap: 40,
-}));
+const ParameterUsageRow: FC> = ({
+ children,
+ ...attrs
+}) => {
+ return (
+
+ {children}
+
+ );
+};
-const ParameterUsageLabel = ({
- usage,
- parameter,
-}: {
+interface ParameterUsageLabelProps {
usage: TemplateParameterValue;
parameter: TemplateParameterUsage;
+}
+
+const ParameterUsageLabel: FC = ({
+ 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 (
-
{icon && (
-
-
+
-
+
)}
- {label}
-
+ {label}
+
);
}
@@ -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,
}}
>
{usage.value}
theme.palette.primary.light,
+ color: theme.palette.primary.light,
}}
/>
@@ -638,30 +686,28 @@ const ParameterUsageLabel = ({
if (parameter.type === "list(string)") {
const values = JSON.parse(usage.value) as string[];
return (
-
- {values.map((v, i) => {
- return (
- theme.palette.divider,
- whiteSpace: "nowrap",
- }}
- >
- {v}
-
- );
- })}
-
+
+ {values.map((v, i) => (
+
+ {v}
+
+ ))}
+
);
}
if (parameter.type === "bool") {
return (
-
theme.palette.error.light,
+ color: theme.palette.error.light,
}}
/>
False
@@ -681,91 +727,122 @@ const ParameterUsageLabel = ({
) : (
<>
theme.palette.success.light,
+ color: theme.palette.success.light,
}}
/>
True
>
)}
-
+
);
}
return {usage.value};
};
-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 {}
+
+const Panel: FC = ({ children, ...attrs }) => {
+ const theme = useTheme();
-type PanelProps = ComponentProps;
+ return (
+
+ {children}
+
+ );
+};
+
+const PanelHeader: FC> = ({
+ children,
+ ...attrs
+}) => {
+ return (
+
+ {children}
+
+ );
+};
-const PanelHeader = styled(Box)(() => ({
- padding: "20px 24px 24px",
-}));
+const PanelTitle: FC> = ({
+ children,
+ ...attrs
+}) => {
+ return (
+
+ {children}
+
+ );
+};
-const PanelTitle = styled(Box)(() => ({
- fontSize: 14,
- fontWeight: 500,
-}));
+const PanelContent: FC> = ({
+ children,
+ ...attrs
+}) => {
+ return (
+
+ {children}
+
+ );
+};
-const PanelContent = styled(Box)(() => ({
- padding: "0 24px 24px",
- flex: 1,
-}));
+const NoDataAvailable = (props: HTMLAttributes) => {
+ const theme = useTheme();
-const NoDataAvailable = (props: BoxProps) => {
return (
- theme.palette.text.secondary,
+ color: theme.palette.text.secondary,
textAlign: "center",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
- ...props.sx,
}}
>
No data available
-
+
);
};
-const TextValue = ({ children }: { children: ReactNode }) => {
+const TextValue: FC = ({ children }) => {
+ const theme = useTheme();
+
return (
-
- theme.palette.text.secondary,
+
+
"
-
+
{children}
- theme.palette.text.secondary,
+
"
-
-
+
+
);
};
From bae0da9cc55e5b51f75014187ab2e346311c3f6e Mon Sep 17 00:00:00 2001
From: McKayla Washburn
Date: Tue, 21 Nov 2023 17:01:31 +0000
Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=A7=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../components/BuildAvatar/BuildAvatar.tsx | 35 ++++++++++++-------
1 file changed, 23 insertions(+), 12 deletions(-)
diff --git a/site/src/components/BuildAvatar/BuildAvatar.tsx b/site/src/components/BuildAvatar/BuildAvatar.tsx
index a762a0d70c8cc..3f4c349b87461 100644
--- a/site/src/components/BuildAvatar/BuildAvatar.tsx
+++ b/site/src/components/BuildAvatar/BuildAvatar.tsx
@@ -1,8 +1,10 @@
import Badge from "@mui/material/Badge";
+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";
@@ -13,28 +15,26 @@ export interface BuildAvatarProps {
export const BuildAvatar: FC = ({ 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 (
}
- css={{
- backgroundColor: theme.palette[displayBuildStatus.type].light,
- borderRadius: "100%",
- width: 8,
- minWidth: 8,
- height: 8,
- display: "block",
- padding: 0,
- }}
+ classes={{ badge: cx(classNames.badge, badgeType) }}
>
@@ -42,3 +42,14 @@ export const BuildAvatar: FC = ({ build, size }) => {
);
};
+
+const classNames = {
+ badge: css({
+ borderRadius: "100%",
+ width: 8,
+ minWidth: 8,
+ height: 8,
+ display: "block",
+ padding: 0,
+ }),
+};
From 1114bd2c1cd27a7c0cb3c1f321f7e8575d4911e0 Mon Sep 17 00:00:00 2001
From: McKayla Washburn
Date: Tue, 21 Nov 2023 17:04:50 +0000
Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=A7=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../components/BuildAvatar/BuildAvatar.tsx | 5 +--
.../components/GroupAvatar/GroupAvatar.tsx | 44 ++++++++++---------
2 files changed, 24 insertions(+), 25 deletions(-)
diff --git a/site/src/components/BuildAvatar/BuildAvatar.tsx b/site/src/components/BuildAvatar/BuildAvatar.tsx
index 3f4c349b87461..dcd0351840b7e 100644
--- a/site/src/components/BuildAvatar/BuildAvatar.tsx
+++ b/site/src/components/BuildAvatar/BuildAvatar.tsx
@@ -29,10 +29,7 @@ export const BuildAvatar: FC = ({ build, size }) => {
aria-label={status}
title={status}
overlap="circular"
- anchorOrigin={{
- vertical: "bottom",
- horizontal: "right",
- }}
+ anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
badgeContent={}
classes={{ badge: cx(classNames.badge, badgeType) }}
>
diff --git a/site/src/components/GroupAvatar/GroupAvatar.tsx b/site/src/components/GroupAvatar/GroupAvatar.tsx
index 1b4c56f0d0230..811e9b7ad93e8 100644
--- a/site/src/components/GroupAvatar/GroupAvatar.tsx
+++ b/site/src/components/GroupAvatar/GroupAvatar.tsx
@@ -1,7 +1,7 @@
import Badge from "@mui/material/Badge";
import Group from "@mui/icons-material/Group";
-import { useTheme } from "@emotion/react";
import { type FC } from "react";
+import { type ClassName, useClassName } from "hooks/useClassName";
import { Avatar } from "components/Avatar/Avatar";
export interface GroupAvatarProps {
@@ -10,31 +10,14 @@ export interface GroupAvatarProps {
}
export const GroupAvatar: FC = ({ name, avatarURL }) => {
- const theme = useTheme();
+ const badge = useClassName(classNames.badge, []);
return (
}
- 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,
- },
- }}
+ classes={{ badge }}
>
{name}
@@ -42,3 +25,22 @@ export const GroupAvatar: FC = ({ name, avatarURL }) => {
);
};
+
+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;
From 75604f386bd21ca840f63758f4c42ca775c929e8 Mon Sep 17 00:00:00 2001
From: McKayla Washburn
Date: Tue, 21 Nov 2023 17:33:35 +0000
Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A7=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../TemplateInsightsPage/TemplateInsightsPage.tsx | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx
index 71a75222feeda..4e07b1abaf076 100644
--- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx
+++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx
@@ -522,13 +522,14 @@ const TemplateParametersUsagePanel: FC = ({
display: "flex",
alignItems: "start",
padding: 24,
- marginLeft: -3,
- marginRight: -3,
+ marginLeft: -24,
+ marginRight: -24,
borderTop: `1px solid ${theme.palette.divider}`,
width: "calc(100% + 48px)",
"&:first-child": {
borderTop: 0,
},
+ gap: 24,
}}
>
@@ -544,7 +545,7 @@ const TemplateParametersUsagePanel: FC = ({
{parameter.description}
-
+
> = ({
alignItems: "baseline",
justifyContent: "space-between",
padding: "4px 0",
- gap: 320, // TODO: is this right????
}}
{...attrs}
>
@@ -710,7 +710,7 @@ const ParameterUsageLabel: FC = ({
css={{
display: "flex",
alignItems: "center",
- gap: 1,
+ gap: 8,
}}
>
{usage.value === "false" ? (