From c8317cbeba51e839657e1eaefb8d70fbfb656d39 Mon Sep 17 00:00:00 2001 From: Maurice Yap Date: Thu, 12 Dec 2024 16:17:47 +0000 Subject: [PATCH 1/3] ARMADA-3015 add colours and icons to Lookout UI for displaying job statuses (#307) * Add coloured icons to display of job statuses in Jobs view * Add job status colours to job sets page --- config/lookoutv2/config.yaml | 1 + internal/lookout/ui/package.json | 6 ++ internal/lookout/ui/src/App.tsx | 37 ++----- .../lookout/ui/src/components/LinkCell.css | 9 -- .../lookout/ui/src/components/LinkCell.tsx | 15 --- internal/lookout/ui/src/components/NavBar.css | 7 ++ internal/lookout/ui/src/components/NavBar.tsx | 36 ++++--- .../ui/src/components/fontAwesomeIcons.tsx | 66 ++++++++++++ .../src/components/job-sets/JobSetTable.tsx | 100 +++++++----------- .../lookoutV2/JobGroupStateCounts.module.css | 4 +- .../lookoutV2/JobGroupStateCounts.tsx | 57 +++++----- .../src/components/lookoutV2/JobStateChip.tsx | 15 +++ .../lookoutV2/JobStateCountChip.tsx | 18 ++++ .../components/lookoutV2/JobStateLabel.tsx | 27 ----- .../components/lookoutV2/JobsTableFilter.tsx | 30 ++++-- .../lookoutV2/sidebar/SidebarHeader.tsx | 14 +-- ...lookoutV2Models.ts => lookoutV2Models.tsx} | 47 ++++++-- internal/lookout/ui/src/theme/palette.ts | 78 ++++++++++++++ internal/lookout/ui/src/theme/theme.ts | 10 ++ internal/lookout/ui/src/theme/typography.ts | 17 +++ .../lookout/ui/src/utils/jobsTableColumns.tsx | 26 ++--- .../lookout/ui/src/utils/styleUtils.test.ts | 23 ---- internal/lookout/ui/src/utils/styleUtils.ts | 4 - internal/lookout/ui/yarn.lock | 40 +++++++ 24 files changed, 431 insertions(+), 256 deletions(-) delete mode 100644 internal/lookout/ui/src/components/LinkCell.css delete mode 100644 internal/lookout/ui/src/components/LinkCell.tsx create mode 100644 internal/lookout/ui/src/components/fontAwesomeIcons.tsx create mode 100644 internal/lookout/ui/src/components/lookoutV2/JobStateChip.tsx create mode 100644 internal/lookout/ui/src/components/lookoutV2/JobStateCountChip.tsx delete mode 100644 internal/lookout/ui/src/components/lookoutV2/JobStateLabel.tsx rename internal/lookout/ui/src/models/{lookoutV2Models.ts => lookoutV2Models.tsx} (79%) create mode 100644 internal/lookout/ui/src/theme/palette.ts create mode 100644 internal/lookout/ui/src/theme/theme.ts create mode 100644 internal/lookout/ui/src/theme/typography.ts delete mode 100644 internal/lookout/ui/src/utils/styleUtils.test.ts delete mode 100644 internal/lookout/ui/src/utils/styleUtils.ts diff --git a/config/lookoutv2/config.yaml b/config/lookoutv2/config.yaml index 78432997326..1172e332c25 100644 --- a/config/lookoutv2/config.yaml +++ b/config/lookoutv2/config.yaml @@ -24,6 +24,7 @@ prunerConfig: uiConfig: backend: "jsonb" armadaApiBaseUrl: "http://armada-server:8080" + customTitle: "Local Dev" userAnnotationPrefix: "armadaproject.io/" binocularsBaseUrlPattern: "http://armada-binoculars:8080" commandSpecs: diff --git a/internal/lookout/ui/package.json b/internal/lookout/ui/package.json index c597fc7d4de..717418b763c 100644 --- a/internal/lookout/ui/package.json +++ b/internal/lookout/ui/package.json @@ -20,6 +20,12 @@ "dependencies": { "@emotion/react": "^11.13.5", "@emotion/styled": "^11.13.5", + "@fortawesome/fontawesome-common-types": "^6.7.1", + "@fortawesome/fontawesome-svg-core": "^6.7.1", + "@fortawesome/free-brands-svg-icons": "^6.7.1", + "@fortawesome/free-regular-svg-icons": "^6.7.1", + "@fortawesome/free-solid-svg-icons": "^6.7.1", + "@fortawesome/react-fontawesome": "^0.2.2", "@mui/icons-material": "^6.1.10", "@mui/lab": "^6.0.0-beta.18", "@mui/material": "^6.1.10", diff --git a/internal/lookout/ui/src/App.tsx b/internal/lookout/ui/src/App.tsx index be474c44ab3..52ed9f1840f 100644 --- a/internal/lookout/ui/src/App.tsx +++ b/internal/lookout/ui/src/App.tsx @@ -1,19 +1,18 @@ import { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react" -import { ThemeProvider, createTheme } from "@mui/material" +import { ThemeProvider } from "@mui/material" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import { JobsTableContainer } from "containers/lookoutV2/JobsTableContainer" import { SnackbarProvider } from "notistack" import { UserManager, WebStorageStateStore, UserManagerSettings, User } from "oidc-client-ts" import { BrowserRouter, Navigate, Route, Routes, useNavigate } from "react-router-dom" -import { Services, ServicesProvider } from "services/context" -import { withRouter } from "utils" import NavBar from "./components/NavBar" import JobSetsContainer from "./containers/JobSetsContainer" +import { JobsTableContainer } from "./containers/lookoutV2/JobsTableContainer" import { UserManagerContext, useUserManager } from "./oidc" -import { CommandSpec } from "./utils" -import { OidcConfig } from "./utils" +import { Services, ServicesProvider } from "./services/context" +import { theme } from "./theme/theme" +import { CommandSpec, OidcConfig, withRouter } from "./utils" import "./App.css" @@ -23,30 +22,6 @@ export const queryClient = new QueryClient({ }, }) -const theme = createTheme({ - palette: { - primary: { - main: "#00aae1", - contrastText: "#fff", - }, - }, - typography: { - fontFamily: [ - "-apple-system", - "BlinkMacSystemFont", - "'Segoe UI'", - "'Roboto'", - "'Oxygen'", - "'Ubuntu'", - "'Cantarell'", - "'Fira Sans'", - "'Droid Sans'", - "'Helvetica Neue'", - "sans-serif", - ].join(","), - }, -}) - type AppProps = { customTitle: string oidcConfig?: OidcConfig @@ -159,7 +134,7 @@ export function App(props: AppProps): JSX.Element { }, [props.customTitle]) const result = ( - + diff --git a/internal/lookout/ui/src/components/LinkCell.css b/internal/lookout/ui/src/components/LinkCell.css deleted file mode 100644 index fee8b033b91..00000000000 --- a/internal/lookout/ui/src/components/LinkCell.css +++ /dev/null @@ -1,9 +0,0 @@ -.link { - width: 100%; - color: blue; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-decoration: underline; - cursor: pointer; -} diff --git a/internal/lookout/ui/src/components/LinkCell.tsx b/internal/lookout/ui/src/components/LinkCell.tsx deleted file mode 100644 index 6539d448054..00000000000 --- a/internal/lookout/ui/src/components/LinkCell.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { TableCellProps } from "react-virtualized" - -import "./LinkCell.css" - -type LinkCellProps = { - onClick: () => void -} & TableCellProps - -export default function LinkCell(props: LinkCellProps) { - return ( -
- {props.cellData} -
- ) -} diff --git a/internal/lookout/ui/src/components/NavBar.css b/internal/lookout/ui/src/components/NavBar.css index 64d65e4d0d9..669fef7fcdb 100644 --- a/internal/lookout/ui/src/components/NavBar.css +++ b/internal/lookout/ui/src/components/NavBar.css @@ -27,3 +27,10 @@ flex-direction: row; align-items: flex-end; } + +.toolbar .nav-end { + display: flex; + flex-direction: row; + gap: 1em; + align-items: center; +} diff --git a/internal/lookout/ui/src/components/NavBar.tsx b/internal/lookout/ui/src/components/NavBar.tsx index 9b7bc8ad66f..26568b5909c 100644 --- a/internal/lookout/ui/src/components/NavBar.tsx +++ b/internal/lookout/ui/src/components/NavBar.tsx @@ -51,17 +51,19 @@ function NavBar({ customTitle, router, username }: NavBarProps) { return ( - - {""} - - Lookout - - {customTitle && ( - - {customTitle} +
+ + {""} + + Lookout - )} - + {customTitle && ( + + {customTitle} + + )} + +
- {username && ( - - Welcome, {username}! - - )} +
+
+ {username && ( + + Welcome, {username}! + + )} +
+
) diff --git a/internal/lookout/ui/src/components/fontAwesomeIcons.tsx b/internal/lookout/ui/src/components/fontAwesomeIcons.tsx new file mode 100644 index 00000000000..354c5e4a307 --- /dev/null +++ b/internal/lookout/ui/src/components/fontAwesomeIcons.tsx @@ -0,0 +1,66 @@ +import { forwardRef } from "react" + +import { IconDefinition } from "@fortawesome/fontawesome-common-types" +import { + faBan, + faCheckCircle, + faExchangeAlt, + faFileContract, + faHand, + faHourglassHalf, + faPlayCircle, + faSpinner, + faTimesCircle, +} from "@fortawesome/free-solid-svg-icons" +import { SvgIcon, SvgIconProps } from "@mui/material" + +import "@fortawesome/fontawesome-svg-core/styles.css" + +export interface FontAwesomeSvgIconProps extends SvgIconProps { + icon: IconDefinition +} + +export const FontAwesomeSvgIcon = forwardRef( + ({ icon, ...svgIconProps }, ref) => { + const { + icon: [width, height, , , svgPathData], + } = icon + + return ( + + {typeof svgPathData === "string" ? ( + + ) : ( + /** + * A multi-path Font Awesome icon seems to imply a duotune icon. The 0th path seems to + * be the faded element (referred to as the "secondary" path in the Font Awesome docs) + * of a duotone icon. 40% is the default opacity. + * + * @see https://fontawesome.com/how-to-use/on-the-web/styling/duotone-icons#changing-opacity + */ + svgPathData.map((d: string, i: number) => ) + )} + + ) + }, +) + +export const FaHourglassHalf = (svgIconProps: SvgIconProps) => ( + +) +export const FaSpinner = (svgIconProps: SvgIconProps) => +export const FaPlayCircle = (svgIconProps: SvgIconProps) => +export const FaCheckCircle = (svgIconProps: SvgIconProps) => ( + +) +export const FaTimesCircle = (svgIconProps: SvgIconProps) => ( + +) +export const FaBan = (svgIconProps: SvgIconProps) => +export const FaExcahngeAlt = (svgIconProps: SvgIconProps) => ( + +) +export const FaFileContract = (svgIconProps: SvgIconProps) => ( + +) +export const FaHand = (svgIconProps: SvgIconProps) => diff --git a/internal/lookout/ui/src/components/job-sets/JobSetTable.tsx b/internal/lookout/ui/src/components/job-sets/JobSetTable.tsx index 2b41df5ca2e..68627db7b91 100644 --- a/internal/lookout/ui/src/components/job-sets/JobSetTable.tsx +++ b/internal/lookout/ui/src/components/job-sets/JobSetTable.tsx @@ -1,13 +1,15 @@ import Truncate from "react-truncate" -import { TableCellProps, Table as VirtualizedTable, Column, defaultTableCellRenderer } from "react-virtualized" +import { TableCellProps, Table as VirtualizedTable, Column } from "react-virtualized" -import { JobState } from "../../models/lookoutV2Models" +import { JobState, jobStateColors, jobStateIcons } from "../../models/lookoutV2Models" import { JobSet } from "../../services/JobService" import CheckboxHeaderRow from "../CheckboxHeaderRow" import CheckboxRow from "../CheckboxRow" import "./JobSetTable.css" -import LinkCell from "../LinkCell" import SortableHeaderCell from "../SortableHeaderCell" +import { JobStateCountChip } from "../lookoutV2/JobStateCountChip" +import { Stack } from "@mui/material" +import { formatJobState } from "../../utils/jobsTableFormatters" interface JobSetTableProps { height: number @@ -24,12 +26,8 @@ interface JobSetTableProps { onJobSetStateClick(rowIndex: number, state: string): void } -function cellRendererForState(cellProps: TableCellProps, onClickFunc: () => void) { - if (cellProps.cellData > 0) { - return - } - - return defaultTableCellRenderer(cellProps) +function cellRendererForState(cellProps: TableCellProps, jobState: JobState, onClickFunc: () => void) { + return } function cellRendererForJobSet(cellProps: TableCellProps, width: number) { @@ -99,6 +97,7 @@ export default function JobSetTable(props: JobSetTableProps) { dataKey="jobSetId" width={0.5 * props.width} label="Job Set" + headerRenderer={(cellProps) => cellProps.label} cellRenderer={(cellProps) => cellRendererForJobSet(cellProps, 0.5 * props.width)} className="job-set-table-job-set-name-cell" /> @@ -116,60 +115,35 @@ export default function JobSetTable(props: JobSetTableProps) { /> )} /> - - cellRendererForState(cellProps, () => props.onJobSetStateClick(cellProps.rowIndex, JobState.Queued)) - } - /> - - cellRendererForState(cellProps, () => props.onJobSetStateClick(cellProps.rowIndex, JobState.Pending)) - } - /> - - cellRendererForState(cellProps, () => props.onJobSetStateClick(cellProps.rowIndex, JobState.Running)) - } - /> - - cellRendererForState(cellProps, () => props.onJobSetStateClick(cellProps.rowIndex, JobState.Succeeded)) - } - /> - - cellRendererForState(cellProps, () => props.onJobSetStateClick(cellProps.rowIndex, JobState.Failed)) - } - /> - - cellRendererForState(cellProps, () => props.onJobSetStateClick(cellProps.rowIndex, JobState.Cancelled)) - } - /> + {( + [ + [JobState.Queued, "jobsQueued"], + [JobState.Pending, "jobsPending"], + [JobState.Running, "jobsRunning"], + [JobState.Succeeded, "jobsSucceeded"], + [JobState.Failed, "jobsFailed"], + [JobState.Cancelled, "jobsCancelled"], + ] as [JobState, keyof JobSet][] + ).map(([jobState, dataKey]) => { + const Icon = jobStateIcons[jobState] + return ( + + {formatJobState(jobState)} + + + } + className="job-set-table-number-cell" + cellRenderer={(cellProps) => + cellRendererForState(cellProps, jobState, () => props.onJobSetStateClick(cellProps.rowIndex, jobState)) + } + /> + ) + })} ) diff --git a/internal/lookout/ui/src/components/lookoutV2/JobGroupStateCounts.module.css b/internal/lookout/ui/src/components/lookoutV2/JobGroupStateCounts.module.css index fa6c99e3582..a00c31629f9 100644 --- a/internal/lookout/ui/src/components/lookoutV2/JobGroupStateCounts.module.css +++ b/internal/lookout/ui/src/components/lookoutV2/JobGroupStateCounts.module.css @@ -1,8 +1,8 @@ .container { display: flex; flex-direction: row; - justify-content: center; - align-items: center; + flex-wrap: wrap; + justify-content: flex-start; gap: 5px; } diff --git a/internal/lookout/ui/src/components/lookoutV2/JobGroupStateCounts.tsx b/internal/lookout/ui/src/components/lookoutV2/JobGroupStateCounts.tsx index 42c2051797e..b79774ce1e5 100644 --- a/internal/lookout/ui/src/components/lookoutV2/JobGroupStateCounts.tsx +++ b/internal/lookout/ui/src/components/lookoutV2/JobGroupStateCounts.tsx @@ -1,32 +1,37 @@ -import { getContrastText } from "utils/styleUtils" +import { Chip, Tooltip, Typography } from "@mui/material" import styles from "./JobGroupStateCounts.module.css" -import { JobState, jobStateColors } from "../../models/lookoutV2Models" +import { JobState, jobStateColors, jobStateIcons } from "../../models/lookoutV2Models" +import { formatJobState } from "../../utils/jobsTableFormatters" interface JobGroupStateCountsProps { - stateCounts: Record + stateCounts: Partial> } -export const JobGroupStateCounts = ({ stateCounts }: JobGroupStateCountsProps) => { - const content = [] - for (const [_, state] of Object.entries(JobState)) { - if (!((state as string) in stateCounts)) { - continue - } - const backgroundColor = jobStateColors[state] - const val = ( - - {stateCounts[state as string]} - - ) - content.push(val) - } - return
{content}
-} +export const JobGroupStateCounts = ({ stateCounts }: JobGroupStateCountsProps) => ( +
+ {Object.values(JobState) + .flatMap( + (jobState) => (jobState in stateCounts ? [[jobState, stateCounts[jobState]]] : []) as [JobState, number][], + ) + .map(([_jobState, count]) => { + const jobState = _jobState as JobState + const Icon = jobStateIcons[jobState] + return ( + + + {count.toString()} + + } + color={jobStateColors[jobState]} + icon={} + /> + + ) + })} +
+) diff --git a/internal/lookout/ui/src/components/lookoutV2/JobStateChip.tsx b/internal/lookout/ui/src/components/lookoutV2/JobStateChip.tsx new file mode 100644 index 00000000000..c5a7e814212 --- /dev/null +++ b/internal/lookout/ui/src/components/lookoutV2/JobStateChip.tsx @@ -0,0 +1,15 @@ +import { Chip } from "@mui/material" + +import { JobState, jobStateColors, jobStateIcons } from "../../models/lookoutV2Models" +import { formatJobState } from "../../utils/jobsTableFormatters" + +export interface JobStateChipProps { + state: JobState +} +export const JobStateChip = ({ state }: JobStateChipProps) => { + if (!state) { + return null + } + const Icon = jobStateIcons[state] + return } /> +} diff --git a/internal/lookout/ui/src/components/lookoutV2/JobStateCountChip.tsx b/internal/lookout/ui/src/components/lookoutV2/JobStateCountChip.tsx new file mode 100644 index 00000000000..354f7aaa948 --- /dev/null +++ b/internal/lookout/ui/src/components/lookoutV2/JobStateCountChip.tsx @@ -0,0 +1,18 @@ +import { Chip } from "@mui/material" + +import { JobState, jobStateColors } from "../../models/lookoutV2Models" + +export interface JobStateCountChipProps { + state: JobState + count: number + onClick?: () => void +} +export const JobStateCountChip = ({ state, count, onClick }: JobStateCountChipProps) => { + const label = count.toString() + + return count > 0 ? ( + + ) : ( + <>{label} + ) +} diff --git a/internal/lookout/ui/src/components/lookoutV2/JobStateLabel.tsx b/internal/lookout/ui/src/components/lookoutV2/JobStateLabel.tsx deleted file mode 100644 index 90970ecb203..00000000000 --- a/internal/lookout/ui/src/components/lookoutV2/JobStateLabel.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { ReactNode } from "react" - -import { Box } from "@mui/material" -import { defaultJobStateColor, JobState, jobStateColors } from "models/lookoutV2Models" -import { getContrastText } from "utils/styleUtils" - -export interface JobStateLabelProps { - state?: JobState - children: ReactNode -} -export const JobStateLabel = ({ state, children }: JobStateLabelProps) => { - const backgroundColor = state ? jobStateColors[state] : defaultJobStateColor - return ( - - {children} - - ) -} diff --git a/internal/lookout/ui/src/components/lookoutV2/JobsTableFilter.tsx b/internal/lookout/ui/src/components/lookoutV2/JobsTableFilter.tsx index 65650528c65..c5858c20866 100644 --- a/internal/lookout/ui/src/components/lookoutV2/JobsTableFilter.tsx +++ b/internal/lookout/ui/src/components/lookoutV2/JobsTableFilter.tsx @@ -1,4 +1,4 @@ -import { MouseEvent, RefObject, useEffect, useRef, useState } from "react" +import { ElementType, MouseEvent, RefObject, useEffect, useRef, useState } from "react" import { Check, MoreVert } from "@mui/icons-material" import { @@ -6,16 +6,25 @@ import { Checkbox, IconButton, InputAdornment, + ListItemIcon, ListItemText, MenuItem, OutlinedInput, Select, + SvgIconProps, TextField, } from "@mui/material" import Menu from "@mui/material/Menu" -import { Match, MATCH_DISPLAY_STRINGS } from "models/lookoutV2Models" import { useDebouncedCallback } from "use-debounce" -import { ANNOTATION_COLUMN_PREFIX, FilterType, isStandardColId, VALID_COLUMN_MATCHES } from "utils/jobsTableColumns" + +import { Match, MATCH_DISPLAY_STRINGS } from "../../models/lookoutV2Models" +import { CustomPaletteColorToken } from "../../theme/palette" +import { + ANNOTATION_COLUMN_PREFIX, + FilterType, + isStandardColId, + VALID_COLUMN_MATCHES, +} from "../../utils/jobsTableColumns" const ELLIPSIS = "\u2026" @@ -91,6 +100,8 @@ export const JobsTableFilter = ({ export interface EnumFilterOption { value: string displayName: string + Icon?: ElementType + iconColor?: CustomPaletteColorToken } interface EnumFilterProps { currentFilter: string[] @@ -135,10 +146,15 @@ const EnumFilter = ({ currentFilter, enumFilterValues, label, onFilterChange }: }, }} > - {(enumFilterValues ?? []).map((option) => ( - - -1} size="small" sx={{ padding: "3px" }} /> - + {(enumFilterValues ?? []).map(({ value, displayName, Icon, iconColor }) => ( + + -1} size="small" sx={{ padding: "3px" }} /> + + {Icon && ( + + + + )} ))} diff --git a/internal/lookout/ui/src/components/lookoutV2/sidebar/SidebarHeader.tsx b/internal/lookout/ui/src/components/lookoutV2/sidebar/SidebarHeader.tsx index ac842b0f955..a9555eec006 100644 --- a/internal/lookout/ui/src/components/lookoutV2/sidebar/SidebarHeader.tsx +++ b/internal/lookout/ui/src/components/lookoutV2/sidebar/SidebarHeader.tsx @@ -2,10 +2,10 @@ import { memo, ReactNode } from "react" import { Close } from "@mui/icons-material" import { Box, IconButton, Typography } from "@mui/material" -import { Job } from "models/lookoutV2Models" -import { formatJobState, formatTimeSince } from "utils/jobsTableFormatters" -import { JobStateLabel } from "../JobStateLabel" +import { Job } from "../../../models/lookoutV2Models" +import { formatTimeSince } from "../../../utils/jobsTableFormatters" +import { JobStateChip } from "../JobStateChip" export interface SidebarHeaderProps { job: Job @@ -18,11 +18,11 @@ export const SidebarHeader = memo(({ job, onClose, className }: SidebarHeaderPro {job.jobId}} /> - {formatJobState(job.state)} for {formatTimeSince(job.lastTransitionTime)} - + <> + for {formatTimeSince(job.lastTransitionTime)} + } /> diff --git a/internal/lookout/ui/src/models/lookoutV2Models.ts b/internal/lookout/ui/src/models/lookoutV2Models.tsx similarity index 79% rename from internal/lookout/ui/src/models/lookoutV2Models.ts rename to internal/lookout/ui/src/models/lookoutV2Models.tsx index 47dc744f4bc..675184b9939 100644 --- a/internal/lookout/ui/src/models/lookoutV2Models.ts +++ b/internal/lookout/ui/src/models/lookoutV2Models.tsx @@ -1,3 +1,18 @@ +import { SvgIconProps } from "@mui/material" + +import { + FaHourglassHalf, + FaSpinner, + FaPlayCircle, + FaCheckCircle, + FaTimesCircle, + FaBan, + FaExcahngeAlt, + FaFileContract, + FaHand, +} from "../components/fontAwesomeIcons" +import { CustomPaletteColorToken } from "../theme/palette" + // Values must match the server-side states export enum JobState { Queued = "QUEUED", @@ -11,19 +26,29 @@ export enum JobState { Rejected = "REJECTED", } -export const jobStateColors: Record = { - [JobState.Queued]: "#6c757d", - [JobState.Pending]: "#6c757d", - [JobState.Running]: "#007bff", - [JobState.Succeeded]: "#28a745", - [JobState.Failed]: "#88022b", - [JobState.Cancelled]: "#ffc107", - [JobState.Preempted]: "#ffc107", - [JobState.Leased]: "#6c757d", - [JobState.Rejected]: "#88022b", +export const jobStateColors: Record = { + [JobState.Queued]: "statusGrey", + [JobState.Pending]: "statusGrey", + [JobState.Running]: "statusBlue", + [JobState.Succeeded]: "statusGreen", + [JobState.Failed]: "statusRed", + [JobState.Cancelled]: "statusAmber", + [JobState.Preempted]: "statusAmber", + [JobState.Leased]: "statusGrey", + [JobState.Rejected]: "statusRed", } -export const defaultJobStateColor = "#6f42c1" +export const jobStateIcons: Record JSX.Element> = { + [JobState.Queued]: FaHourglassHalf, + [JobState.Pending]: FaSpinner, + [JobState.Running]: FaPlayCircle, + [JobState.Succeeded]: FaCheckCircle, + [JobState.Failed]: FaTimesCircle, + [JobState.Cancelled]: FaBan, + [JobState.Preempted]: FaExcahngeAlt, + [JobState.Leased]: FaFileContract, + [JobState.Rejected]: FaHand, +} export const jobStateDisplayNames: Record = { [JobState.Leased]: "Leased", diff --git a/internal/lookout/ui/src/theme/palette.ts b/internal/lookout/ui/src/theme/palette.ts new file mode 100644 index 00000000000..00960fe3cc4 --- /dev/null +++ b/internal/lookout/ui/src/theme/palette.ts @@ -0,0 +1,78 @@ +import { darken, getContrastRatio, lighten, Palette, PaletteColorOptions, PaletteOptions } from "@mui/material" + +const TONAL_OFFSET = 0.3 +const CONTRAST_THRESHOLD = 3 + +// Brand colours +export const LIGHT_BLUE = "#25AEFF" as const +export const ORANGE = "#FF7625" as const // the complementary colour to LIGHT_BLUE + +// Status colours +export const STATUS_GREY = "#6C757D" as const +export const STATUS_BLUE = "#007BFF" as const +export const STATUS_GREEN = "#28A745" as const +export const STATUS_AMBER = "#FFC107" as const +export const STATUS_RED = "#88022B" as const + +export const CUSTOM_PALETTE_COLOR_TOKENS = [ + "statusGrey", + "statusBlue", + "statusGreen", + "statusAmber", + "statusRed", +] as const + +export type CustomPaletteColorToken = (typeof CUSTOM_PALETTE_COLOR_TOKENS)[number] + +type CustomPaletteColorTokensPaletteMap = { [Name in CustomPaletteColorToken]: Palette["primary"] } +type CustomPaletteColorTokensPaletteOptionsMap = { + [Name in CustomPaletteColorToken]: PaletteOptions["primary"] +} +type CustomPaletteColorTokensTrueMap = { [Name in CustomPaletteColorToken]: true } + +const augmentColor = (main: string): PaletteColorOptions => ({ + light: lighten(main, TONAL_OFFSET), + main, + dark: darken(main, TONAL_OFFSET), + contrastText: getContrastRatio(main, "#fff") > CONTRAST_THRESHOLD ? "#fff" : "#000", +}) + +export const palette: PaletteOptions = { + primary: { main: LIGHT_BLUE, contrastText: "#FFF" }, + secondary: { main: ORANGE }, + statusGrey: augmentColor(STATUS_GREY), + statusBlue: augmentColor(STATUS_BLUE), + statusGreen: augmentColor(STATUS_GREEN), + statusAmber: augmentColor(STATUS_AMBER), + statusRed: augmentColor(STATUS_RED), + tonalOffset: TONAL_OFFSET, + contrastThreshold: CONTRAST_THRESHOLD, +} + +declare module "@mui/material/styles" { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface Palette extends CustomPaletteColorTokensPaletteMap {} + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface PaletteOptions extends CustomPaletteColorTokensPaletteOptionsMap {} +} + +declare module "@mui/material/Alert" { + interface AlertPropsColorOverrides extends CustomPaletteColorTokensTrueMap { + secondary: true + } +} + +declare module "@mui/material/Button" { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface ButtonPropsColorOverrides extends CustomPaletteColorTokensTrueMap {} +} + +declare module "@mui/material/Chip" { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface ChipPropsColorOverrides extends CustomPaletteColorTokensTrueMap {} +} + +declare module "@mui/material/SvgIcon" { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface SvgIconPropsColorOverrides extends CustomPaletteColorTokensTrueMap {} +} diff --git a/internal/lookout/ui/src/theme/theme.ts b/internal/lookout/ui/src/theme/theme.ts new file mode 100644 index 00000000000..c18e2419890 --- /dev/null +++ b/internal/lookout/ui/src/theme/theme.ts @@ -0,0 +1,10 @@ +import { createTheme } from "@mui/material" + +import { palette } from "./palette" +import { typography } from "./typography" + +export const theme = createTheme({ + colorSchemes: { dark: false, light: true }, + palette, + typography, +}) diff --git a/internal/lookout/ui/src/theme/typography.ts b/internal/lookout/ui/src/theme/typography.ts new file mode 100644 index 00000000000..70c2eadc2a9 --- /dev/null +++ b/internal/lookout/ui/src/theme/typography.ts @@ -0,0 +1,17 @@ +import { TypographyOptions } from "@mui/material/styles/createTypography" + +export const typography: TypographyOptions = { + fontFamily: [ + "-apple-system", + "BlinkMacSystemFont", + "'Segoe UI'", + "'Roboto'", + "'Oxygen'", + "'Ubuntu'", + "'Cantarell'", + "'Fira Sans'", + "'Droid Sans'", + "'Helvetica Neue'", + "sans-serif", + ].join(","), +} diff --git a/internal/lookout/ui/src/utils/jobsTableColumns.tsx b/internal/lookout/ui/src/utils/jobsTableColumns.tsx index fb4f947389c..a6bd08ad4a5 100644 --- a/internal/lookout/ui/src/utils/jobsTableColumns.tsx +++ b/internal/lookout/ui/src/utils/jobsTableColumns.tsx @@ -1,15 +1,15 @@ import { Checkbox } from "@mui/material" import { CellContext, Row } from "@tanstack/react-table" import { ColumnDef, createColumnHelper, VisibilityState } from "@tanstack/table-core" -import { JobStateLabel } from "components/lookoutV2/JobStateLabel" -import { EnumFilterOption } from "components/lookoutV2/JobsTableFilter" -import { isJobGroupRow, JobTableRow } from "models/jobsTableModels" -import { JobState, Match } from "models/lookoutV2Models" import { formatJobState, formatTimeSince, formatUtcDate } from "./jobsTableFormatters" import { formatBytes, formatCpu, parseBytes, parseCpu, parseInteger } from "./resourceUtils" import { JobGroupStateCounts } from "../components/lookoutV2/JobGroupStateCounts" +import { JobStateChip } from "../components/lookoutV2/JobStateChip" +import { EnumFilterOption } from "../components/lookoutV2/JobsTableFilter" import { LookoutColumnOrder } from "../containers/lookoutV2/JobsTableContainer" +import { isJobGroupRow, JobTableRow } from "../models/jobsTableModels" +import { JobState, jobStateColors, jobStateIcons, Match } from "../models/lookoutV2Models" export type JobTableColumn = ColumnDef @@ -225,7 +225,7 @@ export const JOB_COLUMNS: JobTableColumn[] = [ enableGrouping: true, enableColumnFilter: true, size: 300, - cell: (cell) => { + cell: (cell: CellContext) => { if ( cell.row.original && isJobGroupRow(cell.row.original) && @@ -234,14 +234,10 @@ export const JOB_COLUMNS: JobTableColumn[] = [ ) { return } else { - return ( - - {formatJobState(cell.getValue() as JobState)} - - ) + return } }, - aggregatedCell: (cell) => { + aggregatedCell: (cell: CellContext) => { if ( cell.row.original && isJobGroupRow(cell.row.original) && @@ -250,11 +246,7 @@ export const JOB_COLUMNS: JobTableColumn[] = [ ) { return } else { - return ( - - {formatJobState(cell.getValue() as JobState)} - - ) + return } }, }, @@ -263,6 +255,8 @@ export const JOB_COLUMNS: JobTableColumn[] = [ enumFilterValues: Object.values(JobState).map((state) => ({ value: state, displayName: formatJobState(state), + Icon: jobStateIcons[state], + iconColor: jobStateColors[state], })), defaultMatchType: Match.AnyOf, }, diff --git a/internal/lookout/ui/src/utils/styleUtils.test.ts b/internal/lookout/ui/src/utils/styleUtils.test.ts deleted file mode 100644 index 015db326257..00000000000 --- a/internal/lookout/ui/src/utils/styleUtils.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { getContrastText } from "./styleUtils" - -describe("getContrastText", () => { - it("returns black when no background color is provided", () => { - expect(getContrastText("")).toEqual("#000") - }) - - it("returns black for a white background", () => { - expect(getContrastText("#fff")).toEqual("#000") - }) - - it("returns white for a black background", () => { - expect(getContrastText("#000")).toEqual("#fff") - }) - - it("returns white for a dark blue background", () => { - expect(getContrastText("#000013")).toEqual("#fff") - }) - - it("returns black for a yellow background", () => { - expect(getContrastText("#ffff00")).toEqual("#000") - }) -}) diff --git a/internal/lookout/ui/src/utils/styleUtils.ts b/internal/lookout/ui/src/utils/styleUtils.ts deleted file mode 100644 index b91810291e3..00000000000 --- a/internal/lookout/ui/src/utils/styleUtils.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { getContrastRatio } from "@mui/material" - -export const getContrastText = (backgroundColor: string) => - getContrastRatio(backgroundColor || "#fff", "#fff") > 3 ? "#fff" : "#000" diff --git a/internal/lookout/ui/yarn.lock b/internal/lookout/ui/yarn.lock index 9fa9ab07ac3..dd8dd6a3580 100644 --- a/internal/lookout/ui/yarn.lock +++ b/internal/lookout/ui/yarn.lock @@ -1781,6 +1781,46 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62" integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig== +"@fortawesome/fontawesome-common-types@6.7.1", "@fortawesome/fontawesome-common-types@^6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.1.tgz#6201640f39fdcf8e41cd9d1a92b2da3a96817fa4" + integrity sha512-gbDz3TwRrIPT3i0cDfujhshnXO9z03IT1UKRIVi/VEjpNHtSBIP2o5XSm+e816FzzCFEzAxPw09Z13n20PaQJQ== + +"@fortawesome/fontawesome-svg-core@^6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.1.tgz#1f8ebb6f35cf02f89c110198514e848de17ac99e" + integrity sha512-8dBIHbfsKlCk2jHQ9PoRBg2Z+4TwyE3vZICSnoDlnsHA6SiMlTwfmW6yX0lHsRmWJugkeb92sA0hZdkXJhuz+g== + dependencies: + "@fortawesome/fontawesome-common-types" "6.7.1" + +"@fortawesome/free-brands-svg-icons@^6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.1.tgz#1c1bbbac3ab897d02322b18fbbdf4d9b67ec1619" + integrity sha512-nJR76eqPzCnMyhbiGf6X0aclDirZriTPRcFm1YFvuupyJOGwlNF022w3YBqu+yrHRhnKRpzFX+8wJKqiIjWZkA== + dependencies: + "@fortawesome/fontawesome-common-types" "6.7.1" + +"@fortawesome/free-regular-svg-icons@^6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.1.tgz#d7ec06f896ee91116a388a5a234cd26420ccdfe4" + integrity sha512-e13cp+bAx716RZOTQ59DhqikAgETA9u1qTBHO3e3jMQQ+4H/N1NC1ZVeFYt1V0m+Th68BrEL1/X6XplISutbXg== + dependencies: + "@fortawesome/fontawesome-common-types" "6.7.1" + +"@fortawesome/free-solid-svg-icons@^6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.1.tgz#c1f9a6c25562a12c283e87e284f9d82a5b0dbcc0" + integrity sha512-BTKc0b0mgjWZ2UDKVgmwaE0qt0cZs6ITcDgjrti5f/ki7aF5zs+N91V6hitGo3TItCFtnKg6cUVGdTmBFICFRg== + dependencies: + "@fortawesome/fontawesome-common-types" "6.7.1" + +"@fortawesome/react-fontawesome@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz#68b058f9132b46c8599875f6a636dad231af78d4" + integrity sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g== + dependencies: + prop-types "^15.8.1" + "@humanwhocodes/config-array@^0.11.6": version "0.11.7" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.7.tgz#38aec044c6c828f6ed51d5d7ae3d9b9faf6dbb0f" From a95b4dbd53a90b4e119c344eaa1f856b4980ddb0 Mon Sep 17 00:00:00 2001 From: Maurice Yap Date: Thu, 12 Dec 2024 17:42:52 +0000 Subject: [PATCH 2/3] Fix lint (#308) --- config/lookoutv2/config.yaml | 10 +++++----- .../lookout/ui/src/components/job-sets/JobSetTable.tsx | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config/lookoutv2/config.yaml b/config/lookoutv2/config.yaml index 1172e332c25..f423cc5eeed 100644 --- a/config/lookoutv2/config.yaml +++ b/config/lookoutv2/config.yaml @@ -17,7 +17,7 @@ postgres: dbname: lookout sslmode: disable prunerConfig: - expireAfter: 1008h # 42 days / 6 weeks + expireAfter: 1008h # 42 days / 6 weeks deduplicationExpireAfter: 168h # 7 days timeout: 1h batchSize: 1000 @@ -28,7 +28,7 @@ uiConfig: userAnnotationPrefix: "armadaproject.io/" binocularsBaseUrlPattern: "http://armada-binoculars:8080" commandSpecs: - - name: Logs - template: "kubectl --context {{ runs[runs.length - 1].cluster }} -n {{ namespace }} logs armada-{{ jobId }}-0" - - name: Exec - template: "kubectl --context {{ runs[runs.length - 1].cluster }} -n {{ namespace }} exec -it armada-{{ jobId }}-0 /bin/sh" + - name: Logs + template: "kubectl --context {{ runs[runs.length - 1].cluster }} -n {{ namespace }} logs armada-{{ jobId }}-0" + - name: Exec + template: "kubectl --context {{ runs[runs.length - 1].cluster }} -n {{ namespace }} exec -it armada-{{ jobId }}-0 /bin/sh" diff --git a/internal/lookout/ui/src/components/job-sets/JobSetTable.tsx b/internal/lookout/ui/src/components/job-sets/JobSetTable.tsx index 68627db7b91..42286c80b85 100644 --- a/internal/lookout/ui/src/components/job-sets/JobSetTable.tsx +++ b/internal/lookout/ui/src/components/job-sets/JobSetTable.tsx @@ -1,15 +1,15 @@ +import { Stack } from "@mui/material" import Truncate from "react-truncate" import { TableCellProps, Table as VirtualizedTable, Column } from "react-virtualized" import { JobState, jobStateColors, jobStateIcons } from "../../models/lookoutV2Models" import { JobSet } from "../../services/JobService" +import { formatJobState } from "../../utils/jobsTableFormatters" import CheckboxHeaderRow from "../CheckboxHeaderRow" import CheckboxRow from "../CheckboxRow" import "./JobSetTable.css" import SortableHeaderCell from "../SortableHeaderCell" import { JobStateCountChip } from "../lookoutV2/JobStateCountChip" -import { Stack } from "@mui/material" -import { formatJobState } from "../../utils/jobsTableFormatters" interface JobSetTableProps { height: number From d2e710ee576d7abcf7200fea54f8a55f0188c62b Mon Sep 17 00:00:00 2001 From: Maurice Yap Date: Fri, 13 Dec 2024 12:07:36 +0000 Subject: [PATCH 3/3] Fix TypeScript with casting The TypeScript type checker sometimes fails when the development server is run because it does not correctly infer this type. This seems to be inconsistent and so was not caught in the previous commit which introduced this change. --- internal/lookout/ui/src/utils/jobsTableColumns.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/lookout/ui/src/utils/jobsTableColumns.tsx b/internal/lookout/ui/src/utils/jobsTableColumns.tsx index a6bd08ad4a5..99f1cb548ba 100644 --- a/internal/lookout/ui/src/utils/jobsTableColumns.tsx +++ b/internal/lookout/ui/src/utils/jobsTableColumns.tsx @@ -225,7 +225,8 @@ export const JOB_COLUMNS: JobTableColumn[] = [ enableGrouping: true, enableColumnFilter: true, size: 300, - cell: (cell: CellContext) => { + cell: (_cell) => { + const cell = _cell as CellContext if ( cell.row.original && isJobGroupRow(cell.row.original) && @@ -237,7 +238,8 @@ export const JOB_COLUMNS: JobTableColumn[] = [ return } }, - aggregatedCell: (cell: CellContext) => { + aggregatedCell: (_cell) => { + const cell = _cell as CellContext if ( cell.row.original && isJobGroupRow(cell.row.original) &&