diff --git a/config/lookoutv2/config.yaml b/config/lookoutv2/config.yaml index 9a80e80fe9b..1a4e480b7c5 100644 --- a/config/lookoutv2/config.yaml +++ b/config/lookoutv2/config.yaml @@ -26,3 +26,8 @@ uiConfig: armadaApiBaseUrl: "http://armada-server:8080" 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" diff --git a/internal/lookout/ui/src/App.tsx b/internal/lookout/ui/src/App.tsx index fca2b7c9243..23e857934fb 100644 --- a/internal/lookout/ui/src/App.tsx +++ b/internal/lookout/ui/src/App.tsx @@ -20,6 +20,7 @@ import { ICordonService } from "./services/lookoutV2/CordonService" import { IGetJobSpecService } from "./services/lookoutV2/GetJobSpecService" import { IGetRunErrorService } from "./services/lookoutV2/GetRunErrorService" import { ILogService } from "./services/lookoutV2/LogService" +import { CommandSpec } from "./utils" import { OidcConfig } from "./utils" import "./App.css" @@ -76,6 +77,7 @@ type AppProps = { jobSetsAutoRefreshMs: number | undefined jobsAutoRefreshMs: number | undefined debugEnabled: boolean + commandSpecs: CommandSpec[] } function OidcCallback(): JSX.Element { @@ -130,6 +132,7 @@ export function App(props: AppProps): JSX.Element { cordonService={props.v2CordonService} debug={props.debugEnabled} autoRefreshMs={props.jobsAutoRefreshMs} + commandSpecs={props.commandSpecs} /> } /> diff --git a/internal/lookout/ui/src/components/lookoutV2/sidebar/Sidebar.test.tsx b/internal/lookout/ui/src/components/lookoutV2/sidebar/Sidebar.test.tsx index fdc9340e36d..61b066bd3a0 100644 --- a/internal/lookout/ui/src/components/lookoutV2/sidebar/Sidebar.test.tsx +++ b/internal/lookout/ui/src/components/lookoutV2/sidebar/Sidebar.test.tsx @@ -50,6 +50,7 @@ describe("Sidebar", () => { sidebarWidth={600} onClose={onClose} onWidthChange={() => undefined} + commandSpecs={[]} /> , ) diff --git a/internal/lookout/ui/src/components/lookoutV2/sidebar/Sidebar.tsx b/internal/lookout/ui/src/components/lookoutV2/sidebar/Sidebar.tsx index 9376a140e75..37397e11e24 100644 --- a/internal/lookout/ui/src/components/lookoutV2/sidebar/Sidebar.tsx +++ b/internal/lookout/ui/src/components/lookoutV2/sidebar/Sidebar.tsx @@ -6,6 +6,7 @@ import { Job, JobState } from "models/lookoutV2Models" import styles from "./Sidebar.module.css" import { SidebarHeader } from "./SidebarHeader" +import { SidebarTabJobCommands } from "./SidebarTabJobCommands" import { SidebarTabJobDetails } from "./SidebarTabJobDetails" import { SidebarTabJobLogs } from "./SidebarTabJobLogs" import { SidebarTabJobRuns } from "./SidebarTabJobRuns" @@ -14,12 +15,14 @@ import { ICordonService } from "../../../services/lookoutV2/CordonService" import { IGetJobSpecService } from "../../../services/lookoutV2/GetJobSpecService" import { IGetRunErrorService } from "../../../services/lookoutV2/GetRunErrorService" import { ILogService } from "../../../services/lookoutV2/LogService" +import { CommandSpec } from "../../../utils" enum SidebarTab { JobDetails = "JobDetails", JobRuns = "JobRuns", Yaml = "Yaml", Logs = "Logs", + Commands = "Commands", } type ResizeState = { @@ -35,6 +38,7 @@ export interface SidebarProps { logService: ILogService cordonService: ICordonService sidebarWidth: number + commandSpecs: CommandSpec[] onClose: () => void onWidthChange: (width: number) => void } @@ -49,6 +53,7 @@ export const Sidebar = memo( sidebarWidth, onClose, onWidthChange, + commandSpecs, }: SidebarProps) => { const [openTab, setOpenTab] = useState(SidebarTab.JobDetails) @@ -168,6 +173,12 @@ export const Sidebar = memo( sx={{ minWidth: "50px" }} disabled={job.state === JobState.Queued} > + @@ -185,6 +196,10 @@ export const Sidebar = memo( + + + + diff --git a/internal/lookout/ui/src/components/lookoutV2/sidebar/SidebarTabJobCommands.module.css b/internal/lookout/ui/src/components/lookoutV2/sidebar/SidebarTabJobCommands.module.css new file mode 100644 index 00000000000..845c34e93d1 --- /dev/null +++ b/internal/lookout/ui/src/components/lookoutV2/sidebar/SidebarTabJobCommands.module.css @@ -0,0 +1,11 @@ +.commandsText { + width: 100%; + white-space: pre-wrap; + font-family: monospace; + word-wrap: break-word; + background: #1b1b1b none; + color: #fff; + padding: 5px; + border-radius: 5px; + position: relative; +} diff --git a/internal/lookout/ui/src/components/lookoutV2/sidebar/SidebarTabJobCommands.tsx b/internal/lookout/ui/src/components/lookoutV2/sidebar/SidebarTabJobCommands.tsx new file mode 100644 index 00000000000..ee00357036c --- /dev/null +++ b/internal/lookout/ui/src/components/lookoutV2/sidebar/SidebarTabJobCommands.tsx @@ -0,0 +1,61 @@ +import React, { useCallback } from "react" + +import { ContentCopy } from "@mui/icons-material" +import { IconButton, Link } from "@mui/material" +import { template, templateSettings } from "lodash" +import { Job } from "models/lookoutV2Models" +import validator from "validator" + +import styles from "./SidebarTabJobCommands.module.css" +import { useCustomSnackbar } from "../../../hooks/useCustomSnackbar" +import { CommandSpec } from "../../../utils" + +export interface SidebarTabJobCommandsProps { + job: Job + commandSpecs: CommandSpec[] +} + +function getCommandText(job: Job, commandSpec: CommandSpec): string { + templateSettings.interpolate = /{{([\s\S]+?)}}/g + const compiledTemplate = template(commandSpec.template) + return compiledTemplate(job) +} + +export const SidebarTabJobCommands = ({ job, commandSpecs }: SidebarTabJobCommandsProps) => { + const openSnackbar = useCustomSnackbar() + + const copyCommand = useCallback(async (commandText: string) => { + await navigator.clipboard.writeText(commandText) + openSnackbar("Copied command to clipboard!", "info", { + autoHideDuration: 3000, + preventDuplicate: true, + }) + }, []) + + return ( +
+ {job.runs?.length ? ( +
+ {commandSpecs.map((c, i) => ( + <> +
+ {i > 0 ?
: undefined} + {c.name} + copyCommand(getCommandText(job, c))}> + + +
+ {validator.isURL(getCommandText(job, c)) ? ( + +
{getCommandText(job, c)}
+ + ) : ( +
{getCommandText(job, c)}
+ )} + + ))} +
+ ) : undefined} +
+ ) +} diff --git a/internal/lookout/ui/src/containers/lookoutV2/JobsTableContainer.test.tsx b/internal/lookout/ui/src/containers/lookoutV2/JobsTableContainer.test.tsx index cf69aaf48c2..a4f9d4a134b 100644 --- a/internal/lookout/ui/src/containers/lookoutV2/JobsTableContainer.test.tsx +++ b/internal/lookout/ui/src/containers/lookoutV2/JobsTableContainer.test.tsx @@ -91,6 +91,7 @@ describe("JobsTableContainer", () => { cordonService={new FakeCordonService()} debug={false} autoRefreshMs={30000} + commandSpecs={[]} /> ) diff --git a/internal/lookout/ui/src/containers/lookoutV2/JobsTableContainer.tsx b/internal/lookout/ui/src/containers/lookoutV2/JobsTableContainer.tsx index 0f8f7957fe5..71c65000855 100644 --- a/internal/lookout/ui/src/containers/lookoutV2/JobsTableContainer.tsx +++ b/internal/lookout/ui/src/containers/lookoutV2/JobsTableContainer.tsx @@ -73,7 +73,7 @@ import { ICordonService } from "../../services/lookoutV2/CordonService" import { CustomViewsService } from "../../services/lookoutV2/CustomViewsService" import { IGetJobSpecService } from "../../services/lookoutV2/GetJobSpecService" import { ILogService } from "../../services/lookoutV2/LogService" -import { getErrorMessage, waitMillis } from "../../utils" +import { getErrorMessage, waitMillis, CommandSpec } from "../../utils" import { EmptyInputError, ParseError } from "../../utils/resourceUtils" const PAGE_SIZE_OPTIONS = [5, 25, 50, 100] @@ -88,6 +88,7 @@ interface JobsTableContainerProps { cordonService: ICordonService debug: boolean autoRefreshMs: number | undefined + commandSpecs: CommandSpec[] } export type LookoutColumnFilter = { @@ -132,6 +133,7 @@ export const JobsTableContainer = ({ cordonService, debug, autoRefreshMs, + commandSpecs, }: JobsTableContainerProps) => { const openSnackbar = useCustomSnackbar() @@ -846,6 +848,7 @@ export const JobsTableContainer = ({ sidebarWidth={sidebarWidth} onClose={sideBarClose} onWidthChange={setSidebarWidth} + commandSpecs={commandSpecs} /> )} diff --git a/internal/lookout/ui/src/index.tsx b/internal/lookout/ui/src/index.tsx index ab5e1822bf7..564b9f74eb5 100644 --- a/internal/lookout/ui/src/index.tsx +++ b/internal/lookout/ui/src/index.tsx @@ -63,6 +63,7 @@ import "./index.css" jobSetsAutoRefreshMs={uiConfig.jobSetsAutoRefreshMs} jobsAutoRefreshMs={uiConfig.jobsAutoRefreshMs} debugEnabled={uiConfig.debugEnabled} + commandSpecs={uiConfig.commandSpecs} />, document.getElementById("root"), ) diff --git a/internal/lookout/ui/src/utils.tsx b/internal/lookout/ui/src/utils.tsx index 1176bdf9312..c60413d08ff 100644 --- a/internal/lookout/ui/src/utils.tsx +++ b/internal/lookout/ui/src/utils.tsx @@ -7,6 +7,10 @@ export interface OidcConfig { clientId: string scope: string } +export interface CommandSpec { + name: string + template: string +} interface UIConfig { armadaApiBaseUrl: string @@ -19,6 +23,7 @@ interface UIConfig { customTitle: string oidcEnabled: boolean oidc?: OidcConfig + commandSpecs: CommandSpec[] } export type RequestStatus = "Loading" | "Idle" @@ -46,6 +51,7 @@ export async function getUIConfig(): Promise { customTitle: "", oidcEnabled: false, oidc: undefined, + commandSpecs: [], } try { @@ -64,6 +70,11 @@ export async function getUIConfig(): Promise { clientId: json.Oidc.ClientId, scope: json.Oidc.Scope, } + if (json.CommandSpecs) { + config.commandSpecs = json.CommandSpecs.map((c: { Name: string; Template: string }) => { + return { name: c.Name, template: c.Template } + }) + } } } catch (e) { console.error(e) diff --git a/internal/lookoutv2/configuration/types.go b/internal/lookoutv2/configuration/types.go index 4eed975322a..61995ef81f8 100644 --- a/internal/lookoutv2/configuration/types.go +++ b/internal/lookoutv2/configuration/types.go @@ -33,6 +33,11 @@ type PrunerConfig struct { BatchSize int } +type CommandSpec struct { + Name string + Template string +} + type UIConfig struct { CustomTitle string @@ -51,4 +56,5 @@ type UIConfig struct { JobSetsAutoRefreshMs int `json:",omitempty"` JobsAutoRefreshMs int `json:",omitempty"` + CommandSpecs []CommandSpec }