diff --git a/lib/server/database/migrations/1_create_catalog_table.sql b/lib/server/database/migrations/1_create_catalog_table.sql index 3a0c281..8a561c8 100644 --- a/lib/server/database/migrations/1_create_catalog_table.sql +++ b/lib/server/database/migrations/1_create_catalog_table.sql @@ -10,7 +10,7 @@ CREATE TABLE catalog ( created TIMESTAMP NOT NULL DEFAULT now(), mr varchar(255) DEFAULT NULL, tags varchar(255)[] DEFAULT NULL, - crons varchar(127) DEFAULT NULL, + crons varchar(127)[] DEFAULT NULL, env varchar(31)[] DEFAULT NULL, regions varchar(15)[] DEFAULT NULL, triggers varchar(255)[] DEFAULT NULL, diff --git a/package-lock.json b/package-lock.json index 8f8b0a8..b3096f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "dotenv": "^16.0.2", "express": "^4.18.1", "figlet": "^1.5.2", - "lodash": "^4.17.21", "moment": "^2.29.4", "path": "^0.12.7", "pg-promise": "^10.12.0", @@ -4053,7 +4052,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash.defaults": { "version": "4.2.0", @@ -9241,7 +9241,8 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "lodash.defaults": { "version": "4.2.0", diff --git a/src/job-core/JobExtra.jsx b/src/job-core/JobExtra.jsx index 716a95a..ea3980a 100644 --- a/src/job-core/JobExtra.jsx +++ b/src/job-core/JobExtra.jsx @@ -9,6 +9,7 @@ import ViewColumnIcon from "@mui/icons-material/ViewColumn"; import PendingIcon from "@mui/icons-material/Pending"; import VisibilityIcon from "@mui/icons-material/Visibility"; import DoNotDisturbIcon from "@mui/icons-material/DoNotDisturb"; +import TimerIcon from "@mui/icons-material/Timer"; import ReplayIcon from "@mui/icons-material/Replay"; function statusIcon(status) { @@ -25,6 +26,8 @@ function statusIcon(status) { return ; case jobStatus.QUEUED: return ; + case jobStatus.TIMER: + return ; default: return ; } @@ -42,6 +45,7 @@ export function useJobExtra() { function pipelineIcon(pl) { const jobStatuses = pipelineJobs(pl).map(({ status }) => status); + if (pl.pendingTriggers.length > 0) return statusIcon(jobStatus.TIMER); if (jobStatuses.includes(jobStatus.ERROR)) return statusIcon(jobStatus.ERROR); if (jobStatuses.includes(jobStatus.ACTIVE)) diff --git a/src/job-core/PipelineCore.jsx b/src/job-core/PipelineCore.jsx index 0bb1339..9e3d99f 100644 --- a/src/job-core/PipelineCore.jsx +++ b/src/job-core/PipelineCore.jsx @@ -37,15 +37,26 @@ export function usePipelineCore() { const onPipelineTrigger = (p) => { const { triggers } = p; - for (var t in triggers) { + for (const t in triggers) { if (t === "__testDelay") continue; const delay = triggers[t].__testDelay ?? 0; delete triggers[t].__testDelay; const plTrigger = { ...p, triggers: triggers[t], __test: t }; const jobReq = { ...plReq, pipeline: plTrigger }; - const timer = setTimeout(() => pipelineJob(pl, jobReq), delay); const triggerAt = Date.now() + delay; - pl.pendingTriggers.push({ testName: t, timer, triggerAt }); + const testName = t; + function removeTrigger() { + const i = pl.pendingTriggers.findIndex((pt) => pt.testName === t); + if (i < 0) return; + pl.pendingTriggers.splice(i, 1); + } + function launchTrigger() { + pipelineJob(pl, jobReq); + removeTrigger(); + } + const doTrigger = { removeTrigger, launchTrigger }; + const timer = setTimeout(launchTrigger, delay); + pl.pendingTriggers.push({ testName, timer, triggerAt, ...doTrigger }); } }; const started = initiator.newPipelineJob( diff --git a/src/job-core/job-config.js b/src/job-core/job-config.js index 9bca43c..3adf660 100644 --- a/src/job-core/job-config.js +++ b/src/job-core/job-config.js @@ -5,5 +5,6 @@ export const jobStatus = { CANCELED: "c", ACTIVE: "a", ERROR: "e", + TIMER: "t", }; export const socketUrl = "/"; diff --git a/src/views/alerting/Alerting.jsx b/src/views/alerting/Alerting.jsx index f5b6f60..fab1f74 100644 --- a/src/views/alerting/Alerting.jsx +++ b/src/views/alerting/Alerting.jsx @@ -73,7 +73,9 @@ export default function Alerting() { justifyContent="center" sx={{ flexFlow: "wrap" }} > - No alerts silenced! {" "} + + No alerts silenced!{" "} + - {" "} - + Click the '+' to create a new one! diff --git a/src/views/catalog/CatalogBox.jsx b/src/views/catalog/CatalogBox.jsx index 33b785b..483e2fa 100644 --- a/src/views/catalog/CatalogBox.jsx +++ b/src/views/catalog/CatalogBox.jsx @@ -19,6 +19,8 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import { asTree, asBranches, as1d } from "@qltr/util/pipelines.js"; +import CatalogItemDetails from "./CatalogItemDetails.jsx"; + export default function CatalogBox(props) { const { catalogTest } = props; @@ -129,9 +131,7 @@ export default function CatalogBox(props) { - - {"Test info"} - + ); diff --git a/src/views/catalog/CatalogItemDetails.jsx b/src/views/catalog/CatalogItemDetails.jsx new file mode 100644 index 0000000..4eee6c4 --- /dev/null +++ b/src/views/catalog/CatalogItemDetails.jsx @@ -0,0 +1,45 @@ +import Typography from "@mui/material/Typography"; +export default function CatalogItemDetails(props) { + const { catalogTest } = props; + + const { + name: testName, + class: testClass, + isPipeline, + description, + image, + crons, + regions, + env, + tags, + projects, + } = catalogTest; + + return ( + +
+ Image: + {image} +
+
+ Env: + {env.join(", ")} +
+
+ Regions: + {regions.join(", ")} +
+
+ Crons: + {JSON.stringify(crons)} +
+
+ Projects: + {projects.join(", ")} +
+
+ {description ?? "No Description Provided!"} +
+
+ ); +} diff --git a/src/views/failing/Failing.jsx b/src/views/failing/Failing.jsx index f6e86ed..7bcb708 100644 --- a/src/views/failing/Failing.jsx +++ b/src/views/failing/Failing.jsx @@ -90,7 +90,9 @@ export default function Failing() { {!failsLoading && failing.length === 0 && ( - No tests failing! + + No tests failing! + )} {!failsLoading && diff --git a/src/views/jobs/JobPipelineDisplay.jsx b/src/views/jobs/JobPipelineDisplay.jsx index 7456d60..9d2f9c6 100644 --- a/src/views/jobs/JobPipelineDisplay.jsx +++ b/src/views/jobs/JobPipelineDisplay.jsx @@ -1,5 +1,6 @@ -import React, { useContext } from "react"; +import React, { useContext, useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; +import moment from "moment"; import { useJobCore, jobStatus } from "@qltr/jobcore"; import Box from "@mui/material/Box"; @@ -38,17 +39,36 @@ function JobPipelineDisplay(props) { const nav = useNavigate(); - const [anchorEl, setAnchorEl] = React.useState(null); + const [time, setTime] = useState(Date.now()); + const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); const handleClick = (event) => setAnchorEl(event.currentTarget); const handleClose = () => setAnchorEl(null); + const branches = selectedPipelineBranches(pipeline); + const pipelineTriggers = branches.map(({ name }) => + pipeline.pendingTriggers.find(({ testName }) => testName === name) + ); + + useEffect(() => { + const interval = setInterval(() => setTime(Date.now()), 1000); + return () => clearInterval(interval); + }, []); + const selectJob = (testName) => () => { + const pt = pipeline.pendingTriggers.find( + ({ testName }) => testName === name + ); + if (pt) return selectTimer(pt); const job = findPipelineJobByTestName(pipeline, testName); if (!job) return; toJob(job.jobId); }; + const selectTimer = (pt) => { + console.log("Selected timer:", pt); + }; + function cancelPipeline() { pipelineCancel(pipeline.id); } @@ -69,12 +89,22 @@ function JobPipelineDisplay(props) { } function boxIcon(name) { + if (pipeline.pendingTriggers.find(({ testName }) => testName === name)) + return jobIcon({ status: jobStatus.TIMER }); if (pipeline.isCanceled) return ; const job = findPipelineJobByTestName(pipeline, name); if (!job) return ; return jobIcon(job); } + function timerDisplay(name) { + const pt = pipeline.pendingTriggers.find( + ({ testName }) => testName === name + ); + if (!pt) return; + return moment(moment(pt.triggerAt).diff(moment())).format("mm:ss"); + } + return ( nav(-1)}> - + {pipeline.id} @@ -101,7 +142,7 @@ function JobPipelineDisplay(props) { - {selectedPipelineBranches(pipeline).map((track, i) => ( + {branches.map((track, i) => ( {i + 1} @@ -125,11 +166,19 @@ function JobPipelineDisplay(props) { > {test.name} - - - {boxIcon(test.name)} - - +
+ + {timerDisplay(test.name)} + + + + {boxIcon(test.name)} + + +
))} diff --git a/src/views/jobs/JobPiplinePendingView.jsx b/src/views/jobs/JobPiplinePendingView.jsx index d0534a4..0306808 100644 --- a/src/views/jobs/JobPiplinePendingView.jsx +++ b/src/views/jobs/JobPiplinePendingView.jsx @@ -91,7 +91,7 @@ export default function JobPipelinePendingView(props) { - {job.name} + {job.jobId} {job.isPipeline && ( diff --git a/src/views/jobs/JobView.jsx b/src/views/jobs/JobView.jsx index db4694a..00d9e88 100644 --- a/src/views/jobs/JobView.jsx +++ b/src/views/jobs/JobView.jsx @@ -91,8 +91,18 @@ export default function JobView(props) { nav(-1)}> - - {job.name} + + {job.jobId} {job.isPipeline && ( diff --git a/src/views/jobs/Jobs.jsx b/src/views/jobs/Jobs.jsx index 1846d5c..d6fcbd6 100644 --- a/src/views/jobs/Jobs.jsx +++ b/src/views/jobs/Jobs.jsx @@ -55,7 +55,9 @@ export default function Jobs() { justifyContent="center" sx={{ flexFlow: "wrap" }} > - No jobs found! + + No jobs found! +
- + Click the '+' to start a new one!