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" ? (