Merge branch 'SwitchToLocalDev' into 'master'
Link K8S deps properly See merge request Dunemask/qualiteer!3
This commit is contained in:
commit
945afdfbbe
64 changed files with 4282 additions and 3069 deletions
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"files": {}
|
||||
}
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@ node_modules/
|
|||
build/
|
||||
jobs/*
|
||||
qltr-executor
|
||||
.env
|
||||
|
|
67
.replit
67
.replit
|
@ -1,67 +0,0 @@
|
|||
# The command that is executed when the run button is clicked.
|
||||
run = ["echo", "run"]
|
||||
|
||||
entrypoint = "index.js"
|
||||
|
||||
[nix]
|
||||
channel = "stable-21_11"
|
||||
|
||||
[env]
|
||||
XDG_CONFIG_HOME = "/home/runner/.config"
|
||||
|
||||
[packager]
|
||||
language = "nodejs"
|
||||
|
||||
[packager.features]
|
||||
packageSearch = true
|
||||
guessImports = true
|
||||
enabledForHosting = false
|
||||
|
||||
[unitTest]
|
||||
language = "nodejs"
|
||||
|
||||
[languages.javascript]
|
||||
pattern = "**/{*.js,*.jsx,*.ts,*.tsx}"
|
||||
|
||||
[languages.javascript.languageServer]
|
||||
start = [ "typescript-language-server", "--stdio" ]
|
||||
|
||||
[debugger]
|
||||
support = true
|
||||
|
||||
[debugger.interactive]
|
||||
transport = "localhost:0"
|
||||
startCommand = [ "dap-node" ]
|
||||
|
||||
[debugger.interactive.initializeMessage]
|
||||
command = "initialize"
|
||||
type = "request"
|
||||
|
||||
[debugger.interactive.initializeMessage.arguments]
|
||||
clientID = "replit"
|
||||
clientName = "replit.com"
|
||||
columnsStartAt1 = true
|
||||
linesStartAt1 = true
|
||||
locale = "en-us"
|
||||
pathFormat = "path"
|
||||
supportsInvalidatedEvent = true
|
||||
supportsProgressReporting = true
|
||||
supportsRunInTerminalRequest = true
|
||||
supportsVariablePaging = true
|
||||
supportsVariableType = true
|
||||
|
||||
[debugger.interactive.launchMessage]
|
||||
command = "launch"
|
||||
type = "request"
|
||||
|
||||
[debugger.interactive.launchMessage.arguments]
|
||||
args = []
|
||||
console = "externalTerminal"
|
||||
cwd = "."
|
||||
environment = []
|
||||
pauseForSourceMap = false
|
||||
program = "./index.js"
|
||||
request = "launch"
|
||||
sourceMaps = true
|
||||
stopOnEntry = false
|
||||
type = "pwa-node"
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:16
|
||||
FROM node:18
|
||||
WORKDIR /dunemask/net/qualiteer
|
||||
# Copy dependencies
|
||||
COPY package.json .
|
||||
|
@ -6,11 +6,13 @@ COPY package-lock.json .
|
|||
RUN npm i
|
||||
# Copy react build resources over
|
||||
COPY public public
|
||||
COPY dist dist
|
||||
COPY src src
|
||||
COPY lib lib
|
||||
COPY index.html .
|
||||
RUN npm run build:react
|
||||
COPY executor.config.js .
|
||||
COPY vite.config.js .
|
||||
RUN npm run build:all
|
||||
# Copy bin over
|
||||
COPY bin bin
|
||||
CMD ["npm","start"]
|
||||
|
||||
|
|
14
ROADMAP.md
14
ROADMAP.md
|
@ -7,20 +7,20 @@
|
|||
- [x] Initial Skeleton
|
||||
- [x] Frontend Drafts
|
||||
- [x] Frontend Core
|
||||
- [ ] Frontend Pages
|
||||
- [x] Frontend Pages
|
||||
|
||||
## v0.0.2
|
||||
|
||||
- [ ] Database Queries (Req PG)
|
||||
- [ ] Database Tables (Req PG)
|
||||
- [ ] Backend Routes (Req Database)
|
||||
- [x] Database Queries (Req PG)
|
||||
- [x] Database Tables (Req PG)
|
||||
- [x] Backend Routes (Req Database)
|
||||
- [ ] Crons
|
||||
|
||||
## v0.0.3
|
||||
|
||||
- [ ] Rabbitmq Consumers (Req Database)
|
||||
- [ ] Alerting
|
||||
- [ ] Silencing
|
||||
- [x] Rabbitmq Consumers (Req Database)
|
||||
- [x] Alerting
|
||||
- [x] Silencing
|
||||
|
||||
## v0.0.4
|
||||
|
||||
|
|
BIN
bin/executor
BIN
bin/executor
Binary file not shown.
2
dist/app.js
vendored
2
dist/app.js
vendored
|
@ -4,5 +4,3 @@ import Qualiteer from "qualiteer";
|
|||
|
||||
const qltr = new Qualiteer();
|
||||
await qltr.start();
|
||||
|
||||
process.env.INTERNAL_EXECUTOR = "true";
|
||||
|
|
2
dist/bundles/qualiteer-executor.mjs
vendored
2
dist/bundles/qualiteer-executor.mjs
vendored
File diff suppressed because one or more lines are too long
|
@ -3,5 +3,5 @@ export default function executorConfig(payload){
|
|||
command: ({ command }) => command,
|
||||
url: ({ url }) => url,
|
||||
jobId: ({ jobId }) => jobId,
|
||||
}
|
||||
};
|
||||
}
|
|
@ -6,13 +6,14 @@ import { INFO, OK, logInfo } from "../util/logging.js";
|
|||
|
||||
// Import Core Modules
|
||||
import buildRoutes from "../routes/router.js";
|
||||
import buildPostgres from "../database/postgres.js";
|
||||
import pg from "../database/postgres.js";
|
||||
import injectSockets from "../sockets/socket-server.js";
|
||||
import JobManager from "../jobs/JobManager.js";
|
||||
import buildRabbiteer from "../rabbit/rabbit-workers.js";
|
||||
|
||||
// Constants
|
||||
const title = "QLTR";
|
||||
const rabbiteerEnabled = process.env.QUALITEER_RABBITEER_ENABLED !== "false";
|
||||
const port = process.env.QUALITEER_DEV_PORT ?? 52000;
|
||||
|
||||
// Class
|
||||
|
@ -27,7 +28,7 @@ export default class Qualiteer {
|
|||
logInfo(fig.textSync(title, "Cyberlarge"));
|
||||
INFO("INIT", "Initializing...");
|
||||
this.app = express();
|
||||
this.pg = buildPostgres();
|
||||
this.pg = pg;
|
||||
this.server = http.createServer(this.app);
|
||||
this.sockets = injectSockets(this.server, this.jobs);
|
||||
this.routes = buildRoutes(this.pg, this.sockets);
|
||||
|
@ -37,7 +38,8 @@ export default class Qualiteer {
|
|||
|
||||
async _connect() {
|
||||
await this.pg.connect();
|
||||
// await this.rabbiteer.connect();
|
||||
if (!rabbiteerEnabled) return;
|
||||
await this.rabbiteer.connect();
|
||||
}
|
||||
|
||||
start() {
|
||||
|
|
23
lib/database/delays.js
Normal file
23
lib/database/delays.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
const seconds = 1000;
|
||||
const minutes = 60 * seconds;
|
||||
const hours = 60 * minutes;
|
||||
export const DELAYS = {
|
||||
"1sec": 1 * seconds,
|
||||
"5sec": 5 * seconds,
|
||||
"10sec": 10 * seconds,
|
||||
"30sec": 30 * seconds,
|
||||
"1min": 1 * minutes,
|
||||
"5min": 5 * minutes,
|
||||
"10min": 10 * minutes,
|
||||
"15min": 15 * minutes,
|
||||
"30min": 30 * minutes,
|
||||
"1hour": 1 * hours,
|
||||
"2hour": 2 * hours,
|
||||
"3hour": 3 * hours,
|
||||
"4hour": 4 * hours,
|
||||
};
|
||||
|
||||
export default function getDelay(delayStr) {
|
||||
if (DELAYS[delayStr]) return DELAYS[delayStr];
|
||||
return 0;
|
||||
}
|
23
lib/database/migrations/1_create_catalog_table.sql
Normal file
23
lib/database/migrations/1_create_catalog_table.sql
Normal file
|
@ -0,0 +1,23 @@
|
|||
CREATE SEQUENCE catalog_id_seq;
|
||||
CREATE TABLE catalog (
|
||||
id bigint NOT NULL DEFAULT nextval('catalog_id_seq') PRIMARY KEY,
|
||||
name varchar(255) DEFAULT NULL,
|
||||
class varchar(255) DEFAULT NULL,
|
||||
image varchar(255) DEFAULT NULL,
|
||||
"path" varchar(255) DEFAULT NULL,
|
||||
description varchar(1023) DEFAULT NULL,
|
||||
type varchar(31) DEFAULT NULL,
|
||||
created TIMESTAMP NOT NULL DEFAULT now(),
|
||||
mr varchar(255) DEFAULT NULL,
|
||||
tags varchar(255)[] DEFAULT NULL,
|
||||
crons varchar(127) DEFAULT NULL,
|
||||
env varchar(31)[] DEFAULT NULL,
|
||||
regions varchar(15)[] DEFAULT NULL,
|
||||
triggers varchar(255)[] DEFAULT NULL,
|
||||
pipeline BOOLEAN DEFAULT FALSE,
|
||||
coverage varchar(255)[] DEFAULT NULL,
|
||||
projects varchar(255)[] DEFAULT NULL,
|
||||
delay varchar(31) DEFAULT NULL,
|
||||
CONSTRAINT unique_name UNIQUE(name)
|
||||
);
|
||||
ALTER SEQUENCE catalog_id_seq OWNED BY catalog.id;
|
|
@ -1,14 +0,0 @@
|
|||
CREATE SEQUENCE test_results_id_seq;
|
||||
CREATE TABLE test_results (
|
||||
id bigint NOT NULL DEFAULT nextval('test_results_id_seq') PRIMARY KEY,
|
||||
name varchar(255) DEFAULT NULL,
|
||||
"method" varchar(255) DEFAULT NULL,
|
||||
env varchar(31) DEFAULT NULL,
|
||||
"timestamp" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
retry BOOLEAN DEFAULT FALSE,
|
||||
failed BOOLEAN DEFAULT FALSE,
|
||||
failed_message varchar(2047) DEFAULT NULL,
|
||||
screenshot varchar(255) DEFAULT NULL,
|
||||
weblog varchar(255) DEFAULT NULL
|
||||
);
|
||||
ALTER SEQUENCE test_results_id_seq OWNED BY test_results.id;
|
|
@ -1,18 +0,0 @@
|
|||
CREATE SEQUENCE test_catalog_id_seq;
|
||||
CREATE TABLE test_catalog (
|
||||
id bigint NOT NULL DEFAULT nextval('test_catalog_id_seq') PRIMARY KEY,
|
||||
name varchar(255) DEFAULT NULL,
|
||||
class varchar(255) DEFAULT NULL,
|
||||
compound BOOLEAN DEFAULT FALSE,
|
||||
type varchar(31) DEFAULT NULL,
|
||||
markers varchar(255)[] DEFAULT NULL,
|
||||
ignored BOOLEAN DEFAULT FALSE,
|
||||
comment varchar(1023) DEFAULT NULL,
|
||||
coverage varchar(255)[] DEFAULT NULL,
|
||||
env varchar(31)[] DEFAULT NULL,
|
||||
"path" varchar(255) DEFAULT NULL,
|
||||
regions varchar(15)[] DEFAULT NULL,
|
||||
origin varchar(255) DEFAULT NULL,
|
||||
cron varchar(127) DEFAULT NULL
|
||||
);
|
||||
ALTER SEQUENCE test_catalog_id_seq OWNED BY test_catalog.id;
|
15
lib/database/migrations/2_create_results_table.sql
Normal file
15
lib/database/migrations/2_create_results_table.sql
Normal file
|
@ -0,0 +1,15 @@
|
|||
CREATE SEQUENCE results_id_seq;
|
||||
CREATE TABLE results (
|
||||
id bigint NOT NULL DEFAULT nextval('results_id_seq') PRIMARY KEY,
|
||||
name varchar(255) DEFAULT NULL,
|
||||
class varchar(255) DEFAULT NULL,
|
||||
"method" varchar(255) DEFAULT NULL,
|
||||
env varchar(31) DEFAULT NULL,
|
||||
"timestamp" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
triage BOOLEAN DEFAULT FALSE,
|
||||
failed BOOLEAN DEFAULT FALSE,
|
||||
message varchar(2047) DEFAULT NULL,
|
||||
screenshot varchar(255) DEFAULT NULL,
|
||||
console varchar(255) DEFAULT NULL
|
||||
);
|
||||
ALTER SEQUENCE results_id_seq OWNED BY results.id;
|
9
lib/database/migrations/3_create_alerting_table.sql
Normal file
9
lib/database/migrations/3_create_alerting_table.sql
Normal file
|
@ -0,0 +1,9 @@
|
|||
CREATE SEQUENCE alerting_id_seq;
|
||||
CREATE TABLE alerting (
|
||||
id bigint NOT NULL DEFAULT nextval('alerting_id_seq') PRIMARY KEY,
|
||||
name varchar(255) DEFAULT NULL,
|
||||
class varchar(255) DEFAULT NULL,
|
||||
"method" varchar(255) DEFAULT NULL,
|
||||
expires TIMESTAMP NOT NULL DEFAULT now()
|
||||
);
|
||||
ALTER SEQUENCE alerting_id_seq OWNED BY alerting.id;
|
|
@ -52,7 +52,8 @@ export const deleteQuery = (table, jsEntry) => {
|
|||
const conditionals = [];
|
||||
for (var col of cols) {
|
||||
entry[col] = buildPostgresValue(entry[col]);
|
||||
conditionals.push(`x.${col}=${entry[col]}`);
|
||||
if (entry[col] === "null") conditionals.push(`x.${col} IS NULL`);
|
||||
else conditionals.push(`x.${col}=${entry[col]}`);
|
||||
}
|
||||
return `DELETE FROM ${table} x WHERE ${conditionals.join(" AND ")}`;
|
||||
};
|
||||
|
|
|
@ -2,15 +2,15 @@
|
|||
import { migrate } from "postgres-migrations";
|
||||
import createPgp from "pg-promise";
|
||||
import moment from "moment";
|
||||
import { INFO, WARN } from "../util/logging.js";
|
||||
import { INFO, WARN, OK, VERB } from "../util/logging.js";
|
||||
// Environment Variables
|
||||
const {
|
||||
POSTGRES_DATABASE: database,
|
||||
POSTGRES_DISABLED: pgDisabled,
|
||||
POSTGRES_HOST: host,
|
||||
POSTGRES_PASSWORD: password,
|
||||
POSTGRES_PORT: port,
|
||||
POSTGRES_USER: user,
|
||||
QUALITEER_POSTGRES_DATABASE: database,
|
||||
QUALITEER_POSTGRES_ENABLED: pgEnabled,
|
||||
QUALITEER_POSTGRES_HOST: host,
|
||||
QUALITEER_POSTGRES_PASSWORD: password,
|
||||
QUALITEER_POSTGRES_PORT: port,
|
||||
QUALITEER_POSTGRES_USER: user,
|
||||
} = process.env;
|
||||
|
||||
// Postgres-promise Configuration
|
||||
|
@ -34,20 +34,26 @@ const migrationsDir = "lib/database/migrations";
|
|||
const queryMock = (str) => INFO("POSTGRES MOCK", str);
|
||||
|
||||
const connect = (pg) => async () => {
|
||||
if (pgDisabled) {
|
||||
if (pgEnabled === "false") {
|
||||
WARN("POSTGRES", "Postgres Disabled!");
|
||||
return { query: queryMock };
|
||||
}
|
||||
VERB("POSTGRES", "Migrating...");
|
||||
await migrate(dbConfig, migrationsDir);
|
||||
// Override the global variable DB
|
||||
pg = pgp(dbConfig);
|
||||
// Override fake methods
|
||||
const pgInstance = pgp(dbConfig);
|
||||
for (var k in pgInstance) pg[k] = pgInstance[k];
|
||||
VERB("POSTGRES", "Migrated Successfully");
|
||||
await pg.connect();
|
||||
OK("POSTGRES", `Connected to database ${database}!`);
|
||||
VERB("POSTGRES", "Postgres connected Successfully!");
|
||||
|
||||
OK("POSTGRES", `Connected to database ${dbConfig.database}!`);
|
||||
};
|
||||
|
||||
const buildPostgres = () => {
|
||||
var pg = { query: queryMock, connect: connect(pg) };
|
||||
var pg = { query: queryMock };
|
||||
pg.connect = connect(pg);
|
||||
return pg;
|
||||
};
|
||||
|
||||
export default buildPostgres;
|
||||
export default buildPostgres();
|
||||
|
|
|
@ -1,18 +1,56 @@
|
|||
import pg from "../postgres.js";
|
||||
import { silencedMock } from "../mocks/alerting-mock.js";
|
||||
import moment from "moment";
|
||||
// Imports
|
||||
import {
|
||||
insertQuery,
|
||||
selectWhereAnyQuery,
|
||||
updateWhereAnyQuery,
|
||||
deleteQuery,
|
||||
} from "../pg-query.js";
|
||||
// Constants
|
||||
const table = "silenced_tests";
|
||||
const table = "alerting";
|
||||
const PG_DISABLED = process.env.POSTGRES_DISABLED;
|
||||
|
||||
export const upsertAlertSilence = async (silence) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
class: className,
|
||||
method,
|
||||
expires: duration,
|
||||
keepExpires,
|
||||
} = silence;
|
||||
const { h, m } = duration;
|
||||
const expires = moment().add(h, "hours").add(m, "minutes").utc().format();
|
||||
const entry = {
|
||||
name,
|
||||
class: className,
|
||||
method,
|
||||
expires: keepExpires ? undefined : expires,
|
||||
};
|
||||
const asUpdate = {};
|
||||
for (var k of Object.keys(entry))
|
||||
asUpdate[k] = entry[k] === "*" ? null : entry[k];
|
||||
var query = id
|
||||
? updateWhereAnyQuery(table, asUpdate, { id })
|
||||
: insertQuery(table, entry);
|
||||
return pg.query(query);
|
||||
};
|
||||
|
||||
export const deleteAlertSilence = async (silence) => {
|
||||
const { id } = silence;
|
||||
const query = deleteQuery(table, { id });
|
||||
return pg.query(query);
|
||||
};
|
||||
|
||||
// Queries
|
||||
export const getSilencedTests = async () => {
|
||||
if (PG_DISABLED) return silencedMock();
|
||||
const query = `SELECT * from ${table}`;
|
||||
return pg.query(query);
|
||||
const silenced = await pg.query(query);
|
||||
silenced.forEach((t, i) => {
|
||||
for (var k of Object.keys(t)) silenced[i][k] = t[k] === null ? "*" : t[k];
|
||||
});
|
||||
return silenced;
|
||||
};
|
||||
|
|
|
@ -3,10 +3,13 @@ import pg from "../postgres.js";
|
|||
import {
|
||||
insertQuery,
|
||||
selectWhereAnyQuery,
|
||||
updateWhereAnyQuery,
|
||||
onConflictUpdate,
|
||||
} from "../pg-query.js";
|
||||
|
||||
import getFilteredTags from "../tags.js";
|
||||
import getDelay from "../delays.js";
|
||||
// Constants
|
||||
const table = "tests";
|
||||
const table = "catalog";
|
||||
const PG_DISABLED = process.env.POSTGRES_DISABLED;
|
||||
import { testsMock, mappingsMock } from "../mocks/catalog-mock.js";
|
||||
// Queries
|
||||
|
@ -19,8 +22,30 @@ export const getTests = async () => {
|
|||
|
||||
export const getPipelineMappings = async () => {
|
||||
if (PG_DISABLED) return mappingsMock();
|
||||
const query = `SELECT * from ${table}`;
|
||||
return pg.query(query);
|
||||
const query = `SELECT * from ${table} WHERE pipeline`;
|
||||
const tests = await pg.query(query);
|
||||
const mappings = [];
|
||||
var newTrigger;
|
||||
for (var test of tests) {
|
||||
if (test.triggers) continue;
|
||||
const { name, delay: delayStr } = test;
|
||||
var triggerStack = [{ name, delay: getDelay(delayStr), delayStr }];
|
||||
newTrigger = { name, delayStr };
|
||||
while (
|
||||
(newTrigger = tests.find(
|
||||
(te) => te.triggers && te.triggers.includes(newTrigger.name)
|
||||
)) !== null
|
||||
) {
|
||||
if (!newTrigger) break;
|
||||
triggerStack.push({
|
||||
name: newTrigger.name,
|
||||
delay: getDelay(newTrigger.delay),
|
||||
delayStr: newTrigger.delay,
|
||||
});
|
||||
}
|
||||
mappings.push(triggerStack.reverse());
|
||||
}
|
||||
return mappings;
|
||||
};
|
||||
|
||||
export const getProjects = async () => {
|
||||
|
@ -28,3 +53,47 @@ export const getProjects = async () => {
|
|||
const tests = testsMock();
|
||||
}
|
||||
};
|
||||
|
||||
export const upsertTest = async (test) => {
|
||||
if (PG_DISABLED) return console.log("Would insert test", test);
|
||||
const {
|
||||
name,
|
||||
class: className,
|
||||
image,
|
||||
path,
|
||||
description,
|
||||
type,
|
||||
created,
|
||||
mergeRequest,
|
||||
tags,
|
||||
} = test;
|
||||
|
||||
const filteredTags = getFilteredTags(tags);
|
||||
|
||||
const env =
|
||||
filteredTags.ignore && filteredTags.env
|
||||
? filteredTags.env.filter((e) => !filteredTags.ignore.includes(e))
|
||||
: filteredTags.env;
|
||||
const catalogEntry = {
|
||||
name,
|
||||
class: className,
|
||||
image,
|
||||
path,
|
||||
description: description ? description : null,
|
||||
type,
|
||||
created,
|
||||
mr: mergeRequest,
|
||||
tags,
|
||||
crons: filteredTags.crons,
|
||||
env,
|
||||
regions: filteredTags.regions,
|
||||
triggers: filteredTags.triggers,
|
||||
pipeline: filteredTags.pipeline ? true : false,
|
||||
coverage: filteredTags.coverage,
|
||||
projects: filteredTags.projects,
|
||||
delay: filteredTags.delay ? filteredTags.delay[0] : null,
|
||||
};
|
||||
const query =
|
||||
insertQuery(table, catalogEntry) + onConflictUpdate(["name"], catalogEntry);
|
||||
return await pg.query(query);
|
||||
};
|
||||
|
|
|
@ -8,41 +8,36 @@ import {
|
|||
updateWhereAnyQuery,
|
||||
} from "../pg-query.js";
|
||||
// Constants
|
||||
const table = "test_results";
|
||||
const table = "results";
|
||||
const recentResultsMax = 5;
|
||||
const PG_DISABLED = process.env.POSTGRES_DISABLED;
|
||||
|
||||
// Queries
|
||||
export const insertTestResult = (testResult) => {
|
||||
const {
|
||||
test_name,
|
||||
test_class,
|
||||
test_method,
|
||||
test_path,
|
||||
test_type,
|
||||
test_timestamp,
|
||||
test_retry,
|
||||
origin,
|
||||
name,
|
||||
class: className,
|
||||
method,
|
||||
env,
|
||||
timestamp,
|
||||
triage,
|
||||
failed,
|
||||
failed_message,
|
||||
screenshot_url,
|
||||
expected_screenshot_url,
|
||||
weblog_url,
|
||||
message,
|
||||
screenshot,
|
||||
console: cs,
|
||||
} = testResult;
|
||||
|
||||
var query = insertQuery(table, {
|
||||
test_name,
|
||||
test_class,
|
||||
test_method,
|
||||
test_path,
|
||||
test_type,
|
||||
test_timestamp,
|
||||
test_retry,
|
||||
origin,
|
||||
name,
|
||||
class: className,
|
||||
method,
|
||||
env,
|
||||
timestamp,
|
||||
triage,
|
||||
failed,
|
||||
failed_message,
|
||||
screenshot_url,
|
||||
expected_screenshot_url,
|
||||
weblog_url,
|
||||
message,
|
||||
screenshot,
|
||||
console: cs,
|
||||
});
|
||||
|
||||
query += "\n RETURNING *";
|
||||
|
@ -51,19 +46,48 @@ export const insertTestResult = (testResult) => {
|
|||
|
||||
export const getCurrentlyFailing = async () => {
|
||||
if (PG_DISABLED) return failingMock();
|
||||
/**/
|
||||
const query = `WITH recent as (SELECT * FROM test_results WHERE (timestamp BETWEEN NOW() - INTERVAL '24 HOURS' AND NOW()) AND NOT(failed AND retry)) SELECT * FROM recent WHERE timestamp = (SELECT MAX(timestamp) FROM recent r2 WHERE recent.name = r2.name) AND failed;
|
||||
`;
|
||||
return pg.query(query);
|
||||
/* This can probably be changed into a super query, but perhaps faster/smaller */
|
||||
const recent = `SELECT * FROM ${table} WHERE (timestamp BETWEEN NOW() - INTERVAL '24 HOURS' AND NOW()) AND NOT(failed AND triage)`;
|
||||
const slimCatalog = `SELECT name, crons, class, type, pipeline, env AS enabled_env FROM catalog`;
|
||||
const failing = `SELECT * FROM recent INNER JOIN slim_catalog USING(name) WHERE timestamp = (SELECT MAX(timestamp) FROM recent r2 WHERE recent.name = r2.name) AND failed`;
|
||||
const applicableFailing = `SELECT name, count(*) as fails FROM recent WHERE recent.name IN (SELECT name FROM failing) GROUP BY name`;
|
||||
/*const runHistory = `SELECT name, timestamp, failed FROM (SELECT *, ROW_NUMBER() OVER(PARTITION BY name ORDER BY timestamp) as n
|
||||
FROM ${table} WHERE name IN (SELECT name FROM failing)) as ord WHERE n <= ${recentResultsMax} ORDER BY name DESC`;*/
|
||||
const runHistory = `SELECT name, timestamp, failed FROM results WHERE NOT triage AND name IN (SELECT name FROM failing) ORDER BY timestamp DESC LIMIT ${recentResultsMax}`;
|
||||
// const recentQuery = pg.query(recent);
|
||||
const failingQuery = pg.query(
|
||||
`WITH recent as (${recent}), slim_catalog as (${slimCatalog}) ${failing}`
|
||||
);
|
||||
const applicableQuery = pg.query(
|
||||
`WITH recent as (${recent}), slim_catalog as (${slimCatalog}), failing as (${failing}) ${applicableFailing}`
|
||||
);
|
||||
const historyQuery = pg.query(
|
||||
`WITH recent as (${recent}), slim_catalog as (${slimCatalog}), failing as (${failing}) ${runHistory}`
|
||||
);
|
||||
|
||||
/*SELECT * FROM test_results WHERE "timestamp" BETWEEN NOW() - INTERVAL '24 HOURS' AND NOW(); <-- Last 24 hours all runs*/
|
||||
|
||||
/* SELECT * FROM test_results tr1 WHERE timestamp BETWEEN NOW() - INTERVAL '24 HOURS' AND NOW() AND timestamp = (SELECT MAX(timestamp) FROM test_results tr2 WHERE tr1.name = tr2.name); <-- Last 24 hours only most recent
|
||||
*/
|
||||
const [currentlyFailing, applicableFails, failHistory] = await Promise.all([
|
||||
failingQuery,
|
||||
applicableQuery,
|
||||
historyQuery,
|
||||
]);
|
||||
for (var i = 0; i < currentlyFailing.length; i++) {
|
||||
currentlyFailing[i].dailyFails = parseInt(
|
||||
applicableFails.find((af) => af.name === currentlyFailing[i].name).fails
|
||||
);
|
||||
currentlyFailing[i].recentResults = [];
|
||||
currentlyFailing[i].enabledEnv = currentlyFailing[i].enabled_env;
|
||||
currentlyFailing[i].isPipeline = currentlyFailing[i].pipeline;
|
||||
delete currentlyFailing[i].enabled_env;
|
||||
delete currentlyFailing[i].pipeline;
|
||||
for (var fh of failHistory) {
|
||||
if (fh.name !== currentlyFailing[i].name) continue;
|
||||
currentlyFailing[i].recentResults.push(fh);
|
||||
}
|
||||
}
|
||||
return currentlyFailing;
|
||||
};
|
||||
|
||||
export const getCurrentlyFailingFull = (env) => {
|
||||
const query = `WITH recent AS (SELECT * FROM test_results WHERE (timestamp BETWEEN NOW() - INTERVAL '24 HOURS' AND NOW()) AND NOT(failed AND retry) AND env='prod') SELECT * FROM recent INNER JOIN test_catalog USING(name) WHERE timestamp = (SELECT MAX(timestamp) FROM recent r2 WHERE recent.name = r2.name) AND failed;
|
||||
;`;
|
||||
export const ignoreResult = async ({ id }) => {
|
||||
const query = updateWhereAnyQuery(table, { failed: false }, { id });
|
||||
return pg.query(query);
|
||||
};
|
||||
|
|
30
lib/database/seed.js
Normal file
30
lib/database/seed.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import pg from "./postgres.js";
|
||||
import { upsertTest } from "./queries/catalog.js";
|
||||
import { insertTestResult } from "./queries/results.js";
|
||||
import { upsertAlertSilence } from "./queries/alerting.js";
|
||||
import {
|
||||
seed as catalogSeed,
|
||||
table as catalogTable,
|
||||
} from "./seeds/catalog-seed.js";
|
||||
import {
|
||||
seed as resultsSeed,
|
||||
table as resultsTable,
|
||||
} from "./seeds/results-seed.js";
|
||||
import {
|
||||
seed as alertingSeed,
|
||||
table as alertingTable,
|
||||
} from "./seeds/alerting-seed.js";
|
||||
|
||||
const database = process.env.POSTGRES_DATABASE ?? "qualiteer";
|
||||
await pg.connect();
|
||||
|
||||
const resetAndSeed = async (table, getSeeds, seed) => {
|
||||
await pg.query(`TRUNCATE ${table} RESTART IDENTITY CASCADE;`);
|
||||
for (var s of getSeeds()) await seed(s);
|
||||
};
|
||||
|
||||
await resetAndSeed(catalogTable, catalogSeed, upsertTest);
|
||||
await resetAndSeed(resultsTable, resultsSeed, insertTestResult);
|
||||
await resetAndSeed(alertingTable, alertingSeed, upsertAlertSilence);
|
||||
|
||||
process.exit();
|
11
lib/database/seeds/alerting-seed.js
Normal file
11
lib/database/seeds/alerting-seed.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export const table = "alerting";
|
||||
export const seed = () => {
|
||||
return [
|
||||
{
|
||||
name: `failing`,
|
||||
class: `failing.js`,
|
||||
method: "FAKEMETHOD",
|
||||
expires: new Date().toJSON(),
|
||||
},
|
||||
];
|
||||
};
|
126
lib/database/seeds/catalog-seed.js
Normal file
126
lib/database/seeds/catalog-seed.js
Normal file
|
@ -0,0 +1,126 @@
|
|||
export const table = "catalog";
|
||||
export const seed = () => {
|
||||
return [
|
||||
{
|
||||
name: "single",
|
||||
class: "single.js",
|
||||
image: "node:latest",
|
||||
path: "tests/assets/suite/single.js",
|
||||
description: "This is a single test",
|
||||
type: "api",
|
||||
created: new Date().toJSON(),
|
||||
mergeRequest: "https://example.com",
|
||||
tags: ["cron_1hour", "reg_us", "env_ci", "proj_core", "ignore_alt"],
|
||||
},
|
||||
{
|
||||
name: "failing",
|
||||
class: "failing.js",
|
||||
image: "node:latest",
|
||||
path: "tests/assets/suite/failing.js",
|
||||
description: "This is a failing test",
|
||||
type: "ui",
|
||||
created: new Date().toJSON(),
|
||||
mergeRequest: "https://example.com",
|
||||
tags: ["cron_1hour", "reg_us", "env_ci", "proj_core"],
|
||||
},
|
||||
{
|
||||
name: "primary",
|
||||
class: "primary.js",
|
||||
image: "node:latest",
|
||||
path: "tests/assets/suite/primary.js",
|
||||
description: "This is a primary test",
|
||||
type: "api",
|
||||
created: new Date().toJSON(),
|
||||
mergeRequest: "https://example.com",
|
||||
tags: [
|
||||
"pipeline",
|
||||
"cron_1hour",
|
||||
"reg_us",
|
||||
"proj_core",
|
||||
"ignore_alt",
|
||||
"triggers_secondary1",
|
||||
"triggers_secondary2",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "secondary1",
|
||||
class: "secondary1.js",
|
||||
image: "node:latest",
|
||||
path: "tests/assets/suite/secondary1.js",
|
||||
description: "This is a secondary test",
|
||||
type: "api",
|
||||
created: new Date().toJSON(),
|
||||
mergeRequest: "https://example.com",
|
||||
tags: [
|
||||
"pipeline",
|
||||
"cron_1hour",
|
||||
"reg_us",
|
||||
"proj_core",
|
||||
"triggers_tertiary1",
|
||||
"triggers_tertiary2",
|
||||
"delay_1sec",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "secondary2",
|
||||
class: "secondary2.js",
|
||||
image: "node:latest",
|
||||
path: "tests/assets/suite/secondary2.js",
|
||||
description: "This is a secondary2 test",
|
||||
type: "api",
|
||||
created: new Date().toJSON(),
|
||||
mergeRequest: "https://example.com",
|
||||
tags: [
|
||||
"pipeline",
|
||||
"cron_1hour",
|
||||
"reg_us",
|
||||
"proj_core",
|
||||
"triggers_tertiary3",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "tertiary1",
|
||||
class: "tertiary1.js",
|
||||
image: "node:latest",
|
||||
path: "tests/assets/suite/tertiary1.js",
|
||||
description: "This is a third test",
|
||||
type: "api",
|
||||
created: new Date().toJSON(),
|
||||
mergeRequest: "https://example.com",
|
||||
tags: ["pipeline", "cron_1hour", "reg_us", "proj_core"],
|
||||
},
|
||||
{
|
||||
name: "tertiary2",
|
||||
class: "tertiary2.js",
|
||||
image: "node:latest",
|
||||
path: "tests/assets/suite/tertiary2.js",
|
||||
description: "This is a third2 test",
|
||||
type: "api",
|
||||
created: new Date().toJSON(),
|
||||
mergeRequest: "https://example.com",
|
||||
tags: ["pipeline", "cron_1hour", "reg_us", "proj_core", "delay_10sec"],
|
||||
},
|
||||
{
|
||||
name: "tertiary3",
|
||||
class: "tertiary3.js",
|
||||
image: "node:latest",
|
||||
path: "tests/assets/suite/tertiary3.js",
|
||||
description: "This is a third3 test",
|
||||
type: "api",
|
||||
created: new Date().toJSON(),
|
||||
mergeRequest: "https://example.com",
|
||||
tags: ["pipeline", "cron_1hour", "reg_us", "proj_core", "delay_5sec"],
|
||||
},
|
||||
{
|
||||
name: "single-alt",
|
||||
class: "single-alt.js",
|
||||
image: "node:latest",
|
||||
path: "tests/assets/suite/single-alt.js",
|
||||
description: "This is an alternative test",
|
||||
type: "ui",
|
||||
created: new Date().toJSON(),
|
||||
mergeRequest: "https://example.com",
|
||||
tags: ["cron_1hour", "reg_us", "env_ci", "proj_alt"],
|
||||
},
|
||||
];
|
||||
};
|
29
lib/database/seeds/results-seed.js
Normal file
29
lib/database/seeds/results-seed.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
export const table = "results";
|
||||
export const seed = () => {
|
||||
return [
|
||||
{
|
||||
name: "failing",
|
||||
class: "failing.js",
|
||||
method: "FAKEMETHOD",
|
||||
env: "prod",
|
||||
timestamp: new Date().toJSON(),
|
||||
triage: false,
|
||||
failed: true,
|
||||
message: "Some Test FailureMessage",
|
||||
screenshot: "https://picsum.photos/1920/1080",
|
||||
console: "https://example.com",
|
||||
},
|
||||
{
|
||||
name: "secondary1",
|
||||
class: "secondary1.js",
|
||||
method: "FAKEMETHOD",
|
||||
env: "prod",
|
||||
timestamp: new Date().toJSON(),
|
||||
triage: false,
|
||||
failed: true,
|
||||
message: "Some Test FailureMessage from Secondary1",
|
||||
screenshot: "https://picsum.photos/1920/1080",
|
||||
console: "https://example.com",
|
||||
},
|
||||
];
|
||||
};
|
26
lib/database/tags.js
Normal file
26
lib/database/tags.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { WARN } from "../util/logging.js";
|
||||
export const TAGS = {
|
||||
IGNORE: { name: "ignore", tag: "ignore_", value: (t) => t },
|
||||
CRON: { name: "crons", tag: "cron_", value: (t) => t },
|
||||
ENV: { name: "env", tag: "env_", value: (t) => t },
|
||||
REGIONS: { name: "regions", tag: "reg_", value: (t) => t },
|
||||
PIPELINE: { name: "pipeline", tag: "pipeline", value: (t) => t },
|
||||
COVERAGE: { name: "coverage", tag: "coverage_", value: (t) => t },
|
||||
PROJECT: { name: "projects", tag: "proj_", value: (t) => t },
|
||||
DELAY: { name: "delay", tag: "delay_", value: (t) => t },
|
||||
TRIGGERS: { name: "triggers", tag: "triggers_", value: (t) => t },
|
||||
};
|
||||
|
||||
export default function getFilteredTags(tags) {
|
||||
const filtered = {};
|
||||
for (var t of tags) {
|
||||
const tag = Object.values(TAGS).find((ta) => t.startsWith(ta.tag));
|
||||
if (!tag) {
|
||||
WARN("CATALOG", `Tag '${t}' did not have a valid prefix!`);
|
||||
continue;
|
||||
}
|
||||
if (!filtered[tag.name]) filtered[tag.name] = [];
|
||||
filtered[tag.name].push(tag.value(t.replace(tag.tag, "")));
|
||||
}
|
||||
return filtered;
|
||||
}
|
|
@ -27,6 +27,8 @@ class JobManager {
|
|||
|
||||
pushLog(jobId, log) {
|
||||
const job = this.getJobById(jobId);
|
||||
if (!job) return;
|
||||
|
||||
if (log instanceof Array) job.log.push(...log);
|
||||
else job.log.push(log);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
const baseCommand = "node";
|
||||
const suiteEntry = "tests/assets/suite/runner.js";
|
||||
const pipelineMapping = [
|
||||
{
|
||||
id: 0,
|
||||
pipeline: [{ name: "primary" }, { name: "secondary", delay: 5000 }],
|
||||
},
|
||||
];
|
||||
|
||||
const buildCommon = (jobRequest) => {
|
||||
const { isTriage, ignore, region, testNames } = jobRequest;
|
||||
|
@ -25,7 +19,6 @@ const buildManual = (jobReq) => {
|
|||
throw Error("Currently only 1 test can be selected!");
|
||||
|
||||
command.push(`test=${testNames[0]}`);
|
||||
|
||||
return { ...jobReq, command };
|
||||
};
|
||||
|
||||
|
@ -51,7 +44,6 @@ const buildPipeline = (jobReq, socketId) => {
|
|||
};
|
||||
|
||||
export default function jobBuilder(jobRequest, id) {
|
||||
console.log(jobRequest);
|
||||
const jobReq = buildCommon(jobRequest, id);
|
||||
const { pipeline, testNames, tags } = jobReq;
|
||||
if (pipeline) return buildPipeline(jobReq, id);
|
||||
|
|
|
@ -5,22 +5,13 @@
|
|||
"name": "qltr-job-test-suite-1"
|
||||
},
|
||||
"spec": {
|
||||
"ttlSecondsAfterFinished": 2,
|
||||
"template": {
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"resources": {
|
||||
"requests": {
|
||||
"memory": "64MI",
|
||||
"cpu": "250m"
|
||||
},
|
||||
"limits": {
|
||||
"memory": "128MI",
|
||||
"cpu": "500m"
|
||||
}
|
||||
},
|
||||
"name": "qltr-job-test-suite-1",
|
||||
"image": "node",
|
||||
"image": "node:latest",
|
||||
"imagePullPolicy": "Always",
|
||||
"command": ["node", "--version"]
|
||||
}
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
import cp from "node:child_process";
|
||||
import k8s from "@kubernetes/client-node";
|
||||
import { INFO, ERR } from "../../util/logging.js";
|
||||
import { jobBuilder, createFile, deleteFile } from "./k8s-common.js";
|
||||
|
||||
const applyFile = async (filePath) => {
|
||||
const command = `kubectl apply -f ${filePath}`;
|
||||
return new Promise((res, rej) =>
|
||||
cp.exec(command, (err, stdout, stderr) => (err && rej(err)) || res(stdout))
|
||||
);
|
||||
};
|
||||
|
||||
export default async function createJob(jobRequest) {
|
||||
const job = jobBuilder(jobRequest);
|
||||
const filePath = createFile(job);
|
||||
job.spec.template.spec.containers[0].image = "node:latest";
|
||||
job.spec.template.spec.containers[0].command = ["node", "--version"];
|
||||
// job.spec.template.spec.containers[0].image = "reed";
|
||||
// job.spec.template.spec.containers[0].command = "python3 -m pytest -v --tb=no -p no:warnings".split(" ");
|
||||
const kc = new k8s.KubeConfig();
|
||||
kc.loadFromCluster();
|
||||
const batchV1Api = kc.makeApiClient(k8s.BatchV1Api);
|
||||
const batchV1beta1Api = kc.makeApiClient(k8s.BatchV1beta1Api);
|
||||
const jobName = job.metadata.name;
|
||||
batchV1Api
|
||||
.createNamespacedJob("dunestorm-dunemask", job)
|
||||
.then((res) => INFO("K8S", `Job ${jobName} created!`))
|
||||
.catch((err) => ERR("K8S", err));
|
||||
|
||||
/*const filePath = createFile(job);
|
||||
applyFile(filePath);
|
||||
deleteFile(filePath);
|
||||
deleteFile(filePath);*/
|
||||
}
|
||||
|
|
|
@ -2,16 +2,21 @@ import Rabbiteer from "rabbiteer";
|
|||
import buildWorkers from "./workers/index.js";
|
||||
|
||||
// Pull Environment Variables
|
||||
const { RABBIT_HOST: host, RABBIT_USER: user, RABBIT_PASS: pass } = process.env;
|
||||
const {
|
||||
QUALITEER_RABBIT_HOST: host,
|
||||
QUALITEER_RABBIT_USER: user,
|
||||
QUALITEER_RABBIT_PASS: pass,
|
||||
} = process.env;
|
||||
|
||||
// Rabbit Config
|
||||
const rabbitConfig = {
|
||||
host: host ?? "localhost",
|
||||
user: user ?? "rabbit",
|
||||
pass: pass ?? "rabbit",
|
||||
protocol: "amqp:",
|
||||
host: `amqp://${host}` ?? "localhost",
|
||||
user: user ?? "guest",
|
||||
pass: pass ?? "guest",
|
||||
};
|
||||
|
||||
const buildRabbiteer = (skio) =>
|
||||
const buildRabbiteer = (pg, skio) =>
|
||||
new Rabbiteer(null, buildWorkers(skio), { autoRabbit: rabbitConfig });
|
||||
|
||||
export default buildRabbiteer;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// Imports
|
||||
import { Worker } from "rabbiteer";
|
||||
import { VERB } from "../../util/logging.js";
|
||||
import { insertTestResult } from "../../database/queries/results.js";
|
||||
import evt from "../../sockets/events.js";
|
||||
// Class
|
||||
export default class TestResultsWorker extends Worker {
|
||||
|
@ -23,9 +25,9 @@ export default class TestResultsWorker extends Worker {
|
|||
consoleLogUrl: “https://consolelog”
|
||||
}
|
||||
*/
|
||||
onMessage(testResult) {
|
||||
async onMessage(testResult) {
|
||||
const { pipeline } = testResult;
|
||||
|
||||
await this.handleReporting(testResult);
|
||||
// Alter to start next test
|
||||
// TODO the delay should be autopopulated either by the suite, or filled in by the server
|
||||
if (pipeline) return this.pipelineTrigger(pipeline);
|
||||
|
@ -35,4 +37,9 @@ export default class TestResultsWorker extends Worker {
|
|||
const { dashboardSocketId: dsi } = pipeline;
|
||||
this.skio.to(dsi).emit(evt.PPL_TRG, pipeline);
|
||||
}
|
||||
|
||||
handleReporting(result) {
|
||||
VERB("TestResults", result.name);
|
||||
insertTestResult(result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { Router, json as jsonMiddleware } from "express";
|
||||
import { getSilencedTests } from "../database/queries/alerting.js";
|
||||
import {
|
||||
getSilencedTests,
|
||||
upsertAlertSilence,
|
||||
deleteAlertSilence,
|
||||
} from "../database/queries/alerting.js";
|
||||
const router = Router();
|
||||
|
||||
// Apply Middlewares
|
||||
|
@ -12,7 +16,26 @@ router.get("/silenced", (req, res) => {
|
|||
|
||||
// Post Routes
|
||||
router.post("/silence", (req, res) => {
|
||||
res.sendStatus(200);
|
||||
const { name, class: className, method, expires, keepExpires } = req.body;
|
||||
if (!name || !className || !method)
|
||||
return res
|
||||
.status(400)
|
||||
.send("'name', 'class', and 'method' are all required Fields!");
|
||||
if (expires === null)
|
||||
return deleteAlertSilence(req.body)
|
||||
.then(() => res.sendStatus(200))
|
||||
.catch((e) => res.status(500).send(e));
|
||||
const { h, m } = keepExpires ? {} : expires;
|
||||
if (!keepExpires && (h == null || m == null))
|
||||
return res.status(400).send("Both 'h' and 'm' are required fields!");
|
||||
if (!keepExpires && (h < 0 || m < 0))
|
||||
return res
|
||||
.status(400)
|
||||
.send("'h' and 'm' must be greater than or equal to 0!");
|
||||
// TODO set max times as well
|
||||
if (!keepExpires && (h > 72 || m > 59))
|
||||
res.status(400).send("'h' and 'm' must not exceed the set maxes!");
|
||||
upsertAlertSilence(req.body).then(() => res.sendStatus(200));
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { Router, json as jsonMiddleware } from "express";
|
||||
import { getTests, getPipelineMappings } from "../database/queries/catalog.js";
|
||||
import {
|
||||
getTests,
|
||||
getPipelineMappings,
|
||||
upsertTest,
|
||||
} from "../database/queries/catalog.js";
|
||||
const router = Router();
|
||||
|
||||
const maxSize = 1024 * 1024 * 100; // 100MB
|
||||
|
@ -18,8 +22,10 @@ router.get("/pipeline-mappings", (req, res) => {
|
|||
|
||||
// Post Routes
|
||||
router.post("/update", (req, res) => {
|
||||
// Update All Tests
|
||||
res.sendStatus(200);
|
||||
if (!req.body) return res.status(400).send("Body required!");
|
||||
if(!Array.isArray(req.body)) return res.status(400).send("Body must be an array!");
|
||||
const upserts = Promise.all(req.body.map((catalogItem)=>upsertTest(catalogItem)));
|
||||
upserts.then(()=>res.sendStatus(200)).catch((e)=>res.status(500).send(e));
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { Router, json as jsonMiddleware } from "express";
|
||||
import { getCurrentlyFailing } from "../database/queries/results.js";
|
||||
import {
|
||||
getCurrentlyFailing,
|
||||
ignoreResult,
|
||||
} from "../database/queries/results.js";
|
||||
const router = Router();
|
||||
|
||||
// Apply Middlewares
|
||||
|
@ -15,4 +18,10 @@ router.post("/history", (req, res) => {
|
|||
res.send([]);
|
||||
});
|
||||
|
||||
router.post("/ignore", (req, res) => {
|
||||
if (!req.body || !req.body.id)
|
||||
return res.status(400).send("'id' is required!");
|
||||
ignoreResult(req.body).then(() => res.sendStatus(200));
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import express from "express";
|
||||
|
||||
// Routes
|
||||
import vitals from "../routes/vitals-route.js";
|
||||
import results from "../routes/results-route.js";
|
||||
import alerting from "../routes/alerting-route.js";
|
||||
import react from "../routes/react-route.js";
|
||||
|
@ -12,6 +13,7 @@ import buildDevRoute from "../routes/dev-route.js";
|
|||
export default function buildRoutes(pg, skio) {
|
||||
const router = express.Router();
|
||||
// Special Routes
|
||||
router.use(vitals);
|
||||
router.all("/", (req, res) => res.redirect("/qualiteer"));
|
||||
if (process.env.USE_DEV_ROUTER === "true")
|
||||
router.use("/api/dev", buildDevRoute(pg, skio));
|
||||
|
|
7
lib/routes/vitals-route.js
Normal file
7
lib/routes/vitals-route.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { Router } from "express";
|
||||
const router = Router();
|
||||
|
||||
// Get Routes
|
||||
router.get("/healthz", (req, res) => res.sendStatus(200));
|
||||
|
||||
export default router;
|
|
@ -13,7 +13,7 @@ const OUT = "o";
|
|||
|
||||
export default class Executor {
|
||||
constructor(config, payload) {
|
||||
this.url = config.url(payload) ?? process.env.QUALITEER_URL;
|
||||
this.url = config.url(payload) ?? process.env.QUALITEER_EXECUTOR_URL;
|
||||
this.jobId = config.jobId(payload) ?? process.env.QUALITEER_JOB_ID;
|
||||
this.command = config.command(payload) ?? process.env.QUALITEER_COMMAND;
|
||||
this.mode = modes.EXEC;
|
||||
|
|
5643
package-lock.json
generated
5643
package-lock.json
generated
File diff suppressed because it is too large
Load diff
59
package.json
59
package.json
|
@ -14,18 +14,18 @@
|
|||
"qualiteer": "./dist/app.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build:all": "npm run build:react && npm run build:executor",
|
||||
"build:all": "concurrently \"npm run build:react\" \"npm run build:executor\" -n v,s -p -c yellow,green",
|
||||
"build:executor": "node lib/jobs/executor/executor-bundler.js",
|
||||
"build:react": "vite build",
|
||||
"start": "node dist/app.js",
|
||||
"start:dev": "nodemon dist/app.js",
|
||||
"start:dev:replit": "npm run start:react:replit & (sleep 30 && npm run start:dev)",
|
||||
"start:react": "vite preview",
|
||||
"start:react:replit": "vite --host",
|
||||
"dev:server": "nodemon dist/app.js",
|
||||
"dev:react": "vite",
|
||||
"start:dev": "concurrently -k \"QUALITEER_DEV_PORT=52025 npm run dev:server\" \" QUALITEER_VITE_DEV_PORT=52000 QUALITEER_VITE_BACKEND_URL=http://localhost:52025 npm run dev:react\" -n s,v -p -c green,yellow",
|
||||
"test": "node tests/index.js",
|
||||
"test:api": "node tests/api.js",
|
||||
"test:dev": "nodemon tests/index.js",
|
||||
"lint": "prettier -w lib/ src/"
|
||||
"lint": "prettier -w lib/ src/",
|
||||
"seed": "node lib/database/seed.js"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
@ -40,44 +40,45 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"amqplib": "^0.8.0",
|
||||
"@kubernetes/client-node": "^0.17.0",
|
||||
"amqplib": "^0.10.3",
|
||||
"chalk": "^5.0.1",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.17.2",
|
||||
"dotenv": "^16.0.2",
|
||||
"express": "^4.18.1",
|
||||
"figlet": "^1.5.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.3",
|
||||
"moment": "^2.29.4",
|
||||
"path": "^0.12.7",
|
||||
"pg-promise": "^10.11.1",
|
||||
"pg-promise": "^10.12.0",
|
||||
"postgres-migrations": "^5.3.0",
|
||||
"socket.io": "^4.4.1",
|
||||
"socket.io-client": "^4.4.1",
|
||||
"uuid": "^8.3.2"
|
||||
"socket.io": "^4.5.2",
|
||||
"socket.io-client": "^4.5.2",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emotion/react": "^11.9.0",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mui/icons-material": "^5.6.2",
|
||||
"@mui/material": "^5.6.4",
|
||||
"@rollup/plugin-commonjs": "^22.0.0",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@mui/icons-material": "^5.10.3",
|
||||
"@mui/material": "^5.10.4",
|
||||
"@rollup/plugin-commonjs": "^22.0.2",
|
||||
"@rollup/plugin-node-resolve": "^14.0.0",
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@tanstack/react-query": "^4.0.0",
|
||||
"@vitejs/plugin-react": "1.1.1",
|
||||
"@tanstack/react-query": "^4.2.3",
|
||||
"@vitejs/plugin-react": "2.1.0",
|
||||
"axios": "^0.27.2",
|
||||
"caxa": "^2.1.0",
|
||||
"nodemon": "^2.0.15",
|
||||
"concurrently": "^7.3.0",
|
||||
"nodemon": "^2.0.19",
|
||||
"prettier": "^2.7.1",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"readline-sync": "^1.4.10",
|
||||
"rollup": "^2.72.0",
|
||||
"rollup": "^2.79.0",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"vite": "2.7.0"
|
||||
"vite": "3.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"rabbiteer": "gitlab:Dunemask/rabbiteer"
|
||||
},
|
||||
"proxy": "http://localhost:52000/"
|
||||
}
|
||||
}
|
||||
|
|
10
replit.nix
10
replit.nix
|
@ -1,10 +0,0 @@
|
|||
{ pkgs }: {
|
||||
deps = [
|
||||
pkgs.cloc
|
||||
pkgs.nodejs-16_x
|
||||
pkgs.nodePackages.typescript-language-server
|
||||
pkgs.nodePackages.yarn
|
||||
pkgs.replitPackages.jest
|
||||
pkgs.vim
|
||||
];
|
||||
}
|
|
@ -18,7 +18,7 @@ const ACTIONS = {
|
|||
PIPELINE: "p",
|
||||
};
|
||||
|
||||
const url = "https://qualiteer.elijahparker3.repl.co/";
|
||||
const url = "/";
|
||||
|
||||
const initialState = {
|
||||
jobs: [],
|
||||
|
@ -134,7 +134,7 @@ export const JobProvider = ({ children }) => {
|
|||
request,
|
||||
onLog,
|
||||
onClose,
|
||||
null,
|
||||
() => {},
|
||||
onPipelineTrigger
|
||||
);
|
||||
started.then(() => jobUpdate({ status: jobStatus.ACTIVE }, jobId));
|
||||
|
@ -146,6 +146,7 @@ export const JobProvider = ({ children }) => {
|
|||
const pipelineReq = {
|
||||
image: "node",
|
||||
pipeline: { __test, triggers: { ...tree[__test] } },
|
||||
isTriage: builderCache.triageFailing,
|
||||
};
|
||||
const id = `pij${Date.now()}`;
|
||||
const pipeline = { id, branches, pendingTriggers: [], selectedBranches };
|
||||
|
@ -232,6 +233,7 @@ export const JobProvider = ({ children }) => {
|
|||
image: "node",
|
||||
type: "single",
|
||||
name: jobId,
|
||||
isTriage: builderCache.isTriage,
|
||||
};
|
||||
|
||||
jobCreate(job);
|
||||
|
@ -250,7 +252,7 @@ export const JobProvider = ({ children }) => {
|
|||
jobUpdate({ ...job }, jobId);
|
||||
};
|
||||
|
||||
const started = i.newJob(request, onLog, onClose);
|
||||
const started = i.newJob(request, onLog, onClose, () => {});
|
||||
started.then(() => jobUpdate({ status: jobStatus.ACTIVE }, jobId));
|
||||
|
||||
return jobId;
|
||||
|
|
|
@ -5,7 +5,7 @@ const ACTIONS = {
|
|||
UPDATE: "u",
|
||||
};
|
||||
|
||||
const localStorage = { setItem: () => {}, getItem: () => {} };
|
||||
// const localStorage = { setItem: () => {}, getItem: () => {} };
|
||||
|
||||
const localSettings = localStorage.getItem("settings");
|
||||
const defaultSettings = {
|
||||
|
@ -14,6 +14,7 @@ const defaultSettings = {
|
|||
logAppDetails: true,
|
||||
defaultRegion: "us",
|
||||
defaultPage: "failing",
|
||||
triageFailing: true,
|
||||
};
|
||||
|
||||
const settings = localSettings ? JSON.parse(localSettings) : defaultSettings;
|
||||
|
@ -58,7 +59,7 @@ export const StoreProvider = ({ children }) => {
|
|||
name: silenceInfo.name ?? "*",
|
||||
class: silenceInfo.class ?? "*",
|
||||
method: silenceInfo.method ?? "*",
|
||||
silencedUntil: silenceInfo.silencedUntil,
|
||||
expires: silenceInfo.expires,
|
||||
};
|
||||
console.log("Would upsert silence", req);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { silencedMock } from "@qltr/mocks/alerting-mock.js";
|
||||
import { failingMock } from "@qltr/mocks/results-mock.js";
|
||||
import { testsMock, mappingsMock } from "@qltr/mocks/catalog-mock.js";
|
||||
|
||||
const QUALITEER_URL = "https://qualiteer.elijahparker3.repl.co/api";
|
||||
const QUALITEER_URL = "/api";
|
||||
|
||||
const useMock = true;
|
||||
const useMock = false;
|
||||
|
||||
const asMock = (data) => ({ data });
|
||||
|
||||
|
@ -31,3 +31,44 @@ export const useCurrentlyFailing = () =>
|
|||
useMock
|
||||
? asMock(failingMock())
|
||||
: useQuery(["failing"], fetchApi("/results/failing"));
|
||||
|
||||
export const useUpsertAlert = () => {
|
||||
const qc = useQueryClient();
|
||||
return async function upsertAlert(silenceRequest) {
|
||||
var { id, name, class: className, method, expires } = silenceRequest;
|
||||
const keepExpires = typeof expires === "string";
|
||||
name = name ? name : "*";
|
||||
className = className ? className : "*";
|
||||
method = method ? method : "*";
|
||||
const res = await fetch(`${QUALITEER_URL}/alerting/silence`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
name,
|
||||
class: className,
|
||||
method,
|
||||
expires,
|
||||
keepExpires,
|
||||
}),
|
||||
});
|
||||
qc.invalidateQueries(["silenced"]);
|
||||
};
|
||||
};
|
||||
|
||||
export const useIgnoreResult = () => {
|
||||
const qc = useQueryClient();
|
||||
return async function ignoreResult(result) {
|
||||
const { id } = result;
|
||||
const res = await fetch(`${QUALITEER_URL}/results/ignore`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ id }),
|
||||
});
|
||||
qc.invalidateQueries(["failing"]);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ export default function About() {
|
|||
</Typography>
|
||||
<Typography variant="body1">
|
||||
Qualiteer was designed to solve the issue of "on call". A state of
|
||||
being in which QA tests will fail, stiring everyone into a frenzy of
|
||||
being in which QA tests will fail, stirring everyone into a frenzy of
|
||||
what is broken in production! 🤯 Qualiteer gives users power to
|
||||
resolve and reattempt failing tests, run a particular suite of tests,
|
||||
and mute pesky alerts reminding you the navbar's color changed... 🤦♂️
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useState, useContext } from "react";
|
||||
import { useSilencedAlerts } from "@qltr/queries";
|
||||
import { useSilencedAlerts, useUpsertAlert } from "@qltr/queries";
|
||||
import StoreContext from "@qltr/store";
|
||||
import SilencedBox from "./SilencedBox.jsx";
|
||||
import SilenceDialog from "./SilenceDialog.jsx";
|
||||
import SilenceDialog, { useSilenceDialog } from "./SilenceDialog.jsx";
|
||||
|
||||
import SpeedDial from "@mui/material/SpeedDial";
|
||||
import SpeedDialAction from "@mui/material/SpeedDialAction";
|
||||
|
@ -18,41 +18,38 @@ import Typography from "@mui/material/Typography";
|
|||
import Box from "@mui/material/Box";
|
||||
|
||||
export default function Alerting() {
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const { updateStore, silenceRequest } = useContext(StoreContext);
|
||||
|
||||
const { isLoading, data: silenced } = useSilencedAlerts();
|
||||
const upsertAlert = useUpsertAlert();
|
||||
const [sdOpen, sdToggle, silence, setSilence, sdClose] = useSilenceDialog();
|
||||
const toggleDelete = () => setDeleteOpen(!deleteOpen);
|
||||
|
||||
const [silenceEntry, setSilenceEntry] = useState({
|
||||
open: false,
|
||||
deleteOpen: false,
|
||||
});
|
||||
|
||||
const closeSilence = () =>
|
||||
setSilenceEntry({ ...silenceEntry, open: false, deleteOpen: false });
|
||||
|
||||
const handleDeleteClose = (makeRequest) => () => {
|
||||
const silenceReq = { ...silenceEntry };
|
||||
closeSilence();
|
||||
if (!makeRequest) return;
|
||||
silenceRequest({ ...silenceReq, silencedUntil: null });
|
||||
const handleDeleteClose = (confirmed) => () => {
|
||||
toggleDelete();
|
||||
if (!confirmed) return;
|
||||
const silenceReq = { ...silence };
|
||||
upsertAlert({ ...silenceReq, expires: null });
|
||||
};
|
||||
|
||||
const handleClose = (silenceReq) => {
|
||||
closeSilence();
|
||||
if (!silenceReq) return;
|
||||
silenceRequest(silenceReq);
|
||||
upsertAlert(silenceReq);
|
||||
};
|
||||
|
||||
const quickAlertClick = () => {
|
||||
setSilenceEntry({ open: true, deleteOpen: false });
|
||||
setSilence({});
|
||||
sdToggle();
|
||||
};
|
||||
|
||||
const editSilence = (silence) => () => {
|
||||
setSilenceEntry({ ...silence, open: true, deleteOpen: false });
|
||||
sdToggle();
|
||||
setSilence(silence);
|
||||
};
|
||||
|
||||
const removeSilence = (silence) => () => {
|
||||
setSilenceEntry({ ...silence, deleteOpen: true, open: false });
|
||||
setSilence(silence);
|
||||
toggleDelete();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -93,7 +90,7 @@ export default function Alerting() {
|
|||
) : null}
|
||||
|
||||
<Dialog
|
||||
open={silenceEntry.deleteOpen}
|
||||
open={deleteOpen}
|
||||
onClose={handleDeleteClose()}
|
||||
sx={{ "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }}
|
||||
maxWidth="xs"
|
||||
|
@ -110,9 +107,9 @@ export default function Alerting() {
|
|||
|
||||
<SilenceDialog
|
||||
keepMounted
|
||||
open={silenceEntry.open}
|
||||
onClose={handleClose}
|
||||
silence={silenceEntry}
|
||||
open={sdOpen}
|
||||
onClose={sdClose}
|
||||
silence={silence}
|
||||
/>
|
||||
|
||||
<SpeedDial
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { useState, useContext, useEffect } from "react";
|
||||
import StoreContext from "@qltr/store";
|
||||
|
||||
import { useUpsertAlert } from "@qltr/queries";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Button from "@mui/material/Button";
|
||||
|
@ -11,25 +10,53 @@ import Dialog from "@mui/material/Dialog";
|
|||
import TextField from "@mui/material/TextField";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
|
||||
import SilenceSelector from "./SilenceSelector.jsx";
|
||||
export function useSilenceDialog(isOpen = false) {
|
||||
const [open, setOpen] = useState(isOpen);
|
||||
const [silence, setSilence] = useState({});
|
||||
const upsertAlert = useUpsertAlert();
|
||||
const dialogToggle = () => setOpen(!open);
|
||||
const dialogClose = (confirmedSilence) => {
|
||||
setOpen(false);
|
||||
if (!confirmedSilence) return;
|
||||
upsertAlert(confirmedSilence);
|
||||
setSilence({});
|
||||
};
|
||||
return [open, dialogToggle, silence, setSilence, dialogClose];
|
||||
}
|
||||
|
||||
export default function SilenceDialog(props) {
|
||||
const { silence, open, onClose } = props;
|
||||
const [duration, setDuration] = useState({ h: 0, m: 0 });
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const [silenceEntry, setSilenceEntry] = useState(silence);
|
||||
|
||||
const durationModified = duration.h !== 0 || duration.m !== 0;
|
||||
const silenceModified =
|
||||
silence.name !== silenceEntry.name ||
|
||||
silence.method !== silenceEntry.method ||
|
||||
silence.class !== silenceEntry.class;
|
||||
const modified =
|
||||
(Object.keys(silence).length > 0 && durationModified) || silenceModified;
|
||||
|
||||
useEffect(() => {
|
||||
setSilenceEntry(silence);
|
||||
setDuration({ h: 0, m: 0 });
|
||||
}, [silence, open]);
|
||||
|
||||
const { state: store, updateStore } = useContext(StoreContext);
|
||||
|
||||
const handleCancel = () => onClose();
|
||||
|
||||
const handleOk = () => onClose(silenceEntry);
|
||||
|
||||
const updateSilence = (silenceType) => (e) => {
|
||||
silenceEntry[silenceType] = e.target.value;
|
||||
setSilenceEntry({ ...silenceEntry });
|
||||
const s = { ...silenceEntry };
|
||||
s[silenceType] = e.target.value;
|
||||
setSilenceEntry({ ...s, expires: duration });
|
||||
};
|
||||
|
||||
const updateDuration = (dur) => {
|
||||
setDuration(dur);
|
||||
setSilenceEntry({ ...silenceEntry, expires: dur });
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -37,7 +64,7 @@ export default function SilenceDialog(props) {
|
|||
sx={
|
||||
fullScreen
|
||||
? {}
|
||||
: { "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }
|
||||
: { "& .MuiDialog-paper": { width: "80%", maxHeight: 525 } }
|
||||
}
|
||||
maxWidth="xs"
|
||||
open={open}
|
||||
|
@ -70,12 +97,19 @@ export default function SilenceDialog(props) {
|
|||
onChange={updateSilence("class")}
|
||||
margin="normal"
|
||||
/>
|
||||
<SilenceSelector
|
||||
formerExpires={silence.expires}
|
||||
duration={duration}
|
||||
setDuration={updateDuration}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk}>Ok</Button>
|
||||
<Button onClick={handleOk} disabled={!modified}>
|
||||
Ok
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
70
src/views/alerting/SilenceSelector.jsx
Normal file
70
src/views/alerting/SilenceSelector.jsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { useState } from "react";
|
||||
import moment from "moment";
|
||||
import Box from "@mui/material/Box";
|
||||
import TextField from "@mui/material/TextField";
|
||||
|
||||
const MAX_DURATION = { h: 72, m: 59 };
|
||||
const MIN_DURATION = 0;
|
||||
export default function SilenceSelector(props) {
|
||||
const [expires, setExpires] = useState(moment());
|
||||
const { formerExpires, duration, setDuration } = props;
|
||||
const formerExpirey =
|
||||
formerExpires && duration.h === 0 && duration.m === 0
|
||||
? moment(formerExpires)
|
||||
: null;
|
||||
const updateDuration = (t) => (e) => {
|
||||
const d = { ...duration };
|
||||
d[t] = Math.abs(parseInt(e.target.value));
|
||||
if (isNaN(d[t])) d[t] = 0;
|
||||
d[t] = d[t] <= MAX_DURATION[t] ? d[t] : MAX_DURATION[t];
|
||||
setDuration(d);
|
||||
const now = moment().add(d.h, "hours").add(d.m, "minutes");
|
||||
setExpires(now);
|
||||
};
|
||||
const onNumberInput = (e) =>
|
||||
(e.target.value =
|
||||
!!e.target.value && Math.abs(e.target.value) >= MIN_DURATION
|
||||
? Math.abs(e.target.value)
|
||||
: null);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box style={{ display: "flex" }}>
|
||||
<TextField
|
||||
margin="normal"
|
||||
label="Hours"
|
||||
type="number"
|
||||
onChange={updateDuration("h")}
|
||||
inputProps={{
|
||||
min: MIN_DURATION,
|
||||
max: MAX_DURATION.h,
|
||||
onInput: onNumberInput,
|
||||
}}
|
||||
value={duration.h}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
sx={{ marginLeft: 1 }}
|
||||
label="Minutes"
|
||||
type="number"
|
||||
onChange={updateDuration("m")}
|
||||
inputProps={{
|
||||
min: MIN_DURATION,
|
||||
max: MAX_DURATION.m,
|
||||
onInput: onNumberInput,
|
||||
}}
|
||||
value={duration.m}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Box>
|
||||
<TextField
|
||||
margin="normal"
|
||||
label="Expires"
|
||||
value={`${(formerExpirey ?? expires).format("L")} ${(
|
||||
formerExpirey ?? expires
|
||||
).format("LT")}`}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useContext } from "react";
|
||||
import moment from "moment";
|
||||
import StoreContext from "@qltr/store";
|
||||
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
|
@ -18,7 +19,7 @@ export default function SilencingBox(props) {
|
|||
method: testMethod,
|
||||
class: testClass,
|
||||
id: silenceId,
|
||||
silencedUntil,
|
||||
expires,
|
||||
} = silenceEntry;
|
||||
|
||||
const { state: store, updateStore } = useContext(StoreContext);
|
||||
|
@ -61,11 +62,19 @@ export default function SilencingBox(props) {
|
|||
<br />
|
||||
{`Test Class: ${testClass}`}
|
||||
<br />
|
||||
{`Silenced Until: ${silencedUntil} Remaining Time: 2:50`}
|
||||
{`Silenced Until: ${expires} Remaining Time: ${moment(
|
||||
moment(expires).diff(moment())
|
||||
).format("HH:mm")}`}
|
||||
</Typography>
|
||||
|
||||
<Stack
|
||||
sx={{ ml: "auto", display: { md: "none", lg: "none", xl: "none" } }}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
mb: "auto",
|
||||
mt: "auto",
|
||||
whiteSpace: "nowrap",
|
||||
display: { md: "none", lg: "none", xl: "none" },
|
||||
}}
|
||||
>
|
||||
<Actions />
|
||||
</Stack>
|
||||
|
|
|
@ -9,7 +9,7 @@ import TextField from "@mui/material/TextField";
|
|||
export default function Catalog() {
|
||||
const { state: store, updateStore } = useContext(StoreContext);
|
||||
const { state: jobState } = useContext(JobContext);
|
||||
const { isLoading, data: tests } = useCatalogTests();
|
||||
const { isLoading, data: rawTests } = useCatalogTests();
|
||||
const handleSearchChange = (e) =>
|
||||
updateStore({ catalogSearch: e.target.value });
|
||||
|
||||
|
@ -22,6 +22,8 @@ export default function Catalog() {
|
|||
}, []);
|
||||
|
||||
const catalogWithJobs = () => {
|
||||
const tests = rawTests.map((t) => ({ ...t, isPipeline: t.pipeline }));
|
||||
for (var t of tests) delete t.pipeline;
|
||||
for (var test of tests) {
|
||||
if (test.isPipeline) {
|
||||
const pipeline = jobState.pipelines.find((p) =>
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
useJobNav,
|
||||
} from "@qltr/util/JobTools";
|
||||
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
|
@ -40,8 +42,9 @@ export default function CatalogBox(props) {
|
|||
const { jobFactory } = useContext(JobContext);
|
||||
const jobNav = useJobNav();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const toggleOpen = () => setOpen(!open);
|
||||
const theme = useTheme();
|
||||
const minifyActions = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const navigateToJob = () => {
|
||||
if (pipeline) return jobNav.toPipeline(pipeline.id);
|
||||
|
@ -50,7 +53,10 @@ export default function CatalogBox(props) {
|
|||
|
||||
const runTest = () => {
|
||||
if (isPipeline) return runPipelineTest();
|
||||
const jobId = jobFactory({ testNames: [testName], isTriage: true });
|
||||
const jobId = jobFactory({
|
||||
testNames: [testName],
|
||||
isTriage: store.triageFailing,
|
||||
});
|
||||
if (store.focusJob) jobNav.toJob(jobId);
|
||||
};
|
||||
|
||||
|
@ -86,21 +92,6 @@ export default function CatalogBox(props) {
|
|||
return useJobIconState(job);
|
||||
}
|
||||
|
||||
function Actions() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<IconButton
|
||||
color="success"
|
||||
aria-label="play"
|
||||
component="span"
|
||||
onClick={jobOnClick}
|
||||
>
|
||||
{jobIcon()}
|
||||
</IconButton>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
expanded={open}
|
||||
|
@ -129,20 +120,18 @@ export default function CatalogBox(props) {
|
|||
</Box>
|
||||
<br />
|
||||
</Typography>
|
||||
|
||||
<Stack
|
||||
sx={{ ml: "auto", display: { md: "none", lg: "none", xl: "none" } }}
|
||||
direction={minifyActions ? "column" : "row"}
|
||||
sx={{ ml: "auto", mb: "auto", mt: "auto", whiteSpace: "nowrap" }}
|
||||
>
|
||||
<Actions />
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
ml: "auto",
|
||||
display: { xs: "none", sm: "none", md: "flex", lg: "flex" },
|
||||
}}
|
||||
<IconButton
|
||||
color="success"
|
||||
aria-label="play"
|
||||
component="span"
|
||||
onClick={jobOnClick}
|
||||
>
|
||||
<Actions />
|
||||
{jobIcon()}
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
|
|
|
@ -1,135 +1,103 @@
|
|||
import { useState, useContext } from "react";
|
||||
import { useCurrentlyFailing, useSilencedAlerts } from "@qltr/queries";
|
||||
import StoreContext from "@qltr/store";
|
||||
import JobContext from "@qltr/jobs";
|
||||
import { useJobNav } from "@qltr/util/JobTools";
|
||||
import SilenceDialog from "../alerting/SilenceDialog.jsx";
|
||||
import SilenceDialog, { useSilenceDialog } from "../alerting/SilenceDialog.jsx";
|
||||
import FailingBox from "./FailingBox.jsx";
|
||||
import QuickSilence, { useQuickSilence } from "./QuickSilence.jsx";
|
||||
import FailingRetry from "./FailingRetry.jsx";
|
||||
|
||||
import SpeedDial from "@mui/material/SpeedDial";
|
||||
|
||||
import Button from "@mui/material/Button";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import ReplayIcon from "@mui/icons-material/Replay";
|
||||
// MaterialUI
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
export default function Failing() {
|
||||
const { state: jobState, retryAll } = useContext(JobContext);
|
||||
const { state: store, silenceRequest } = useContext(StoreContext);
|
||||
const { isLoading, data: failing } = useCurrentlyFailing();
|
||||
const { isSilencedLoading, data: silencedAlerts } = useSilencedAlerts();
|
||||
const jobNav = useJobNav();
|
||||
const [silenceEntry, setSilenceEntry] = useState({ open: false });
|
||||
const { state: jobState } = useContext(JobContext);
|
||||
const { isLoading: failsLoading, data: failing } = useCurrentlyFailing();
|
||||
const { isLoading: silencesLoading, data: silences } = useSilencedAlerts();
|
||||
const [qsAnchor, qsTest, openQs, closeQs] = useQuickSilence();
|
||||
const [sdOpen, sdToggle, silence, setSilence, sdClose] = useSilenceDialog();
|
||||
|
||||
const closeSilence = () => setSilenceEntry({ ...silenceEntry, open: false });
|
||||
|
||||
const handleSilenceClose = (silenceReq) => {
|
||||
closeSilence();
|
||||
if (!silenceReq) return;
|
||||
silenceRequest(silenceReq);
|
||||
const joinSilence = (result) => {
|
||||
if (!silences) return;
|
||||
const silence = silences.find((s) => s.name === result.name);
|
||||
if (silence) result.silence = silence;
|
||||
};
|
||||
|
||||
const editSilence = (silence) => () => {
|
||||
setSilenceEntry({ ...silence, open: true });
|
||||
};
|
||||
|
||||
const [retryAllOpen, setRetryAllOpen] = useState(false);
|
||||
const retryAllClick = () => setRetryAllOpen(!retryAllOpen);
|
||||
|
||||
const handleClose = (confirmed) => () => {
|
||||
retryAllClick();
|
||||
if (!confirmed) return;
|
||||
const jobId = retryAll(store.failing);
|
||||
|
||||
if (!store.focusJob) return;
|
||||
jobNav.toJob(jobId);
|
||||
};
|
||||
|
||||
const failingTestsWithJobs = () => {
|
||||
if (isLoading) return [];
|
||||
const silences = silencedAlerts ?? [];
|
||||
for (var test of failing) {
|
||||
const silence = silences.find(
|
||||
(s) => s.name === test.name || s.class === test.class
|
||||
const joinJob = (result) => {
|
||||
if (result.isPipeline) return;
|
||||
const job = jobState.jobs.find(
|
||||
(j) => !j.isPipeline && j.builderCache.testNames.includes(result.name)
|
||||
);
|
||||
if (job) result.job = job;
|
||||
};
|
||||
|
||||
if (silence) test.silencedUntil = silence;
|
||||
if (test.isPipeline) {
|
||||
const joinPipeline = (result) => {
|
||||
if (!result.isPipeline) return;
|
||||
const pipeline = jobState.pipelines.find((p) =>
|
||||
p.selectedBranches.find((b) => b.name === test.name)
|
||||
p.selectedBranches.find((b) => b.name === result.name)
|
||||
);
|
||||
if (!pipeline) continue;
|
||||
if (!pipeline) return;
|
||||
const pipelineJob = jobState.jobs.find(
|
||||
(j) =>
|
||||
j.isPipeline &&
|
||||
j.pipelineId === pipeline.id &&
|
||||
j.branchId === test.name
|
||||
j.branchId === result.name
|
||||
);
|
||||
if (!pipelineJob) test.pipeline = pipeline;
|
||||
test.job = pipelineJob;
|
||||
continue;
|
||||
if (!pipelineJob) result.pipeline = pipeline;
|
||||
else result.job = pipelineJob;
|
||||
};
|
||||
|
||||
const resultsCompiled = () => {
|
||||
if (failsLoading) return [];
|
||||
const compiled = [];
|
||||
for (var r of failing) {
|
||||
const rc = { ...r };
|
||||
joinSilence(rc);
|
||||
// Find associated job and skip pipeline
|
||||
joinJob(rc);
|
||||
// Find associated pipeline
|
||||
joinPipeline(rc);
|
||||
compiled.push(rc);
|
||||
}
|
||||
const job = jobState.jobs.find(
|
||||
(j) => !j.isPipeline && j.builderCache.testNames.includes(test.name)
|
||||
);
|
||||
if (job) test.job = job;
|
||||
}
|
||||
return failing;
|
||||
return compiled;
|
||||
};
|
||||
|
||||
const alertClick = (result) => (e) => {
|
||||
if (e) e.preventDefault();
|
||||
if (e && e.type === "contextmenu" && result.silence)
|
||||
return openQs(e, result);
|
||||
const { name, class: className, method } = result;
|
||||
const testInfo = { name, class: className, method };
|
||||
const silenceInfo = result.silence ?? testInfo;
|
||||
sdToggle();
|
||||
setSilence(silenceInfo);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="failing">
|
||||
{isLoading
|
||||
? null
|
||||
: failingTestsWithJobs().map((v, i) => (
|
||||
<FailingBox key={i} failingTest={v} silenceClick={editSilence(v)} />
|
||||
))}
|
||||
|
||||
<Dialog
|
||||
open={retryAllOpen}
|
||||
onClose={handleClose()}
|
||||
sx={{ "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<DialogTitle>Retry all failing tests?</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
This will create x jobs and run y tests
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose()}>Cancel</Button>
|
||||
<Button onClick={handleClose(true)} autoFocus>
|
||||
Yes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<SilenceDialog
|
||||
keepMounted
|
||||
open={silenceEntry.open}
|
||||
onClose={handleSilenceClose}
|
||||
silence={silenceEntry}
|
||||
open={sdOpen}
|
||||
onClose={sdClose}
|
||||
silence={silence}
|
||||
/>
|
||||
{(failing ?? []).length === 0 ? (
|
||||
<QuickSilence
|
||||
anchorEl={qsAnchor}
|
||||
handleClose={closeQs}
|
||||
test={qsTest}
|
||||
editSilence={alertClick(qsTest)}
|
||||
/>
|
||||
<FailingRetry failing={failsLoading ? [] : failing} />
|
||||
{!failsLoading && failing.length === 0 && (
|
||||
<Box display="flex" alignItems="center" justifyContent="center">
|
||||
<Typography variant="h4">No tests failing!</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{(failing ?? []).length === 0 ? null : (
|
||||
<SpeedDial
|
||||
ariaLabel="Retry All"
|
||||
sx={{ position: "fixed", bottom: 16, right: 16 }}
|
||||
icon={<ReplayIcon />}
|
||||
onClick={retryAllClick}
|
||||
open={false}
|
||||
/>
|
||||
)}
|
||||
{!failsLoading &&
|
||||
resultsCompiled().map((v, i) => (
|
||||
<FailingBox key={i} failingTest={v} alertClick={alertClick(v)} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useContext } from "react";
|
||||
import { usePipelineMappings } from "@qltr/queries";
|
||||
import { usePipelineMappings, useIgnoreResult } from "@qltr/queries";
|
||||
import StoreContext from "@qltr/store";
|
||||
import JobContext, { jobStatus } from "@qltr/jobs";
|
||||
import {
|
||||
|
@ -7,7 +7,8 @@ import {
|
|||
usePipelineIconState,
|
||||
useJobNav,
|
||||
} from "@qltr/util/JobTools";
|
||||
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
|
@ -42,30 +43,33 @@ import { asTree, asBranches, as1d } from "@qltr/util/pipelines.js";
|
|||
const stopPropagation = (e) => e.stopPropagation() && e.preventDefault();
|
||||
|
||||
export default function FailingBox(props) {
|
||||
const { failingTest, silenceClick } = props;
|
||||
const { failingTest, alertClick } = props;
|
||||
const {
|
||||
class: testClass,
|
||||
name: testName,
|
||||
timestamp,
|
||||
silencedUntil,
|
||||
silence,
|
||||
type,
|
||||
dailyFails,
|
||||
screenshot: screenshotUrl,
|
||||
console: cs,
|
||||
recentResults,
|
||||
failedMessage,
|
||||
message,
|
||||
isPipeline,
|
||||
job,
|
||||
pipeline,
|
||||
} = failingTest;
|
||||
const ignoreResult = useIgnoreResult();
|
||||
const runHistory = recentResults ? [...recentResults].reverse() : null;
|
||||
|
||||
const { data: pipelineMappings, isLoading } = usePipelineMappings();
|
||||
const { jobFactory } = useContext(JobContext);
|
||||
const { state: store, updateStore, removeFailure } = useContext(StoreContext);
|
||||
const jobNav = useJobNav();
|
||||
|
||||
const theme = useTheme();
|
||||
const minifyActions = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const [open, setOpen] = useState(false);
|
||||
const toggleOpen = () => setOpen(!open);
|
||||
|
||||
const [removeOpen, setRemoveOpen] = useState(false);
|
||||
const removeClick = () => setRemoveOpen(!removeOpen);
|
||||
|
||||
|
@ -73,7 +77,7 @@ export default function FailingBox(props) {
|
|||
stopPropagation(e);
|
||||
setRemoveOpen(false);
|
||||
if (!confirmed) return;
|
||||
removeFailure(failingTest);
|
||||
ignoreResult(failingTest);
|
||||
};
|
||||
|
||||
function badgeColor() {
|
||||
|
@ -91,7 +95,7 @@ export default function FailingBox(props) {
|
|||
branches: asBranches(primaries),
|
||||
tree: asTree(primaries),
|
||||
selectedBranches: as1d(primaries),
|
||||
isTriage: true,
|
||||
isTriage: store.triageFailing,
|
||||
};
|
||||
const pipeline = jobFactory(builderCache);
|
||||
if (store.focusJob) jobNav.toPipeline(pipeline.id);
|
||||
|
@ -99,7 +103,10 @@ export default function FailingBox(props) {
|
|||
|
||||
const retryTest = () => {
|
||||
if (isPipeline) return retryPipelineTest();
|
||||
const jobId = jobFactory({ testNames: [testName], isTriage: true });
|
||||
const jobId = jobFactory({
|
||||
testNames: [testName],
|
||||
isTriage: store.triageFailing,
|
||||
});
|
||||
if (store.focusJob) jobNav.toJob(jobId);
|
||||
};
|
||||
|
||||
|
@ -120,39 +127,6 @@ export default function FailingBox(props) {
|
|||
return useJobIconState(job);
|
||||
}
|
||||
|
||||
function Actions() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<a href={screenshotUrl}>
|
||||
<IconButton aria-label="photo" component="span">
|
||||
<PhotoCameraIcon />
|
||||
</IconButton>
|
||||
</a>
|
||||
|
||||
<IconButton aria-label="retry" component="span" onClick={jobOnClick}>
|
||||
{jobIcon()}
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
aria-label="silence"
|
||||
component="span"
|
||||
color={silencedUntil ? "primary" : "default"}
|
||||
onClick={silenceClick}
|
||||
>
|
||||
<NotificationsIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="error"
|
||||
aria-label="delete"
|
||||
component="span"
|
||||
onClick={removeClick}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
expanded={open}
|
||||
|
@ -212,10 +186,21 @@ export default function FailingBox(props) {
|
|||
<br />
|
||||
<div>
|
||||
<span className="recent-results">
|
||||
{recentResults.map(
|
||||
{runHistory &&
|
||||
runHistory.map(
|
||||
(v, i) =>
|
||||
(v && <CheckIcon key={i} color="success" />) || (
|
||||
<ClearIcon key={i} color="error" />
|
||||
(!v.failed && (
|
||||
<CheckIcon
|
||||
key={i}
|
||||
color="success"
|
||||
titleAccess={v.timestamp}
|
||||
/>
|
||||
)) || (
|
||||
<ClearIcon
|
||||
key={i}
|
||||
color="error"
|
||||
titleAccess={v.timestamp}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
|
@ -225,27 +210,41 @@ export default function FailingBox(props) {
|
|||
|
||||
<Stack
|
||||
onClick={stopPropagation}
|
||||
sx={{ ml: "auto", display: { md: "none", lg: "none", xl: "none" } }}
|
||||
direction={minifyActions ? "column" : "row"}
|
||||
sx={{ ml: "auto", mb: "auto", mt: "auto", whiteSpace: "nowrap" }}
|
||||
>
|
||||
<Actions />
|
||||
</Stack>
|
||||
<Stack
|
||||
onClick={stopPropagation}
|
||||
direction="row"
|
||||
sx={{
|
||||
ml: "auto",
|
||||
mb: "auto",
|
||||
mt: "auto",
|
||||
whiteSpace: "nowrap",
|
||||
display: { xs: "none", sm: "none", md: "block", lg: "block" },
|
||||
}}
|
||||
<a href={screenshotUrl}>
|
||||
<IconButton aria-label="photo" component="span">
|
||||
<PhotoCameraIcon />
|
||||
</IconButton>
|
||||
</a>
|
||||
|
||||
<IconButton aria-label="retry" component="span" onClick={jobOnClick}>
|
||||
{jobIcon()}
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
aria-label="silence"
|
||||
component="span"
|
||||
color={silence && silence.expires ? "primary" : "default"}
|
||||
onClick={alertClick}
|
||||
onContextMenu={alertClick}
|
||||
>
|
||||
<Actions />
|
||||
<NotificationsIcon titleAccess={silence && silence.expires} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="error"
|
||||
aria-label="delete"
|
||||
component="span"
|
||||
onClick={removeClick}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography component={"span"} style={{ wordBreak: "break-word" }}>
|
||||
{failedMessage}
|
||||
{message}
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
|
64
src/views/failing/FailingRetry.jsx
Normal file
64
src/views/failing/FailingRetry.jsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
// React
|
||||
import React, { useState, useContext } from "react";
|
||||
import JobContext from "@qltr/jobs";
|
||||
import StoreContext from "@qltr/store";
|
||||
import { useJobNav } from "@qltr/util/JobTools";
|
||||
|
||||
// Components
|
||||
import Button from "@mui/material/Button";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import SpeedDial from "@mui/material/SpeedDial";
|
||||
|
||||
// Icons
|
||||
import ReplayIcon from "@mui/icons-material/Replay";
|
||||
|
||||
export default function FailingRetry(props) {
|
||||
const { failing } = props;
|
||||
const { state: jobState, retryAll } = useContext(JobContext);
|
||||
const { state: store } = useContext(StoreContext);
|
||||
const [open, setOpen] = useState(false);
|
||||
const jobNav = useJobNav();
|
||||
const toggleOpen = () => setOpen(!open);
|
||||
const dialogClose = (confirmed) => () => {
|
||||
toggleOpen();
|
||||
if (!confirmed) return;
|
||||
const jobId = retryAll(failing);
|
||||
if (!store.focusJob) return;
|
||||
jobNav.toJob(jobId);
|
||||
};
|
||||
if (!failing || failing.length === 0) return;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={dialogClose()}
|
||||
sx={{ "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<DialogTitle>Retry all failing tests?</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
This will create x jobs and run y tests
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={dialogClose()}>Cancel</Button>
|
||||
<Button onClick={dialogClose(true)} autoFocus>
|
||||
Yes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<SpeedDial
|
||||
ariaLabel="Retry All"
|
||||
sx={{ position: "fixed", bottom: 16, right: 16 }}
|
||||
icon={<ReplayIcon />}
|
||||
onClick={toggleOpen}
|
||||
open={false}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
58
src/views/failing/QuickSilence.jsx
Normal file
58
src/views/failing/QuickSilence.jsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
// React
|
||||
import { useState, useContext } from "react";
|
||||
import { useUpsertAlert } from "@qltr/queries";
|
||||
|
||||
// Components
|
||||
import Popover from "@mui/material/Popover";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
|
||||
//Icons
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
|
||||
export function useQuickSilence() {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [test, setTest] = useState(null);
|
||||
const openMenuAt = (e, t) => {
|
||||
e.preventDefault();
|
||||
setTest(t);
|
||||
setAnchorEl(e.currentTarget);
|
||||
};
|
||||
const closeMenu = () => setAnchorEl(null);
|
||||
return [anchorEl, test, openMenuAt, closeMenu];
|
||||
}
|
||||
|
||||
export default function QuickSilence(props) {
|
||||
const { anchorEl, test, handleClose, editSilence } = props;
|
||||
const upsertAlert = useUpsertAlert();
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
async function deleteClick() {
|
||||
await upsertAlert({ id: test.silence.id, expires: null });
|
||||
handleClose();
|
||||
}
|
||||
|
||||
async function editClick() {
|
||||
editSilence();
|
||||
handleClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
<IconButton color="primary" onClick={editClick}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton color="error" onClick={deleteClick}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Popover>
|
||||
);
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useContext } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import JobContext, { jobStatus } from "@qltr/jobs";
|
||||
import {
|
||||
selectedPipelineBranches,
|
||||
|
@ -37,6 +38,7 @@ function JobPipelineDisplay(props) {
|
|||
} = useContext(JobContext);
|
||||
|
||||
const jobNav = useJobNav();
|
||||
const nav = useNavigate();
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
@ -92,7 +94,7 @@ function JobPipelineDisplay(props) {
|
|||
<Toolbar disableGutters />
|
||||
<Box sx={{ flexGrow: 1, margin: "0 10px" }}>
|
||||
<Toolbar disableGutters>
|
||||
<IconButton onClick={jobNav.toJobs}>
|
||||
<IconButton onClick={() => nav(-1)}>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" sx={{ ml: "auto", mr: "auto" }}>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { useJobNav } from "@qltr/util/JobTools";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import JobContext, { jobStatus } from "@qltr/jobs";
|
||||
import StoreContext from "@qltr/store";
|
||||
import Box from "@mui/material/Box";
|
||||
|
@ -27,6 +28,7 @@ export default function JobView(props) {
|
|||
const { jobFactory, jobCancel, jobDestroy } = useContext(JobContext);
|
||||
const { state: store } = useContext(StoreContext);
|
||||
const jobNav = useJobNav();
|
||||
const nav = useNavigate();
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClick = (event) => {
|
||||
|
@ -91,7 +93,7 @@ export default function JobView(props) {
|
|||
<Toolbar disableGutters />
|
||||
<Box sx={{ flexGrow: 1, margin: "0 10px" }}>
|
||||
<Toolbar disableGutters>
|
||||
<IconButton onClick={navigateToJobs}>
|
||||
<IconButton onClick={() => nav(-1)}>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" component="span" sx={{ ml: "auto" }}>
|
||||
|
|
|
@ -52,7 +52,7 @@ export default function JobBuilder() {
|
|||
const handleClose = (confirmed) => () => {
|
||||
setJobDialogOpen(false);
|
||||
if (!confirmed) return;
|
||||
const jobId = jobFactory(cache);
|
||||
const jobId = jobFactory({ ...cache, isTriage: store.triageFailing });
|
||||
if (store.focusJob) jobNav.toJob(jobId);
|
||||
};
|
||||
|
||||
|
|
|
@ -115,6 +115,11 @@ export default function Settings(props) {
|
|||
<Switch edge="end" checked={store.focusJob} />
|
||||
</ListItem>
|
||||
|
||||
<ListItem button divider onClick={handleToggle("triageFailing")}>
|
||||
<ListItemText primary="Triage Failing" />
|
||||
<Switch edge="end" checked={store.triageFailing} />
|
||||
</ListItem>
|
||||
|
||||
<ListItem button divider onClick={handleToggle("logAppDetails")}>
|
||||
<ListItemText primary="Log App Details" />
|
||||
<Switch edge="end" checked={store.logAppDetails} />
|
||||
|
|
12
tests/api.js
12
tests/api.js
|
@ -7,16 +7,18 @@ import axios from "axios";
|
|||
const qltr = new Qualiteer();
|
||||
await qltr.start();
|
||||
|
||||
const url = "https://Qualiteer.elijahparker3.repl.co";
|
||||
const url = process.env.QUALITEER_EXECUTOR_URL;
|
||||
|
||||
const testsUrl = "/api/catalog/tests";
|
||||
const mappingsUrl = "/api/catalog/pipeline-mappings";
|
||||
const resultsUrl = "/api/results/failing";
|
||||
|
||||
const get = (...args) => axios.get(`${url}/${args[0]}`, args[1]);
|
||||
const get = (...args) => axios.get(`${url}${args[0]}`, args[1]);
|
||||
|
||||
var res = await get(resultsUrl);
|
||||
const mappings = await get(resultsUrl, {}).catch((e) => console.log(e));
|
||||
console.log(mappings.data);
|
||||
/*var res = await get(resultsUrl);
|
||||
console.log(res.data);
|
||||
|
||||
res = await get(resultsUrl, { headers: { count: true } });
|
||||
|
||||
console.log(res.data);
|
||||
console.log(res.data);*/
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export default function failingTest() {
|
||||
console.log("This came from a failing test!");
|
||||
return { status: 1 };
|
||||
return { status: 1, message: "This test always fails :(" };
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import axios from "axios";
|
||||
import Rabbiteer from "rabbiteer";
|
||||
import primary from "./primary.js";
|
||||
import secondary1 from "./secondary1.js";
|
||||
import secondary2 from "./secondary2.js";
|
||||
|
@ -13,11 +14,15 @@ import failing from "./failing.js";
|
|||
|
||||
// Constants
|
||||
const liveUpdateDelay = 100;
|
||||
const endLiveCount = 20;
|
||||
const reportingUrl = `${process.env.QUALITEER_URL}/api/dev/rabbit/TestResults`;
|
||||
const endLiveCount = 200;
|
||||
const reportingUrl = `${process.env.QUALITEER_EXECUTOR_URL}/api/dev/rabbit/TestResults`;
|
||||
// Pull args
|
||||
const args = process.argv.slice(2);
|
||||
const test = (args.find((v) => v.includes("test=")) ?? "").replace("test=", "");
|
||||
const isTriage = (args.find((v) => v.includes("isTriage=")) ?? "").replace(
|
||||
"isTriage=",
|
||||
""
|
||||
);
|
||||
var pipeline = (args.find((v) => v.includes("pipeline=")) ?? "").replace(
|
||||
"pipeline=",
|
||||
""
|
||||
|
@ -58,14 +63,26 @@ const runTests = () => {
|
|||
|
||||
// Run
|
||||
liveIndicator();
|
||||
setTimeout(() => {
|
||||
setTimeout(async () => {
|
||||
const status = runTests();
|
||||
if (pipeline) pipeline.testData = status.pipelineData;
|
||||
const testResult = {
|
||||
...status,
|
||||
failed: status.status !== 0,
|
||||
name: test,
|
||||
pipeline,
|
||||
triage: isTriage === "true",
|
||||
};
|
||||
|
||||
/*const rbt = new Rabbiteer(null, [], {
|
||||
autoRabbit: { user: "guest", pass: "guest" },
|
||||
});
|
||||
await rbt.connect();
|
||||
const data = JSON.stringify(testResult);
|
||||
await rbt.publishMessageWithTimeout("TestResults", data);
|
||||
rbt.disconnect();
|
||||
if (status.status === 1) process.exit(1);*/
|
||||
|
||||
axios
|
||||
.post(reportingUrl, { testResult })
|
||||
.catch((e) => {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Initiator, Executor } from "qualiteer/clients";
|
|||
const qltr = new Qualiteer();
|
||||
await qltr.start();
|
||||
|
||||
const url = process.env.QUALITEER_URL;
|
||||
const url = process.env.QUALITEER_EXECUTOR_URL;
|
||||
|
||||
// Create an initiator and make a job request
|
||||
const primary = new Initiator(url);
|
||||
|
|
|
@ -2,20 +2,21 @@ import { defineConfig } from "vite";
|
|||
import react from "@vitejs/plugin-react";
|
||||
import path from "node:path";
|
||||
|
||||
const { QUALITEER_VITE_BACKEND_URL, QUALITEER_VITE_DEV_PORT } = process.env;
|
||||
const backendUrl = QUALITEER_VITE_BACKEND_URL ?? "http://localhost:52000";
|
||||
const vitePort = QUALITEER_VITE_DEV_PORT ?? 52025;
|
||||
export default () => {
|
||||
return defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
hmr: {
|
||||
port: 443,
|
||||
},
|
||||
port: vitePort,
|
||||
proxy: {
|
||||
"/api": "http://localhost:52000",
|
||||
"/socket.io": {
|
||||
target: "ws://localhost:52000",
|
||||
ws: true,
|
||||
"/api": backendUrl,
|
||||
"/socket.io": backendUrl,
|
||||
},
|
||||
hmr: {
|
||||
protocol: process.env.QUALITEER_VITE_DEV_PROTOCOL,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
@ -26,10 +27,11 @@ export default () => {
|
|||
alias: {
|
||||
"@qltr/util": path.resolve("./src/util/"),
|
||||
"@qltr/queries": path.resolve("./src/util/queries"),
|
||||
"@qltr/joins": path.resolve(`./src/util/Joins.jsx`),
|
||||
"@qltr/jobs": path.resolve("./src/ctx/JobContext.jsx"),
|
||||
"@qltr/store": path.resolve("./src/ctx/StoreContext.jsx"),
|
||||
"@qltr/initiator": path.resolve("./lib/sockets/clients/Initiator.js"),
|
||||
"@qltr/mocks": path.resolve("./lib/database/mocks/")
|
||||
"@qltr/mocks": path.resolve("./lib/database/mocks/"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue