diff --git a/.editorconfig b/.editorconfig index d3d63b79..4cc65b20 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,14 +7,11 @@ root = true [*] end_of_line = lf insert_final_newline = true -indent_style = space +indent_style = tab indent_size = 4 trim_trailing_whitespace = true -# Taskfiles should have tabs -[Taskfile] -indent_style = tab - # Yaml files indent with 2 spaces due to their weird array syntax [*.{yaml,yml,md}] +indent_style = spaces indent_size = 2 diff --git a/.eslintrc.json b/.eslintrc.json index 3b8112ed..f135aa7b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,54 +1,53 @@ { - "env": { - "browser": true, - "es2021": true, - "node": true - }, - "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:prettier/recommended"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": ["react", "@typescript-eslint", "simple-import-sort"], - "rules": { - "max-len": [ - "error", - { - "code": 120, - "tabWidth": 4 - } - ], - "react/react-in-jsx-scope": "off", - "simple-import-sort/imports": [ - "error", - { - "groups": [ - // other packages. Node packages first - ["^@?\\w"], - // Sub components of current component - ["^.+\\.style$"], - ["^.+\\.svg$"], - // Own modules - ["^(/frontend|backend|types)(/.*|$)"], - // Shared modules - ["^types(/.*|$)"], - // Wrong imports (should be replaced) - ["^\\.\\.(?!/?$)", "^\\.\\./?$"], - // Sub components of current component - ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"] - ] - } - ], - "simple-import-sort/exports": "error" - }, - "ignorePatterns": ["node_modules/", "./dashboard/", "./app/", ".parcel-cache/", "./cypress/"], - "settings": { - "react": { - "version": "detect" - } - } + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:prettier/recommended"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["react", "@typescript-eslint", "simple-import-sort"], + "rules": { + "max-len": [ + "error", + { + "code": 120 + } + ], + "react/react-in-jsx-scope": "off", + "simple-import-sort/imports": [ + "error", + { + "groups": [ + // other packages. Node packages first + ["^@?\\w"], + // Sub components of current component + ["^.+\\.style$"], + ["^.+\\.svg$"], + // Own modules + ["^(/frontend|backend|types)(/.*|$)"], + // Shared modules + ["^types(/.*|$)"], + // Wrong imports (should be replaced) + ["^\\.\\.(?!/?$)", "^\\.\\./?$"], + // Sub components of current component + ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"] + ] + } + ], + "simple-import-sort/exports": "error" + }, + "ignorePatterns": ["node_modules/", "./dashboard/", "./app/", ".parcel-cache/", "./cypress/"], + "settings": { + "react": { + "version": "detect" + } + } } diff --git a/.gitignore b/.gitignore index 7618c83a..c2969190 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # Generally ignoring .idea/ -.husky/_/ yarn-error.log /.env diff --git a/.prettierrc b/.prettierrc index 8634d15e..157c9eab 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,8 +1,7 @@ { "printWidth": 120, - "tabWidth": 4, "semi": true, - "useTabs": false, + "useTabs": true, "singleQuote": true, "bracketSpacing": true, "trailingComma": "es5" diff --git a/Taskfile b/Taskfile index 402e9661..45d210b1 100755 --- a/Taskfile +++ b/Taskfile @@ -132,6 +132,18 @@ function task:commit { ## Clean up code before committing title "Comitting" } +function task:clean { ## Clean up the codestyle in all files + title "Running eslint" + echo "Cleaning typescript files..." + eslint --fix --ext ts,tsx . + echo "Done." + title "Running prettier" + echo "Cleaning other files..." + prettier --write --list-different *.{json,js} + prettier --write --list-different **/*.{json,css,html,js} + echo "Done." +} + # ========================================================= ## Taskfile # ========================================================= diff --git a/backend/api/github.ts b/backend/api/github.ts index 2c11ddfc..aa9b9491 100644 --- a/backend/api/github.ts +++ b/backend/api/github.ts @@ -4,12 +4,12 @@ export { getContributors } from './github/contributors'; export { getLatestRelease } from './github/release'; const GitHubApi = () => - axios.create({ - headers: { - 'User-Agent': 'github.com/FuturePortal/CIMonitor', - accept: 'application/json', - }, - baseURL: 'https://api.github.com', - }); + axios.create({ + headers: { + 'User-Agent': 'github.com/FuturePortal/CIMonitor', + accept: 'application/json', + }, + baseURL: 'https://api.github.com', + }); export default GitHubApi; diff --git a/backend/api/github/contributors.ts b/backend/api/github/contributors.ts index 899e5357..0df5a034 100644 --- a/backend/api/github/contributors.ts +++ b/backend/api/github/contributors.ts @@ -3,54 +3,54 @@ import { Contributor } from 'types/cimonitor'; import { GitHubContributor, GitHubUser } from 'types/github'; export const getContributors = async (): Promise => { - const response = await GitHubApi().get('repos/FuturePortal/CIMonitor/stats/contributors'); + const response = await GitHubApi().get('repos/FuturePortal/CIMonitor/stats/contributors'); - const contributors: GitHubContributor[] = response.data; + const contributors: GitHubContributor[] = response.data; - const cleanContributors = cleanResponse(contributors); + const cleanContributors = cleanResponse(contributors); - const enrichedContributors = await enrichContributors(cleanContributors); + const enrichedContributors = await enrichContributors(cleanContributors); - return enrichedContributors.sort(byCommits); + return enrichedContributors.sort(byCommits); }; const cleanResponse = (contributors: GitHubContributor[]): Contributor[] => - contributors - .map((contributor) => ({ - commits: contributor.total, - username: contributor.author.login, - profile: contributor.author.html_url, - image: contributor.author.avatar_url, - })) - .filter((contributor) => !['T-888', 'dependabot'].includes(contributor.username)); + contributors + .map((contributor) => ({ + commits: contributor.total, + username: contributor.author.login, + profile: contributor.author.html_url, + image: contributor.author.avatar_url, + })) + .filter((contributor) => !['T-888', 'dependabot'].includes(contributor.username)); const byCommits = (contributorA: Contributor, contributorB: Contributor): number => - contributorB.commits - contributorA.commits; + contributorB.commits - contributorA.commits; const enrichContributors = async (contributors: Contributor[]): Promise => { - const enrichedContributors: Contributor[] = []; + const enrichedContributors: Contributor[] = []; - for (let contributor of contributors) { - enrichedContributors.push(await enrichContributor(contributor)); - } + for (let contributor of contributors) { + enrichedContributors.push(await enrichContributor(contributor)); + } - return enrichedContributors; + return enrichedContributors; }; const enrichContributor = async (contributor: Contributor): Promise => { - try { - const response = await GitHubApi().get(`users/${contributor.username}`); - - const user: GitHubUser = response.data; - - return { - ...contributor, - site: user.blog, - location: user.location, - name: user.name, - company: user.company, - }; - } catch (error) { - return contributor; - } + try { + const response = await GitHubApi().get(`users/${contributor.username}`); + + const user: GitHubUser = response.data; + + return { + ...contributor, + site: user.blog, + location: user.location, + name: user.name, + company: user.company, + }; + } catch (error) { + return contributor; + } }; diff --git a/backend/api/github/release.ts b/backend/api/github/release.ts index 735f586e..be13645b 100644 --- a/backend/api/github/release.ts +++ b/backend/api/github/release.ts @@ -2,7 +2,7 @@ import GitHubApi from 'backend/api/github'; import { GitHubRelease } from 'types/github'; export const getLatestRelease = async (): Promise => { - const response = await GitHubApi().get('/repos/FuturePortal/CIMonitor/releases/latest'); + const response = await GitHubApi().get('/repos/FuturePortal/CIMonitor/releases/latest'); - return response.data; + return response.data; }; diff --git a/backend/module-client.ts b/backend/module-client.ts index 97536829..bfa695c1 100644 --- a/backend/module-client.ts +++ b/backend/module-client.ts @@ -5,20 +5,20 @@ import StorageManager from 'backend/storage/manager'; import 'dotenv/config'; (async () => { - console.log('[module-client] Starting...'); + console.log('[module-client] Starting...'); - await StorageManager.init(); + await StorageManager.init(); - const { events, triggers } = await StorageManager.loadModules(); + const { events, triggers } = await StorageManager.loadModules(); - const hasModules = ModuleManager.init(triggers, events); + const hasModules = ModuleManager.init(triggers, events); - if (!hasModules) { - console.log('[module-client] Without modules, the module client has no purpose.'); - process.exit(1); - } + if (!hasModules) { + console.log('[module-client] Without modules, the module client has no purpose.'); + process.exit(1); + } - SocketClient.init(); + SocketClient.init(); - SocketClient.listen(); + SocketClient.listen(); })(); diff --git a/backend/module/manager.ts b/backend/module/manager.ts index 927f99d2..83d9c5b2 100644 --- a/backend/module/manager.ts +++ b/backend/module/manager.ts @@ -6,74 +6,74 @@ import GpioModule from './type/gpio'; import HttpModule from './type/http'; class ModuleManager { - triggers: ModuleTrigger[] = []; + triggers: ModuleTrigger[] = []; - events: ModuleEvent[] = []; + events: ModuleEvent[] = []; - modules = { - gpio: GpioModule, - http: HttpModule, - }; + modules = { + gpio: GpioModule, + http: HttpModule, + }; - init(triggers: ModuleTrigger[], events: ModuleEvent[]): boolean { - console.log('[module/manager] Init.'); + init(triggers: ModuleTrigger[], events: ModuleEvent[]): boolean { + console.log('[module/manager] Init.'); - this.triggers = triggers; - this.events = events; + this.triggers = triggers; + this.events = events; - if (triggers.length === 0) { - console.log('[module/manager] No triggers defined in the module config, modules disabled.'); - return false; - } + if (triggers.length === 0) { + console.log('[module/manager] No triggers defined in the module config, modules disabled.'); + return false; + } - if (events.length === 0) { - console.log('[module/manager] No events defined in the module config, modules disabled.'); - return false; - } + if (events.length === 0) { + console.log('[module/manager] No events defined in the module config, modules disabled.'); + return false; + } - console.log('[module/manager] Watching status events to trigger modules...'); + console.log('[module/manager] Watching status events to trigger modules...'); - StatusEvents.on(StatusEvents.event.statusStateChange, (status) => this.checkStatusTriggers(status)); - StatusEvents.on(StatusEvents.event.newStatus, (status) => this.checkStatusTriggers(status)); + StatusEvents.on(StatusEvents.event.statusStateChange, (status) => this.checkStatusTriggers(status)); + StatusEvents.on(StatusEvents.event.newStatus, (status) => this.checkStatusTriggers(status)); - return true; - } + return true; + } - checkStatusTriggers(status: Status) { - for (const trigger of this.triggers) { - let triggerMatchesStatus = true; + checkStatusTriggers(status: Status) { + for (const trigger of this.triggers) { + let triggerMatchesStatus = true; - for (const [statusKey, statusValue] of Object.entries(trigger.status)) { - if (!(statusKey in status && String(status[statusKey]) === String(statusValue))) { - triggerMatchesStatus = false; - } - } + for (const [statusKey, statusValue] of Object.entries(trigger.status)) { + if (!(statusKey in status && String(status[statusKey]) === String(statusValue))) { + triggerMatchesStatus = false; + } + } - if (triggerMatchesStatus) { - console.log(`[module/manager] Trigger match! Firing event ${trigger.event}.`); - this.fireEvent(trigger.event); - } - } - } + if (triggerMatchesStatus) { + console.log(`[module/manager] Trigger match! Firing event ${trigger.event}.`); + this.fireEvent(trigger.event); + } + } + } - fireEvent(name: string) { - console.log(`[module/manager] Firing event ${name}...`); + fireEvent(name: string) { + console.log(`[module/manager] Firing event ${name}...`); - const event = this.events.find((event) => event.name === name); + const event = this.events.find((event) => event.name === name); - if (!event) { - console.log(`[module/manager] No event found with name "${name}".`); - } + if (!event) { + console.log(`[module/manager] No event found with name "${name}".`); + } - event.modules.map((module: ModuleConfig) => { - if (!(module.type in this.modules)) { - console.log(`[module/manager] No module found with type "${module.type}".`); - return; - } + event.modules.map((module: ModuleConfig) => { + if (!(module.type in this.modules)) { + console.log(`[module/manager] No module found with type "${module.type}".`); + return; + } - this.modules[module.type].fire(module); - }); - } + this.modules[module.type].fire(module); + }); + } } export default new ModuleManager(); diff --git a/backend/module/type.ts b/backend/module/type.ts index 814b5288..09a78389 100644 --- a/backend/module/type.ts +++ b/backend/module/type.ts @@ -1,10 +1,10 @@ import { ModuleConfig } from 'types/module'; abstract class ModuleType { - abstract name: string; + abstract name: string; - // eslint-disable-next-line no-unused-vars - abstract fire(config: ModuleConfig): void; + // eslint-disable-next-line no-unused-vars + abstract fire(config: ModuleConfig): void; } export default ModuleType; diff --git a/backend/module/type/gpio.ts b/backend/module/type/gpio.ts index d3ea1965..e78b165b 100644 --- a/backend/module/type/gpio.ts +++ b/backend/module/type/gpio.ts @@ -4,46 +4,46 @@ import ModuleType from 'backend/module/type'; import { ModuleConfig } from 'types/module'; class GpioModule extends ModuleType { - name: 'GPIO'; - - fire(config: ModuleConfig): void { - if (config.type !== 'gpio') { - return; - } - - console.log(`[module/gpio] Triggering GPIO ${config.mode}...`); - - switch (config.mode) { - case 'on': - case 'off': - return this.gpio(config.pin, config.mode === 'on'); - case 'on-for': - case 'off-for': - this.gpio(config.pin, config.mode === 'on-for'); - - setTimeout(() => { - this.gpio(config.pin, config.mode !== 'on-for'); - }, config.duration || 5000); - } - } - - handleExecError(error, stdout, stderr) { - if (error) { - console.log(`[module/gpio] Could not execute gpio command.`); - console.error(error); - } - - if (stderr) { - console.log(`[module/gpio] Could not execute gpio command.`); - console.error(stderr); - } - } - - gpio(pin: number, on: boolean) { - console.log(`[module/gpio] gpio set ${pin} ${on ? 'on' : 'off'}.`); - exec(`gpio mode ${pin} out`, this.handleExecError); - exec(`gpio write ${pin} ${on ? '0' : '1'}`, this.handleExecError); - } + name: 'GPIO'; + + fire(config: ModuleConfig): void { + if (config.type !== 'gpio') { + return; + } + + console.log(`[module/gpio] Triggering GPIO ${config.mode}...`); + + switch (config.mode) { + case 'on': + case 'off': + return this.gpio(config.pin, config.mode === 'on'); + case 'on-for': + case 'off-for': + this.gpio(config.pin, config.mode === 'on-for'); + + setTimeout(() => { + this.gpio(config.pin, config.mode !== 'on-for'); + }, config.duration || 5000); + } + } + + handleExecError(error, stdout, stderr) { + if (error) { + console.log(`[module/gpio] Could not execute gpio command.`); + console.error(error); + } + + if (stderr) { + console.log(`[module/gpio] Could not execute gpio command.`); + console.error(stderr); + } + } + + gpio(pin: number, on: boolean) { + console.log(`[module/gpio] gpio set ${pin} ${on ? 'on' : 'off'}.`); + exec(`gpio mode ${pin} out`, this.handleExecError); + exec(`gpio write ${pin} ${on ? '0' : '1'}`, this.handleExecError); + } } export default new GpioModule(); diff --git a/backend/module/type/http.ts b/backend/module/type/http.ts index a4de46e3..f064e6a8 100644 --- a/backend/module/type/http.ts +++ b/backend/module/type/http.ts @@ -2,17 +2,17 @@ import ModuleType from 'backend/module/type'; import { ModuleConfig } from 'types/module'; class HttpModule extends ModuleType { - name: 'HTTP'; + name: 'HTTP'; - fire(config: ModuleConfig): void { - if (config.type !== 'http') { - return; - } + fire(config: ModuleConfig): void { + if (config.type !== 'http') { + return; + } - console.log(`[module/gpio] Triggering HTTP ${config.url}...`); + console.log(`[module/gpio] Triggering HTTP ${config.url}...`); - // TODO: not important for now - } + // TODO: not important for now + } } export default new HttpModule(); diff --git a/backend/parser/firebase.ts b/backend/parser/firebase.ts index 9190322f..3b0cfd95 100644 --- a/backend/parser/firebase.ts +++ b/backend/parser/firebase.ts @@ -1,45 +1,45 @@ class FirebaseDataParser { - convertObjectArraysToArrays(data) { - if (data === null || typeof data !== 'object') { - return data; - } + convertObjectArraysToArrays(data) { + if (data === null || typeof data !== 'object') { + return data; + } - if (this.shouldObjectBeArray(data)) { - const actualArray = this.convertObjectToArray(data); + if (this.shouldObjectBeArray(data)) { + const actualArray = this.convertObjectToArray(data); - return actualArray.map((arrayItem) => this.convertObjectArraysToArrays(arrayItem)); - } + return actualArray.map((arrayItem) => this.convertObjectArraysToArrays(arrayItem)); + } - Object.keys(data).map((objectKey) => { - data[objectKey] = this.convertObjectArraysToArrays(data[objectKey]); - }); + Object.keys(data).map((objectKey) => { + data[objectKey] = this.convertObjectArraysToArrays(data[objectKey]); + }); - return data; - } + return data; + } - shouldObjectBeArray(dataObject) { - let arrayKeyCount = 0; - let shouldBeArray = true; + shouldObjectBeArray(dataObject) { + let arrayKeyCount = 0; + let shouldBeArray = true; - Object.keys(dataObject).map((objectKey) => { - if (isNaN(parseInt(objectKey)) || parseInt(objectKey) !== arrayKeyCount) { - shouldBeArray = false; - } - arrayKeyCount++; - }); + Object.keys(dataObject).map((objectKey) => { + if (isNaN(parseInt(objectKey)) || parseInt(objectKey) !== arrayKeyCount) { + shouldBeArray = false; + } + arrayKeyCount++; + }); - return shouldBeArray; - } + return shouldBeArray; + } - convertObjectToArray(arrayObject) { - const actualArray = []; + convertObjectToArray(arrayObject) { + const actualArray = []; - Object.keys(arrayObject).map((objectKey) => { - actualArray.push(arrayObject[objectKey]); - }); + Object.keys(arrayObject).map((objectKey) => { + actualArray.push(arrayObject[objectKey]); + }); - return actualArray; - } + return actualArray; + } } export default new FirebaseDataParser(); diff --git a/backend/parser/github/helper.ts b/backend/parser/github/helper.ts index f7250ad2..a59a1c86 100644 --- a/backend/parser/github/helper.ts +++ b/backend/parser/github/helper.ts @@ -2,57 +2,57 @@ import { GitHubConclusion, GitHubStatus } from 'types/github'; import { State, StepState } from 'types/status'; export const getStateFromStatus = (status: GitHubStatus, conclusion: GitHubConclusion): State => { - if (conclusion !== null) { - if (conclusion === 'failure') { - return 'error'; - } + if (conclusion !== null) { + if (conclusion === 'failure') { + return 'error'; + } - return 'success'; - } + return 'success'; + } - if (['queued', 'in_progress'].includes(status)) { - return 'warning'; - } + if (['queued', 'in_progress'].includes(status)) { + return 'warning'; + } - return 'info'; + return 'info'; }; export const getJobStateFromStatus = (status: GitHubStatus, conclusion: GitHubConclusion): StepState => { - if (conclusion !== null) { - if (conclusion === 'failure') { - return 'failed'; - } + if (conclusion !== null) { + if (conclusion === 'failure') { + return 'failed'; + } - if (conclusion === 'skipped') { - return 'skipped'; - } + if (conclusion === 'skipped') { + return 'skipped'; + } - return 'success'; - } + return 'success'; + } - if (status === 'in_progress') { - return 'running'; - } + if (status === 'in_progress') { + return 'running'; + } - if (status === 'queued') { - return 'pending'; - } + if (status === 'queued') { + return 'pending'; + } - return 'created'; + return 'created'; }; export const getBranch = (reference: string): string | null => { - if (reference.includes('refs/heads')) { - return reference.replace('refs/heads/', ''); - } + if (reference.includes('refs/heads')) { + return reference.replace('refs/heads/', ''); + } - return null; + return null; }; export const getTag = (reference: string): string | null => { - if (reference.includes('refs/tags')) { - return reference.replace('refs/tags/', ''); - } + if (reference.includes('refs/tags')) { + return reference.replace('refs/tags/', ''); + } - return null; + return null; }; diff --git a/backend/parser/github/index.ts b/backend/parser/github/index.ts index a2181275..d496cbf0 100644 --- a/backend/parser/github/index.ts +++ b/backend/parser/github/index.ts @@ -8,45 +8,45 @@ import GitHubPushParser from './push'; import GitHubRunParser from './run'; class GitLabParser { - getInternalId(projectId: number, repositoryName: string, uniqueElement: string): string { - const base = `github-${projectId}-${Slugify(repositoryName)}`; + getInternalId(projectId: number, repositoryName: string, uniqueElement: string): string { + const base = `github-${projectId}-${Slugify(repositoryName)}`; - return `${base}-${Slugify(uniqueElement.replace('refs/tags/', '').replace('refs/heads/', ''))}`; - } + return `${base}-${Slugify(uniqueElement.replace('refs/tags/', '').replace('refs/heads/', ''))}`; + } - parsePush(push: GitHubPush): Status { - console.log('[parser/github] Parsing push...'); + parsePush(push: GitHubPush): Status { + console.log('[parser/github] Parsing push...'); - const id = this.getInternalId(push.repository.id, push.repository.name, push.ref); + const id = this.getInternalId(push.repository.id, push.repository.name, push.ref); - return GitHubPushParser.parse(id, push); - } + return GitHubPushParser.parse(id, push); + } - parseWorkflowRun(run: GitHubWorkflowRun): Status { - console.log('[parser/github] Parsing workflow run...'); + parseWorkflowRun(run: GitHubWorkflowRun): Status { + console.log('[parser/github] Parsing workflow run...'); - const id = this.getInternalId(run.repository.id, run.repository.name, run.workflow_run.head_branch); + const id = this.getInternalId(run.repository.id, run.repository.name, run.workflow_run.head_branch); - return GitHubRunParser.parse(id, run); - } + return GitHubRunParser.parse(id, run); + } - parseWorkflowJob(job: GitHubWorkflowJob): Status | null { - console.log('[parser/github] Parsing workflow job...'); + parseWorkflowJob(job: GitHubWorkflowJob): Status | null { + console.log('[parser/github] Parsing workflow job...'); - return GitHubJobParser.parse(job); - } + return GitHubJobParser.parse(job); + } - parsePullRequest(pullRequest: GitHubPullRequest): Status | null { - console.log('[parser/github] Parsing pull rquest...'); + parsePullRequest(pullRequest: GitHubPullRequest): Status | null { + console.log('[parser/github] Parsing pull rquest...'); - const id = this.getInternalId( - pullRequest.repository.id, - pullRequest.repository.name, - pullRequest.pull_request.head.ref - ); + const id = this.getInternalId( + pullRequest.repository.id, + pullRequest.repository.name, + pullRequest.pull_request.head.ref + ); - return GitHubPullRequestParser.parse(id, pullRequest); - } + return GitHubPullRequestParser.parse(id, pullRequest); + } } export default new GitLabParser(); diff --git a/backend/parser/github/job.ts b/backend/parser/github/job.ts index f0a3c70b..9a9555b7 100644 --- a/backend/parser/github/job.ts +++ b/backend/parser/github/job.ts @@ -6,131 +6,131 @@ import Status, { Process, Stage, Step, StepState } from 'types/status'; import { getJobStateFromStatus } from './helper'; class GitHubJobParser { - parse(job: GitHubWorkflowJob): Status | null { - const statuses = StatusManager.getStatuses(); - - const processId = job.workflow_job.run_id; - - const targetStatus = statuses.find((status) => status.processes.find((process) => process.id === processId)); - - if (!targetStatus) { - console.log('[parser/github/job] No status with matching process is found, skipping update.'); - return null; - } - - return { - ...targetStatus, - processes: targetStatus.processes.map((process) => { - if (process.id === processId) { - return this.patchProcess(process, job); - } - - return process; - }), - time: new Date().toUTCString(), - }; - } - - patchProcess(process: Process, job: GitHubWorkflowJob): Process { - const stageId = `job-${job.workflow_job.id}`; - - let stages = process.stages; - - if (!process.stages.find((stage) => stage.id === stageId)) { - stages.push({ - id: stageId, - title: job.workflow_job.name, - state: getJobStateFromStatus(job.workflow_job.status, job.workflow_job.conclusion), - steps: [], - time: new Date().toUTCString(), - }); - } - - stages = stages.map((stage) => { - if (stage.id === stageId) { - return this.patchStage(stage, job); - } - - return stage; - }); - - return { - ...process, - stages, - }; - } - - isStepNotBlacklisted(stepId: string): boolean { - const blacklist = ['set-up-job', 'complete-job', 'checkout-branch', /^post-/]; - - for (let bannedItem of blacklist) { - if (bannedItem instanceof RegExp && bannedItem.test(stepId)) { - return false; - } - - if (typeof bannedItem === 'string' && bannedItem === stepId) { - return false; - } - } - - return true; - } - - patchStage(stage: Stage, job: GitHubWorkflowJob): Stage { - let steps = stage.steps; - - for (let step of job.workflow_job.steps) { - const stepId = slug(step.name); - - if (!steps.find((step) => step.id === stepId)) { - if (this.isStepNotBlacklisted(stepId)) { - steps.push({ - id: stepId, - title: step.name, - state: getJobStateFromStatus(step.status, step.conclusion), - time: new Date().toUTCString(), - }); - } - } - } - - steps = steps.map((stageStep) => { - const jobStep = job.workflow_job.steps.find((jobStep) => slug(jobStep.name) === stageStep.id); - - if (jobStep) { - return { - ...stageStep, - state: getJobStateFromStatus(jobStep.status, jobStep.conclusion), - time: new Date().toUTCString(), - }; - } - - return stageStep; - }); - - return { - ...stage, - steps, - state: this.determineStageState(steps), - time: new Date().toUTCString(), - }; - } - - determineStageState(steps: Step[]): StepState { - if (steps.length === 0) { - return 'running'; - } - - if (steps.find((step) => ['running', 'pending'].includes(step.state))) { - return 'running'; - } - - if (steps.find((step) => step.state === 'failed')) { - return 'failed'; - } - - return 'success'; - } + parse(job: GitHubWorkflowJob): Status | null { + const statuses = StatusManager.getStatuses(); + + const processId = job.workflow_job.run_id; + + const targetStatus = statuses.find((status) => status.processes.find((process) => process.id === processId)); + + if (!targetStatus) { + console.log('[parser/github/job] No status with matching process is found, skipping update.'); + return null; + } + + return { + ...targetStatus, + processes: targetStatus.processes.map((process) => { + if (process.id === processId) { + return this.patchProcess(process, job); + } + + return process; + }), + time: new Date().toUTCString(), + }; + } + + patchProcess(process: Process, job: GitHubWorkflowJob): Process { + const stageId = `job-${job.workflow_job.id}`; + + let stages = process.stages; + + if (!process.stages.find((stage) => stage.id === stageId)) { + stages.push({ + id: stageId, + title: job.workflow_job.name, + state: getJobStateFromStatus(job.workflow_job.status, job.workflow_job.conclusion), + steps: [], + time: new Date().toUTCString(), + }); + } + + stages = stages.map((stage) => { + if (stage.id === stageId) { + return this.patchStage(stage, job); + } + + return stage; + }); + + return { + ...process, + stages, + }; + } + + isStepNotBlacklisted(stepId: string): boolean { + const blacklist = ['set-up-job', 'complete-job', 'checkout-branch', /^post-/]; + + for (let bannedItem of blacklist) { + if (bannedItem instanceof RegExp && bannedItem.test(stepId)) { + return false; + } + + if (typeof bannedItem === 'string' && bannedItem === stepId) { + return false; + } + } + + return true; + } + + patchStage(stage: Stage, job: GitHubWorkflowJob): Stage { + let steps = stage.steps; + + for (let step of job.workflow_job.steps) { + const stepId = slug(step.name); + + if (!steps.find((step) => step.id === stepId)) { + if (this.isStepNotBlacklisted(stepId)) { + steps.push({ + id: stepId, + title: step.name, + state: getJobStateFromStatus(step.status, step.conclusion), + time: new Date().toUTCString(), + }); + } + } + } + + steps = steps.map((stageStep) => { + const jobStep = job.workflow_job.steps.find((jobStep) => slug(jobStep.name) === stageStep.id); + + if (jobStep) { + return { + ...stageStep, + state: getJobStateFromStatus(jobStep.status, jobStep.conclusion), + time: new Date().toUTCString(), + }; + } + + return stageStep; + }); + + return { + ...stage, + steps, + state: this.determineStageState(steps), + time: new Date().toUTCString(), + }; + } + + determineStageState(steps: Step[]): StepState { + if (steps.length === 0) { + return 'running'; + } + + if (steps.find((step) => ['running', 'pending'].includes(step.state))) { + return 'running'; + } + + if (steps.find((step) => step.state === 'failed')) { + return 'failed'; + } + + return 'success'; + } } export default new GitHubJobParser(); diff --git a/backend/parser/github/pull-request.ts b/backend/parser/github/pull-request.ts index 6254f8fb..b218603e 100644 --- a/backend/parser/github/pull-request.ts +++ b/backend/parser/github/pull-request.ts @@ -3,33 +3,33 @@ import { GitHubPullRequest } from 'types/github'; import Status from 'types/status'; class GitHubPullRequestParser { - parse(id: string, pullRequest: GitHubPullRequest): Status { - let status = StatusManager.getStatus(id); + parse(id: string, pullRequest: GitHubPullRequest): Status { + let status = StatusManager.getStatus(id); - if (!status) { - status = { - id, - project: `${pullRequest.organization.login} / ${pullRequest.repository.name}`, - state: 'info', - source: 'github', - time: new Date().toUTCString(), - processes: [], - branch: pullRequest.pull_request.head.ref, - }; - } + if (!status) { + status = { + id, + project: `${pullRequest.organization.login} / ${pullRequest.repository.name}`, + state: 'info', + source: 'github', + time: new Date().toUTCString(), + processes: [], + branch: pullRequest.pull_request.head.ref, + }; + } - return { - ...status, - username: pullRequest.sender.login, - userUrl: pullRequest.sender.html_url, - userImage: pullRequest.sender.avatar_url, - projectImage: pullRequest.organization.avatar_url, - sourceUrl: pullRequest.repository.html_url, - mergeTitle: pullRequest.pull_request.title, - mergeUrl: pullRequest.pull_request.html_url, - time: new Date().toUTCString(), - }; - } + return { + ...status, + username: pullRequest.sender.login, + userUrl: pullRequest.sender.html_url, + userImage: pullRequest.sender.avatar_url, + projectImage: pullRequest.organization.avatar_url, + sourceUrl: pullRequest.repository.html_url, + mergeTitle: pullRequest.pull_request.title, + mergeUrl: pullRequest.pull_request.html_url, + time: new Date().toUTCString(), + }; + } } export default new GitHubPullRequestParser(); diff --git a/backend/parser/github/push.ts b/backend/parser/github/push.ts index 833f7786..0b23f398 100644 --- a/backend/parser/github/push.ts +++ b/backend/parser/github/push.ts @@ -5,40 +5,40 @@ import Status from 'types/status'; import { getBranch, getTag } from './helper'; class GitHubPushParser { - parse(id: string, push: GitHubPush): Status { - let status = StatusManager.getStatus(id); + parse(id: string, push: GitHubPush): Status { + let status = StatusManager.getStatus(id); - if (!status) { - status = { - id, - project: `${push.organization.login} / ${push.repository.name}`, - state: 'info', - source: 'github', - time: new Date().toUTCString(), - processes: [], - }; + if (!status) { + status = { + id, + project: `${push.organization.login} / ${push.repository.name}`, + state: 'info', + source: 'github', + time: new Date().toUTCString(), + processes: [], + }; - const branch = getBranch(push.ref); - if (branch) { - status.branch = branch; - } + const branch = getBranch(push.ref); + if (branch) { + status.branch = branch; + } - const tag = getTag(push.ref); - if (tag) { - status.tag = tag; - } - } + const tag = getTag(push.ref); + if (tag) { + status.tag = tag; + } + } - return { - ...status, - username: push.sender.login, - userUrl: push.sender.html_url, - userImage: push.sender.avatar_url, - projectImage: push.organization.avatar_url, - sourceUrl: push.repository.html_url, - time: new Date().toUTCString(), - }; - } + return { + ...status, + username: push.sender.login, + userUrl: push.sender.html_url, + userImage: push.sender.avatar_url, + projectImage: push.organization.avatar_url, + sourceUrl: push.repository.html_url, + time: new Date().toUTCString(), + }; + } } export default new GitHubPushParser(); diff --git a/backend/parser/github/run.ts b/backend/parser/github/run.ts index 399dc0c4..56d1eae4 100644 --- a/backend/parser/github/run.ts +++ b/backend/parser/github/run.ts @@ -6,61 +6,61 @@ import Status from 'types/status'; import { getStateFromStatus } from './helper'; class GitHubRunParser { - parse(id: string, run: GitHubWorkflowRun): Status | null { - let status = StatusManager.getStatus(id); + parse(id: string, run: GitHubWorkflowRun): Status | null { + let status = StatusManager.getStatus(id); - if (!status) { - status = { - id, - project: `${run.organization.login} / ${run.repository.name}`, - state: 'info', - source: 'github', - branch: run.workflow_run.head_branch, - time: new Date().toUTCString(), - processes: [], - }; - } + if (!status) { + status = { + id, + project: `${run.organization.login} / ${run.repository.name}`, + state: 'info', + source: 'github', + branch: run.workflow_run.head_branch, + time: new Date().toUTCString(), + processes: [], + }; + } - let processes = status.processes; + let processes = status.processes; - const processId = run.workflow_run.id; + const processId = run.workflow_run.id; - if (!processes.find((process) => process.id === processId)) { - if (isOldProcess(status, processId)) { - return null; - } + if (!processes.find((process) => process.id === processId)) { + if (isOldProcess(status, processId)) { + return null; + } - processes.push({ - id: processId, - title: run.workflow_run.head_commit.message, - state: 'info', - stages: [], - time: new Date().toUTCString(), - }); - } + processes.push({ + id: processId, + title: run.workflow_run.head_commit.message, + state: 'info', + stages: [], + time: new Date().toUTCString(), + }); + } - processes = processes.map((process) => { - if (process.id === processId) { - return { - ...process, - state: getStateFromStatus(run.workflow_run.status, run.workflow_run.conclusion), - }; - } + processes = processes.map((process) => { + if (process.id === processId) { + return { + ...process, + state: getStateFromStatus(run.workflow_run.status, run.workflow_run.conclusion), + }; + } - return process; - }); + return process; + }); - return { - ...status, - username: run.sender.login, - userUrl: run.sender.html_url, - userImage: run.sender.avatar_url, - projectImage: run.organization.avatar_url, - sourceUrl: run.repository.html_url, - processes, - time: new Date().toUTCString(), - }; - } + return { + ...status, + username: run.sender.login, + userUrl: run.sender.html_url, + userImage: run.sender.avatar_url, + projectImage: run.organization.avatar_url, + sourceUrl: run.repository.html_url, + processes, + time: new Date().toUTCString(), + }; + } } export default new GitHubRunParser(); diff --git a/backend/parser/gitlab/build.ts b/backend/parser/gitlab/build.ts index 82b89b5e..037968d6 100644 --- a/backend/parser/gitlab/build.ts +++ b/backend/parser/gitlab/build.ts @@ -7,149 +7,149 @@ import Status, { Process, Stage, Step, StepState } from 'types/status'; import { statusToStepState } from './helper'; class GitLabBuildParser { - parse(id: string, build: GitLabBuild): Status | null { - if (build.build_status === 'created') { - return null; - } - - const status = this.getStatus(id, build); - - const processes: Process[] = status.processes || []; - - const processId = build.pipeline_id; - - if (!processes.find((process) => process.id === processId)) { - if (isOldProcess(status, processId)) { - return null; - } - - processes.push({ - id: processId, - title: build.commit.message.split('\n\n')[0], - state: 'info', - stages: [], - time: new Date().toUTCString(), - }); - } - - return { - ...status, - processes: processes.map((process) => { - if (process.id === processId) { - return this.patchProcess(process, build); - } - - return process; - }), - time: new Date().toUTCString(), - }; - } - - getStatus(id: string, build: GitLabBuild): Status { - let status = StatusManager.getStatus(id); - - if (!status) { - status = { - id, - project: build.project_name, - state: 'info', - source: 'gitlab', - processes: [], - time: new Date().toUTCString(), - }; - - if (build.tag) { - status.tag = build.tag; - } - - if (build.ref) { - status.branch = build.ref; - } - } - - status.username = build.user.name || build.user.username; - status.userUrl = build.commit.author_url; - status.userImage = build.user.avatar_url; - status.sourceUrl = build.repository.homepage; - - return status; - } - - patchProcess(process: Process, build: GitLabBuild): Process { - let stages: Stage[] = process.stages; - - const stageId = Slugify(build.build_stage); - - if (!stages.find((stage) => stage.id === stageId)) { - stages.push({ - id: stageId, - title: build.build_stage, - state: 'created', - steps: [], - time: new Date().toUTCString(), - }); - } - - stages = stages.map((stage) => { - if (stage.id === stageId) { - return this.patchStage(stage, build); - } - - return stage; - }); - - return { - ...process, - stages, - }; - } - - patchStage(stage: Stage, build: GitLabBuild): Stage { - let steps: Step[] = stage.steps; - - const stepId = Slugify(build.build_name); - - if (!steps.find((step) => step.id === stepId)) { - steps.push({ - id: stepId, - title: build.build_name, - state: 'created', - time: new Date().toUTCString(), - }); - } - - steps = steps.map((step) => { - if (step.id === stepId) { - step.state = statusToStepState(build.build_status, build.build_allow_failure); - step.time = new Date().toUTCString(); - } - - return step; - }); - - return { - ...stage, - steps, - state: this.determineStageState(steps), - time: new Date().toUTCString(), - }; - } - - determineStageState(steps: Step[]): StepState { - if (steps.find((step) => ['failed'].includes(step.state))) { - return 'failed'; - } - - if (steps.find((step) => ['running'].includes(step.state))) { - return 'running'; - } - - if (steps.find((step) => ['created', 'pending'].includes(step.state))) { - return 'pending'; - } - - return 'success'; - } + parse(id: string, build: GitLabBuild): Status | null { + if (build.build_status === 'created') { + return null; + } + + const status = this.getStatus(id, build); + + const processes: Process[] = status.processes || []; + + const processId = build.pipeline_id; + + if (!processes.find((process) => process.id === processId)) { + if (isOldProcess(status, processId)) { + return null; + } + + processes.push({ + id: processId, + title: build.commit.message.split('\n\n')[0], + state: 'info', + stages: [], + time: new Date().toUTCString(), + }); + } + + return { + ...status, + processes: processes.map((process) => { + if (process.id === processId) { + return this.patchProcess(process, build); + } + + return process; + }), + time: new Date().toUTCString(), + }; + } + + getStatus(id: string, build: GitLabBuild): Status { + let status = StatusManager.getStatus(id); + + if (!status) { + status = { + id, + project: build.project_name, + state: 'info', + source: 'gitlab', + processes: [], + time: new Date().toUTCString(), + }; + + if (build.tag) { + status.tag = build.tag; + } + + if (build.ref) { + status.branch = build.ref; + } + } + + status.username = build.user.name || build.user.username; + status.userUrl = build.commit.author_url; + status.userImage = build.user.avatar_url; + status.sourceUrl = build.repository.homepage; + + return status; + } + + patchProcess(process: Process, build: GitLabBuild): Process { + let stages: Stage[] = process.stages; + + const stageId = Slugify(build.build_stage); + + if (!stages.find((stage) => stage.id === stageId)) { + stages.push({ + id: stageId, + title: build.build_stage, + state: 'created', + steps: [], + time: new Date().toUTCString(), + }); + } + + stages = stages.map((stage) => { + if (stage.id === stageId) { + return this.patchStage(stage, build); + } + + return stage; + }); + + return { + ...process, + stages, + }; + } + + patchStage(stage: Stage, build: GitLabBuild): Stage { + let steps: Step[] = stage.steps; + + const stepId = Slugify(build.build_name); + + if (!steps.find((step) => step.id === stepId)) { + steps.push({ + id: stepId, + title: build.build_name, + state: 'created', + time: new Date().toUTCString(), + }); + } + + steps = steps.map((step) => { + if (step.id === stepId) { + step.state = statusToStepState(build.build_status, build.build_allow_failure); + step.time = new Date().toUTCString(); + } + + return step; + }); + + return { + ...stage, + steps, + state: this.determineStageState(steps), + time: new Date().toUTCString(), + }; + } + + determineStageState(steps: Step[]): StepState { + if (steps.find((step) => ['failed'].includes(step.state))) { + return 'failed'; + } + + if (steps.find((step) => ['running'].includes(step.state))) { + return 'running'; + } + + if (steps.find((step) => ['created', 'pending'].includes(step.state))) { + return 'pending'; + } + + return 'success'; + } } export default new GitLabBuildParser(); diff --git a/backend/parser/gitlab/deployment.ts b/backend/parser/gitlab/deployment.ts index bcbbb75f..0c0c4025 100644 --- a/backend/parser/gitlab/deployment.ts +++ b/backend/parser/gitlab/deployment.ts @@ -4,40 +4,40 @@ import { GitLabDeployment } from 'types/gitlab'; import Status from 'types/status'; class GitLabDeploymentParser { - parse(id: string, deployment: GitLabDeployment): Status { - let status = StatusManager.getStatus(id); + parse(id: string, deployment: GitLabDeployment): Status { + let status = StatusManager.getStatus(id); - if (!status) { - status = { - id, - project: `${deployment.project.namespace} / ${deployment.project.name}`, - state: 'info', - source: 'gitlab', - time: new Date().toUTCString(), - processes: [], - branch: deployment.ref, - }; + if (!status) { + status = { + id, + project: `${deployment.project.namespace} / ${deployment.project.name}`, + state: 'info', + source: 'gitlab', + time: new Date().toUTCString(), + processes: [], + branch: deployment.ref, + }; - if (deployment.ref) { - status.branch = deployment.ref; - } - } + if (deployment.ref) { + status.branch = deployment.ref; + } + } - if (status.processes.length === 0) { - status.state = statusToState(deployment.status); - } + if (status.processes.length === 0) { + status.state = statusToState(deployment.status); + } - return { - ...status, - projectImage: deployment.project.avatar_url, - username: deployment.user.name || deployment.user.username, - userImage: deployment.user.avatar_url, - userUrl: deployment.user_url, - sourceUrl: deployment.project.git_http_url, - tag: deployment.environment, - time: new Date().toUTCString(), - }; - } + return { + ...status, + projectImage: deployment.project.avatar_url, + username: deployment.user.name || deployment.user.username, + userImage: deployment.user.avatar_url, + userUrl: deployment.user_url, + sourceUrl: deployment.project.git_http_url, + tag: deployment.environment, + time: new Date().toUTCString(), + }; + } } export default new GitLabDeploymentParser(); diff --git a/backend/parser/gitlab/helper.ts b/backend/parser/gitlab/helper.ts index f6294fa2..f4591f84 100644 --- a/backend/parser/gitlab/helper.ts +++ b/backend/parser/gitlab/helper.ts @@ -1,27 +1,27 @@ import { State, StepState } from 'types/status'; export const statusToState = (status: string): State => { - const gitlabStatuses = { - pending: 'warning', - running: 'warning', - failed: 'error', - success: 'success', - }; + const gitlabStatuses = { + pending: 'warning', + running: 'warning', + failed: 'error', + success: 'success', + }; - return gitlabStatuses[status] || 'info'; + return gitlabStatuses[status] || 'info'; }; export const statusToStepState = (status: string, allowFailure: boolean): StepState => { - switch (status) { - case 'failed': - return allowFailure ? 'soft-failed' : 'failed'; - case 'success': - return 'success'; - case 'running': - return 'running'; - case 'pending': - return 'pending'; - default: - return 'created'; - } + switch (status) { + case 'failed': + return allowFailure ? 'soft-failed' : 'failed'; + case 'success': + return 'success'; + case 'running': + return 'running'; + case 'pending': + return 'pending'; + default: + return 'created'; + } }; diff --git a/backend/parser/gitlab/index.ts b/backend/parser/gitlab/index.ts index ede9ed1c..e9d80ac2 100644 --- a/backend/parser/gitlab/index.ts +++ b/backend/parser/gitlab/index.ts @@ -8,61 +8,61 @@ import GitLabMergeRequestParser from './merge-request'; import GitLabPipelineParser from './pipeline'; class GitLabParser { - getInternalId(projectId: number, repositoryName: string, branch: string | false, tag: string | false): string { - let id = `gitlab-${projectId}-${Slugify(repositoryName)}`; + getInternalId(projectId: number, repositoryName: string, branch: string | false, tag: string | false): string { + let id = `gitlab-${projectId}-${Slugify(repositoryName)}`; - if (branch) { - id += `-${Slugify(branch)}`; - } + if (branch) { + id += `-${Slugify(branch)}`; + } - if (tag) { - id += `-${Slugify(tag)}`; - } + if (tag) { + id += `-${Slugify(tag)}`; + } - return id; - } + return id; + } - parseBuild(build: GitLabBuild): Status { - console.log('[parser/gitlab] Parsing build...'); + parseBuild(build: GitLabBuild): Status { + console.log('[parser/gitlab] Parsing build...'); - const id = this.getInternalId(build.project_id, build.repository.name, build.ref, build.tag); + const id = this.getInternalId(build.project_id, build.repository.name, build.ref, build.tag); - return GitLabBuildParser.parse(id, build); - } + return GitLabBuildParser.parse(id, build); + } - parsePipeline(pipeline: GitLabPipeline): Status { - console.log('[parser/gitlab] Parsing pipeline...'); + parsePipeline(pipeline: GitLabPipeline): Status { + console.log('[parser/gitlab] Parsing pipeline...'); - const id = this.getInternalId( - pipeline.project.id, - pipeline.project.name, - pipeline.object_attributes.ref, - pipeline.object_attributes.tag - ); + const id = this.getInternalId( + pipeline.project.id, + pipeline.project.name, + pipeline.object_attributes.ref, + pipeline.object_attributes.tag + ); - return GitLabPipelineParser.parse(id, pipeline); - } + return GitLabPipelineParser.parse(id, pipeline); + } - parseDeployment(deployment: GitLabDeployment): Status { - console.log('[parser/gitlab] Parsing deployment...'); + parseDeployment(deployment: GitLabDeployment): Status { + console.log('[parser/gitlab] Parsing deployment...'); - const id = this.getInternalId(deployment.project.id, deployment.project.name, deployment.ref, false); + const id = this.getInternalId(deployment.project.id, deployment.project.name, deployment.ref, false); - return GitLabDeploymentParser.parse(id, deployment); - } + return GitLabDeploymentParser.parse(id, deployment); + } - parseMergeRequest(mergeRequest: GitLabMergeRequest): Status { - console.log('[parser/gitlab] Parsing merge request...'); + parseMergeRequest(mergeRequest: GitLabMergeRequest): Status { + console.log('[parser/gitlab] Parsing merge request...'); - const id = this.getInternalId( - mergeRequest.project.id, - mergeRequest.project.name, - mergeRequest.object_attributes.source_branch, - false - ); + const id = this.getInternalId( + mergeRequest.project.id, + mergeRequest.project.name, + mergeRequest.object_attributes.source_branch, + false + ); - return GitLabMergeRequestParser.parse(id, mergeRequest); - } + return GitLabMergeRequestParser.parse(id, mergeRequest); + } } export default new GitLabParser(); diff --git a/backend/parser/gitlab/merge-request.ts b/backend/parser/gitlab/merge-request.ts index d88e650e..da2bf8b9 100644 --- a/backend/parser/gitlab/merge-request.ts +++ b/backend/parser/gitlab/merge-request.ts @@ -3,36 +3,36 @@ import { GitLabMergeRequest } from 'types/gitlab'; import Status from 'types/status'; class GitLabMergeRequestParser { - parse(id: string, mergeRequest: GitLabMergeRequest): Status { - let status = StatusManager.getStatus(id); + parse(id: string, mergeRequest: GitLabMergeRequest): Status { + let status = StatusManager.getStatus(id); - if (!status) { - status = { - id, - project: `${mergeRequest.project.namespace} / ${mergeRequest.project.name}`, - state: 'info', - source: 'gitlab', - time: new Date().toUTCString(), - processes: [], - branch: mergeRequest.object_attributes.source_branch, - }; + if (!status) { + status = { + id, + project: `${mergeRequest.project.namespace} / ${mergeRequest.project.name}`, + state: 'info', + source: 'gitlab', + time: new Date().toUTCString(), + processes: [], + branch: mergeRequest.object_attributes.source_branch, + }; - if (mergeRequest.object_attributes.source_branch) { - status.branch = mergeRequest.object_attributes.source_branch; - } - } + if (mergeRequest.object_attributes.source_branch) { + status.branch = mergeRequest.object_attributes.source_branch; + } + } - return { - ...status, - projectImage: mergeRequest.project.avatar_url, - username: mergeRequest.user.name || mergeRequest.user.username, - userImage: mergeRequest.user.avatar_url, - sourceUrl: mergeRequest.project.git_http_url, - mergeTitle: mergeRequest.object_attributes.title, - mergeUrl: mergeRequest.object_attributes.url, - time: new Date().toUTCString(), - }; - } + return { + ...status, + projectImage: mergeRequest.project.avatar_url, + username: mergeRequest.user.name || mergeRequest.user.username, + userImage: mergeRequest.user.avatar_url, + sourceUrl: mergeRequest.project.git_http_url, + mergeTitle: mergeRequest.object_attributes.title, + mergeUrl: mergeRequest.object_attributes.url, + time: new Date().toUTCString(), + }; + } } export default new GitLabMergeRequestParser(); diff --git a/backend/parser/gitlab/pipeline.ts b/backend/parser/gitlab/pipeline.ts index 38a3f233..0156a63e 100644 --- a/backend/parser/gitlab/pipeline.ts +++ b/backend/parser/gitlab/pipeline.ts @@ -7,101 +7,101 @@ import Status, { Process, Stage } from 'types/status'; import { statusToState } from './helper'; class GitLabPipelineParser { - parse(id: string, pipeline: GitLabPipeline): Status | null { - const status = this.getStatus(id, pipeline); - - let processes: Process[] = status.processes || []; - - const processId = pipeline.object_attributes.id; - - if (!processes.find((process) => process.id === processId)) { - if (isOldProcess(status, processId)) { - return null; - } - - processes.push({ - id: processId, - title: pipeline.commit.title, - state: 'info', - stages: [], - time: new Date().toUTCString(), - }); - } - - processes = processes.map((process) => { - if (process.id === processId) { - return this.patchProcess(process, pipeline); - } - - return process; - }); - - return { - ...status, - processes, - time: new Date().toUTCString(), - }; - } - - getStatus(id: string, pipeline: GitLabPipeline): Status { - let status = StatusManager.getStatus(id); - - if (!status) { - status = { - id, - project: `${pipeline.project.namespace} / ${pipeline.project.name}`, - state: 'info', - source: 'gitlab', - time: new Date().toUTCString(), - processes: [], - }; - - if (pipeline.object_attributes.tag) { - status.tag = pipeline.object_attributes.tag; - } - - if (pipeline.object_attributes.ref) { - status.branch = pipeline.object_attributes.ref; - } - } - - status.username = pipeline.user.name || pipeline.user.username; - status.userImage = pipeline.user.avatar_url; - status.projectImage = pipeline.project.avatar_url; - status.sourceUrl = pipeline.project.git_http_url; - - return status; - } - - patchProcess(process: Process, pipeline: GitLabPipeline): Process { - let stages: Stage[] = process.stages; - const pipelineStages = pipeline.object_attributes.stages; - - for (let stage of pipelineStages) { - const stageId = Slugify(stage); - - if (!stages.find((stage) => stage.id === stageId)) { - stages.push({ - id: stageId, - title: stage, - state: 'pending', - steps: [], - time: new Date().toUTCString(), - }); - } - } - - const state = statusToState(pipeline.object_attributes.status); - - return { - ...process, - stages: stages.sort( - (stageA: Stage, stageB: Stage): number => - pipelineStages.indexOf(stageA.title) - pipelineStages.indexOf(stageB.title) - ), - state, - }; - } + parse(id: string, pipeline: GitLabPipeline): Status | null { + const status = this.getStatus(id, pipeline); + + let processes: Process[] = status.processes || []; + + const processId = pipeline.object_attributes.id; + + if (!processes.find((process) => process.id === processId)) { + if (isOldProcess(status, processId)) { + return null; + } + + processes.push({ + id: processId, + title: pipeline.commit.title, + state: 'info', + stages: [], + time: new Date().toUTCString(), + }); + } + + processes = processes.map((process) => { + if (process.id === processId) { + return this.patchProcess(process, pipeline); + } + + return process; + }); + + return { + ...status, + processes, + time: new Date().toUTCString(), + }; + } + + getStatus(id: string, pipeline: GitLabPipeline): Status { + let status = StatusManager.getStatus(id); + + if (!status) { + status = { + id, + project: `${pipeline.project.namespace} / ${pipeline.project.name}`, + state: 'info', + source: 'gitlab', + time: new Date().toUTCString(), + processes: [], + }; + + if (pipeline.object_attributes.tag) { + status.tag = pipeline.object_attributes.tag; + } + + if (pipeline.object_attributes.ref) { + status.branch = pipeline.object_attributes.ref; + } + } + + status.username = pipeline.user.name || pipeline.user.username; + status.userImage = pipeline.user.avatar_url; + status.projectImage = pipeline.project.avatar_url; + status.sourceUrl = pipeline.project.git_http_url; + + return status; + } + + patchProcess(process: Process, pipeline: GitLabPipeline): Process { + let stages: Stage[] = process.stages; + const pipelineStages = pipeline.object_attributes.stages; + + for (let stage of pipelineStages) { + const stageId = Slugify(stage); + + if (!stages.find((stage) => stage.id === stageId)) { + stages.push({ + id: stageId, + title: stage, + state: 'pending', + steps: [], + time: new Date().toUTCString(), + }); + } + } + + const state = statusToState(pipeline.object_attributes.status); + + return { + ...process, + stages: stages.sort( + (stageA: Stage, stageB: Stage): number => + pipelineStages.indexOf(stageA.title) - pipelineStages.indexOf(stageB.title) + ), + state, + }; + } } export default new GitLabPipelineParser(); diff --git a/backend/parser/readthedocs.ts b/backend/parser/readthedocs.ts index 47e3c06c..61ea6621 100644 --- a/backend/parser/readthedocs.ts +++ b/backend/parser/readthedocs.ts @@ -5,112 +5,112 @@ import ReadTheDocsBuild from 'types/readthedocs'; import Status, { Process, State, StepState } from 'types/status'; class ReadTheDocsParser { - getState(event: string): State { - if (event === 'build:passed') { - return 'success'; - } - - if (event === 'build:failed') { - return 'error'; - } - - return 'warning'; - } - - getStepState(event: string): StepState { - if (event === 'build:passed') { - return 'success'; - } - - if (event === 'build:failed') { - return 'failed'; - } - - return 'running'; - } - - parseBuild(build: ReadTheDocsBuild): Status | null { - console.log('[parser/readthedocs] Parsing build...'); - - const statusId = `readthedocs-${build.slug}-${Slugify(build.version)}`; - - let status = StatusManager.getStatus(statusId); - - if (!status) { - status = { - id: statusId, - project: build.name, - tag: build.version, - source: 'readthedocs', - state: 'warning', - processes: [], - time: new Date().toUTCString(), - }; - } - - let processes: Process[] = status.processes || []; - - const processId = parseInt(build.build); - - if (!processes.find((process) => process.id === processId)) { - if (isOldProcess(status, processId)) { - return null; - } - - processes.push({ - id: processId, - title: `Build ${build.build}`, - state: 'warning', - stages: [], - time: new Date().toUTCString(), - }); - } - - processes = processes.map((process) => { - if (process.id === processId) { - return this.patchProcess(process, build); - } - - return process; - }); - - return { - ...status, - processes, - url: build.docs_url, - sourceUrl: build.build_url, - state: this.determineState(processes), - time: new Date().toUTCString(), - }; - } - - determineState(processes: Process[]): State { - if (processes.find((process) => process.state === 'warning')) { - return 'warning'; - } - - if (processes.find((process) => process.state === 'error')) { - return 'error'; - } - - return 'success'; - } - - patchProcess(process: Process, build: ReadTheDocsBuild): Process { - return { - ...process, - stages: [ - { - id: 'build', - steps: [], - time: new Date().toUTCString(), - state: this.getStepState(build.event), - title: 'Building documentation', - }, - ], - state: this.getState(build.event), - }; - } + getState(event: string): State { + if (event === 'build:passed') { + return 'success'; + } + + if (event === 'build:failed') { + return 'error'; + } + + return 'warning'; + } + + getStepState(event: string): StepState { + if (event === 'build:passed') { + return 'success'; + } + + if (event === 'build:failed') { + return 'failed'; + } + + return 'running'; + } + + parseBuild(build: ReadTheDocsBuild): Status | null { + console.log('[parser/readthedocs] Parsing build...'); + + const statusId = `readthedocs-${build.slug}-${Slugify(build.version)}`; + + let status = StatusManager.getStatus(statusId); + + if (!status) { + status = { + id: statusId, + project: build.name, + tag: build.version, + source: 'readthedocs', + state: 'warning', + processes: [], + time: new Date().toUTCString(), + }; + } + + let processes: Process[] = status.processes || []; + + const processId = parseInt(build.build); + + if (!processes.find((process) => process.id === processId)) { + if (isOldProcess(status, processId)) { + return null; + } + + processes.push({ + id: processId, + title: `Build ${build.build}`, + state: 'warning', + stages: [], + time: new Date().toUTCString(), + }); + } + + processes = processes.map((process) => { + if (process.id === processId) { + return this.patchProcess(process, build); + } + + return process; + }); + + return { + ...status, + processes, + url: build.docs_url, + sourceUrl: build.build_url, + state: this.determineState(processes), + time: new Date().toUTCString(), + }; + } + + determineState(processes: Process[]): State { + if (processes.find((process) => process.state === 'warning')) { + return 'warning'; + } + + if (processes.find((process) => process.state === 'error')) { + return 'error'; + } + + return 'success'; + } + + patchProcess(process: Process, build: ReadTheDocsBuild): Process { + return { + ...process, + stages: [ + { + id: 'build', + steps: [], + time: new Date().toUTCString(), + state: this.getStepState(build.event), + title: 'Building documentation', + }, + ], + state: this.getState(build.event), + }; + } } export default new ReadTheDocsParser(); diff --git a/backend/parser/slug.ts b/backend/parser/slug.ts index 4b0e62ff..0c0b097d 100644 --- a/backend/parser/slug.ts +++ b/backend/parser/slug.ts @@ -1,17 +1,17 @@ const Slugify = (text: unknown): string => - // Make sure text is a string - String(text) - // Replace accented characters for regular ones - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') + // Make sure text is a string + String(text) + // Replace accented characters for regular ones + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') - // Lowercase the string - .toLowerCase() + // Lowercase the string + .toLowerCase() - // Replace anything that is not an alphanumeric character - .replace(/[^0-9a-z]/g, '-') + // Replace anything that is not an alphanumeric character + .replace(/[^0-9a-z]/g, '-') - // Remove double dashes - .replace(/--+/g, '-'); + // Remove double dashes + .replace(/--+/g, '-'); export default Slugify; diff --git a/backend/router/route/contributors.ts b/backend/router/route/contributors.ts index bb17eddb..c84ec258 100644 --- a/backend/router/route/contributors.ts +++ b/backend/router/route/contributors.ts @@ -5,16 +5,16 @@ import { getContributors } from 'backend/api/github'; const router = express.Router(); router.get('/', async (request, response) => { - try { - const contributors = await getContributors(); + try { + const contributors = await getContributors(); - response.json(contributors); + response.json(contributors); - console.log(`[route/contributors] Served contributors.`); - } catch (error) { - response.status(500).json({ message: 'failed to get the contributors' }); - console.log(`[route/contributors] Failed to get the contributors.`); - } + console.log(`[route/contributors] Served contributors.`); + } catch (error) { + response.status(500).json({ message: 'failed to get the contributors' }); + console.log(`[route/contributors] Failed to get the contributors.`); + } }); export default router; diff --git a/backend/router/route/dashboard.ts b/backend/router/route/dashboard.ts index cecd1f15..e58de795 100644 --- a/backend/router/route/dashboard.ts +++ b/backend/router/route/dashboard.ts @@ -6,8 +6,8 @@ const dashboardPath = path.resolve('dashboard'); const router = express.Router(); router.get('/', (request, response) => { - console.log(`[route/dashboard] Serving dashboard.`); - response.sendFile(dashboardPath + '/index.html'); + console.log(`[route/dashboard] Serving dashboard.`); + response.sendFile(dashboardPath + '/index.html'); }); router.use(express.static(dashboardPath)); diff --git a/backend/router/route/status.ts b/backend/router/route/status.ts index 55d58e7b..32d2103a 100644 --- a/backend/router/route/status.ts +++ b/backend/router/route/status.ts @@ -5,19 +5,19 @@ import StatusManager from 'backend/status/manager'; const router = express.Router(); router.delete('/all', async (request, response) => { - console.log(`[route/status] Deleting ALL statuses.`); + console.log(`[route/status] Deleting ALL statuses.`); - StatusManager.deleteAllStatuses(); + StatusManager.deleteAllStatuses(); - return response.json({ message: `Deleted all statuses.` }); + return response.json({ message: `Deleted all statuses.` }); }); router.delete('/:id', async (request, response) => { - console.log(`[route/status] Deleting status ${request.params.id}.`); + console.log(`[route/status] Deleting status ${request.params.id}.`); - StatusManager.deleteStatus(request.params.id); + StatusManager.deleteStatus(request.params.id); - return response.json({ message: `Deleted status ${request.params.id}.` }); + return response.json({ message: `Deleted status ${request.params.id}.` }); }); export default router; diff --git a/backend/router/route/version.ts b/backend/router/route/version.ts index ad2cb893..236dc88f 100644 --- a/backend/router/route/version.ts +++ b/backend/router/route/version.ts @@ -6,23 +6,23 @@ import { GitHubRelease } from 'types/github'; const router = express.Router(); router.get('/', async (request, response) => { - let latestVersion = null; - const serverVersion = process.env.npm_package_version; + let latestVersion = null; + const serverVersion = process.env.npm_package_version; - try { - const githubVersionInfo: GitHubRelease = await getLatestRelease(); + try { + const githubVersionInfo: GitHubRelease = await getLatestRelease(); - latestVersion = githubVersionInfo.tag_name; - } catch (error) { - console.log(`[route/version] Failed to fetch the latest version.`); - } + latestVersion = githubVersionInfo.tag_name; + } catch (error) { + console.log(`[route/version] Failed to fetch the latest version.`); + } - response.json({ - server: serverVersion, - latest: latestVersion, - }); + response.json({ + server: serverVersion, + latest: latestVersion, + }); - console.log(`[route/version] Returning server version ${serverVersion}, latest version is ${latestVersion}.`); + console.log(`[route/version] Returning server version ${serverVersion}, latest version is ${latestVersion}.`); }); export default router; diff --git a/backend/router/route/webhook.ts b/backend/router/route/webhook.ts index 4745e251..1c033b6a 100644 --- a/backend/router/route/webhook.ts +++ b/backend/router/route/webhook.ts @@ -9,65 +9,65 @@ import ReadTheDocsRouter from './webhook/readthedocs'; const router = express.Router(); const cleanHeaders = (headers: IncomingHttpHeaders): IncomingHttpHeaders => { - const headersToClean = [ - 'x-gitlab-event-uuid', - 'connection', - 'host', - 'content-length', - 'accept', - 'x-github-delivery', - 'x-github-hook-id', - 'x-github-hook-installation-target-id', - 'x-github-hook-installation-target-type', - 'x-hub-signature', - 'x-hub-signature-256', - ]; + const headersToClean = [ + 'x-gitlab-event-uuid', + 'connection', + 'host', + 'content-length', + 'accept', + 'x-github-delivery', + 'x-github-hook-id', + 'x-github-hook-installation-target-id', + 'x-github-hook-installation-target-type', + 'x-hub-signature', + 'x-hub-signature-256', + ]; - for (let headerToClean of headersToClean) { - delete headers[headerToClean]; - } + for (let headerToClean of headersToClean) { + delete headers[headerToClean]; + } - return headers; + return headers; }; let recordCount = 0; const shouldPersist = (request: express.Request): boolean => { - if (process.env.PERSIST_WEBHOOKS !== 'true') { - return false; - } + if (process.env.PERSIST_WEBHOOKS !== 'true') { + return false; + } - return !('x-dont-persist' in request.headers); + return !('x-dont-persist' in request.headers); }; // Save the incoming webhook body if requested in the environment variables router.use((request, response, next) => { - if (shouldPersist(request)) { - recordCount++; - const date = new Date(); - const pad = (number: number) => number.toString().padStart(2, '0'); - const pathParts = [ - 'webhooks', - request.url, - `/${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`, - ]; - let path = ''; - for (let pathPart of pathParts) { - path += pathPart; - if (!FileSystem.existsSync(path)) { - FileSystem.mkdirSync(path); - } - } - const file = `${path}/${recordCount}.json`; - const body = { - headers: cleanHeaders(request.headers), - body: request.body, - }; - FileSystem.writeFileSync(file, JSON.stringify(body, null, 4)); - console.log(`[route/webhook] Saved ${file}.`); - } + if (shouldPersist(request)) { + recordCount++; + const date = new Date(); + const pad = (number: number) => number.toString().padStart(2, '0'); + const pathParts = [ + 'webhooks', + request.url, + `/${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`, + ]; + let path = ''; + for (let pathPart of pathParts) { + path += pathPart; + if (!FileSystem.existsSync(path)) { + FileSystem.mkdirSync(path); + } + } + const file = `${path}/${recordCount}.json`; + const body = { + headers: cleanHeaders(request.headers), + body: request.body, + }; + FileSystem.writeFileSync(file, JSON.stringify(body, null, 4)); + console.log(`[route/webhook] Saved ${file}.`); + } - next(); + next(); }); router.use('/gitlab', GitLabRouter); diff --git a/backend/router/route/webhook/github.ts b/backend/router/route/webhook/github.ts index d9d869ca..49dd924f 100644 --- a/backend/router/route/webhook/github.ts +++ b/backend/router/route/webhook/github.ts @@ -7,34 +7,34 @@ import Status from 'types/status'; const router = express.Router(); router.post('/', (request, response) => { - console.log('[route/webhook/github] Webhook received.'); - - const githubWebhookType: string = String(request.headers['x-github-event']); - - let status: Status | null = null; - - switch (githubWebhookType) { - case 'push': - status = GitHubParser.parsePush(request.body); - break; - case 'workflow_run': - status = GitHubParser.parseWorkflowRun(request.body); - break; - case 'workflow_job': - status = GitHubParser.parseWorkflowJob(request.body); - break; - case 'pull_request': - status = GitHubParser.parsePullRequest(request.body); - break; - default: - console.log(`[route/webhook/github] No parser for webhook type ${githubWebhookType}.`); - } - - if (status !== null) { - StatusManager.setStatus(status); - } - - response.json({ message: 'thanks' }); + console.log('[route/webhook/github] Webhook received.'); + + const githubWebhookType: string = String(request.headers['x-github-event']); + + let status: Status | null = null; + + switch (githubWebhookType) { + case 'push': + status = GitHubParser.parsePush(request.body); + break; + case 'workflow_run': + status = GitHubParser.parseWorkflowRun(request.body); + break; + case 'workflow_job': + status = GitHubParser.parseWorkflowJob(request.body); + break; + case 'pull_request': + status = GitHubParser.parsePullRequest(request.body); + break; + default: + console.log(`[route/webhook/github] No parser for webhook type ${githubWebhookType}.`); + } + + if (status !== null) { + StatusManager.setStatus(status); + } + + response.json({ message: 'thanks' }); }); export default router; diff --git a/backend/router/route/webhook/gitlab.ts b/backend/router/route/webhook/gitlab.ts index 764e355c..942f11b2 100644 --- a/backend/router/route/webhook/gitlab.ts +++ b/backend/router/route/webhook/gitlab.ts @@ -8,34 +8,34 @@ import Status from 'types/status'; const router = express.Router(); router.post('/', (request, response) => { - console.log('[route/webhook/gitlab] Webhook received.'); - - const gitlabWebhook: GitLabWebhook = request.body; - - let status: Status | null = null; - - switch (gitlabWebhook.object_kind) { - case 'build': - status = GitLabParser.parseBuild(gitlabWebhook); - break; - case 'pipeline': - status = GitLabParser.parsePipeline(gitlabWebhook); - break; - case 'deployment': - status = GitLabParser.parseDeployment(gitlabWebhook); - break; - case 'merge_request': - status = GitLabParser.parseMergeRequest(gitlabWebhook); - break; - default: - console.log(`[route/webhook/gitlab] No parser for webhook type ${gitlabWebhook.object_kind}.`); - } - - if (status !== null) { - StatusManager.setStatus(status); - } - - response.json({ message: 'thanks' }); + console.log('[route/webhook/gitlab] Webhook received.'); + + const gitlabWebhook: GitLabWebhook = request.body; + + let status: Status | null = null; + + switch (gitlabWebhook.object_kind) { + case 'build': + status = GitLabParser.parseBuild(gitlabWebhook); + break; + case 'pipeline': + status = GitLabParser.parsePipeline(gitlabWebhook); + break; + case 'deployment': + status = GitLabParser.parseDeployment(gitlabWebhook); + break; + case 'merge_request': + status = GitLabParser.parseMergeRequest(gitlabWebhook); + break; + default: + console.log(`[route/webhook/gitlab] No parser for webhook type ${gitlabWebhook.object_kind}.`); + } + + if (status !== null) { + StatusManager.setStatus(status); + } + + response.json({ message: 'thanks' }); }); export default router; diff --git a/backend/router/route/webhook/readthedocs.ts b/backend/router/route/webhook/readthedocs.ts index 1c2e8e65..1f0a5ce5 100644 --- a/backend/router/route/webhook/readthedocs.ts +++ b/backend/router/route/webhook/readthedocs.ts @@ -7,21 +7,21 @@ import ReadTheDocsBuild from 'types/readthedocs'; const router = express.Router(); router.post('/', (request, response) => { - console.log('[route/webhook/readthedocs] Webhook received.'); + console.log('[route/webhook/readthedocs] Webhook received.'); - const webhook: ReadTheDocsBuild = request.body; + const webhook: ReadTheDocsBuild = request.body; - let status = null; + let status = null; - if (['build:triggered', 'build:failed', 'build:passed'].includes(webhook.event)) { - status = ReadTheDocsParser.parseBuild(webhook); - } + if (['build:triggered', 'build:failed', 'build:passed'].includes(webhook.event)) { + status = ReadTheDocsParser.parseBuild(webhook); + } - if (status !== null) { - StatusManager.setStatus(status); - } + if (status !== null) { + StatusManager.setStatus(status); + } - response.json({ message: 'thanks' }); + response.json({ message: 'thanks' }); }); export default router; diff --git a/backend/server.ts b/backend/server.ts index 385ff1a6..73a254e1 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -19,21 +19,21 @@ app.use(bodyParser.json()); app.use(router); (async () => { - StorageManager.init(); + StorageManager.init(); - const statuses = await StorageManager.loadStatuses(); + const statuses = await StorageManager.loadStatuses(); - if (statuses.length > 0) { - StatusManager.setStatuses(statuses); - } + if (statuses.length > 0) { + StatusManager.setStatuses(statuses); + } - StatusManager.init(); + StatusManager.init(); - const { events, triggers } = await StorageManager.loadModules(); + const { events, triggers } = await StorageManager.loadModules(); - ModuleManager.init(triggers, events); + ModuleManager.init(triggers, events); - ConnectionManager.startSocket(server); + ConnectionManager.startSocket(server); - server.listen(port, () => console.log(`[server] CIMonitor running on port ${port}.`)); + server.listen(port, () => console.log(`[server] CIMonitor running on port ${port}.`)); })(); diff --git a/backend/socket/client.ts b/backend/socket/client.ts index 31ae2243..0b9dbf65 100644 --- a/backend/socket/client.ts +++ b/backend/socket/client.ts @@ -4,42 +4,42 @@ import StatusEvents from 'backend/status/events'; import { socketEvent } from 'types/cimonitor'; class SocketClient { - init() { - if (!process.env.CIMONITOR_SERVER_URL) { - console.info('[socket/client] Missing CIMONITOR_SERVER_URL, which is required for the module client.'); - process.exit(1); - } - - if (!process.env.CIMONITOR_SERVER_URL.match(/^http[s]?:\/\//)) { - console.info('[socket/client] CIMONITOR_SERVER_URL must start with http/https.'); - process.exit(1); - } - } - - listen() { - console.log(`[socket/client] Connecting to ${process.env.CIMONITOR_SERVER_URL}...`); - const socket = io(process.env.CIMONITOR_SERVER_URL, { - secure: false, - }); - - socket.on(socketEvent.connect, () => { - console.log(`[socket/client] CIMonitor connected.`); - }); - - socket.on(socketEvent.disconnect, () => { - console.log(`[socket/client] lost connection to ${process.env.CIMONITOR_SERVER_URL}.`); - }); - - socket.on(socketEvent.statusStateChange, (status) => { - console.log(`[socket/client] Received changed state status.`); - StatusEvents.emit(StatusEvents.event.statusStateChange, status); - }); - - socket.on(socketEvent.newStatus, (status) => { - console.log(`[socket/client] Received new status.`); - StatusEvents.emit(StatusEvents.event.newStatus, status); - }); - } + init() { + if (!process.env.CIMONITOR_SERVER_URL) { + console.info('[socket/client] Missing CIMONITOR_SERVER_URL, which is required for the module client.'); + process.exit(1); + } + + if (!process.env.CIMONITOR_SERVER_URL.match(/^http[s]?:\/\//)) { + console.info('[socket/client] CIMONITOR_SERVER_URL must start with http/https.'); + process.exit(1); + } + } + + listen() { + console.log(`[socket/client] Connecting to ${process.env.CIMONITOR_SERVER_URL}...`); + const socket = io(process.env.CIMONITOR_SERVER_URL, { + secure: false, + }); + + socket.on(socketEvent.connect, () => { + console.log(`[socket/client] CIMonitor connected.`); + }); + + socket.on(socketEvent.disconnect, () => { + console.log(`[socket/client] lost connection to ${process.env.CIMONITOR_SERVER_URL}.`); + }); + + socket.on(socketEvent.statusStateChange, (status) => { + console.log(`[socket/client] Received changed state status.`); + StatusEvents.emit(StatusEvents.event.statusStateChange, status); + }); + + socket.on(socketEvent.newStatus, (status) => { + console.log(`[socket/client] Received new status.`); + StatusEvents.emit(StatusEvents.event.newStatus, status); + }); + } } export default new SocketClient(); diff --git a/backend/socket/manager.ts b/backend/socket/manager.ts index 134e6276..bcbada9d 100644 --- a/backend/socket/manager.ts +++ b/backend/socket/manager.ts @@ -7,72 +7,72 @@ import { socketEvent } from 'types/cimonitor'; import Status from 'types/status'; class SocketManager { - socket = null; - socketId = 0; - socketConnections = 0; - - startSocket(server: Server): void { - this.socket = new SocketServer(server, { - cors: { - origin: '*', - }, - }); - - this.socket.on(socketEvent.connect, (socket) => this.onClientConnect(socket)); - - this.listenToStatusEvents(); - } - - listenToStatusEvents() { - StatusEvents.on( - StatusEvents.event.patchStatus, - (status: Status) => this.socket && this.socket.sockets.emit(socketEvent.patchStatus, status) - ); - - StatusEvents.on( - StatusEvents.event.newStatus, - (status: Status) => this.socket && this.socket.sockets.emit(socketEvent.newStatus, status) - ); - - StatusEvents.on( - StatusEvents.event.deleteStatus, - (status: Status) => this.socket && this.socket.sockets.emit(socketEvent.deleteStatus, status) - ); - - StatusEvents.on( - StatusEvents.event.statusStateChange, - (status: Status) => this.socket && this.socket.sockets.emit(socketEvent.statusStateChange, status) - ); - - StatusEvents.on( - StatusEvents.event.deleteAllStatuses, - () => this.socket && this.socket.sockets.emit(socketEvent.allStatuses, []) - ); - } - - onClientConnect(socket) { - const socketId = this.getNewSocketId(); - - console.log(`[socket/manager] Client ${socketId} connected. Now ${this.socketConnections} connections.`); - - socket.emit(socketEvent.allStatuses, StatusManager.getStatuses()); - - socket.on(socketEvent.requestAllStatuses, () => { - socket.emit(socketEvent.allStatuses, StatusManager.getStatuses()); - }); - - socket.on(socketEvent.disconnect, () => { - this.socketConnections--; - console.log(`[socket/manager] Client ${socketId} disconnected. Now ${this.socketConnections} connections.`); - }); - } - - getNewSocketId(): number { - this.socketId++; - this.socketConnections++; - - return this.socketId; - } + socket = null; + socketId = 0; + socketConnections = 0; + + startSocket(server: Server): void { + this.socket = new SocketServer(server, { + cors: { + origin: '*', + }, + }); + + this.socket.on(socketEvent.connect, (socket) => this.onClientConnect(socket)); + + this.listenToStatusEvents(); + } + + listenToStatusEvents() { + StatusEvents.on( + StatusEvents.event.patchStatus, + (status: Status) => this.socket && this.socket.sockets.emit(socketEvent.patchStatus, status) + ); + + StatusEvents.on( + StatusEvents.event.newStatus, + (status: Status) => this.socket && this.socket.sockets.emit(socketEvent.newStatus, status) + ); + + StatusEvents.on( + StatusEvents.event.deleteStatus, + (status: Status) => this.socket && this.socket.sockets.emit(socketEvent.deleteStatus, status) + ); + + StatusEvents.on( + StatusEvents.event.statusStateChange, + (status: Status) => this.socket && this.socket.sockets.emit(socketEvent.statusStateChange, status) + ); + + StatusEvents.on( + StatusEvents.event.deleteAllStatuses, + () => this.socket && this.socket.sockets.emit(socketEvent.allStatuses, []) + ); + } + + onClientConnect(socket) { + const socketId = this.getNewSocketId(); + + console.log(`[socket/manager] Client ${socketId} connected. Now ${this.socketConnections} connections.`); + + socket.emit(socketEvent.allStatuses, StatusManager.getStatuses()); + + socket.on(socketEvent.requestAllStatuses, () => { + socket.emit(socketEvent.allStatuses, StatusManager.getStatuses()); + }); + + socket.on(socketEvent.disconnect, () => { + this.socketConnections--; + console.log(`[socket/manager] Client ${socketId} disconnected. Now ${this.socketConnections} connections.`); + }); + } + + getNewSocketId(): number { + this.socketId++; + this.socketConnections++; + + return this.socketId; + } } export default new SocketManager(); diff --git a/backend/status/events.ts b/backend/status/events.ts index 575ebb5f..f97cbae9 100644 --- a/backend/status/events.ts +++ b/backend/status/events.ts @@ -1,13 +1,13 @@ import EventEmitter from 'events'; class StatusEvents extends EventEmitter { - event = { - newStatus: 'status-new', - patchStatus: 'status-patch', - deleteStatus: 'status-delete', - statusStateChange: 'status-state-change', - deleteAllStatuses: 'status-delete-all', - }; + event = { + newStatus: 'status-new', + patchStatus: 'status-patch', + deleteStatus: 'status-delete', + statusStateChange: 'status-state-change', + deleteAllStatuses: 'status-delete-all', + }; } export default new StatusEvents(); diff --git a/backend/status/helper.ts b/backend/status/helper.ts index 1fc4f2d4..172a06be 100644 --- a/backend/status/helper.ts +++ b/backend/status/helper.ts @@ -4,116 +4,116 @@ const statusesExpire = 60 * 60 * 24 * 7; // 7 days const statusesTimeout = 60 * 60 * 2; // 2 hours const isExpired = (time: string, expireAfterSeconds: number): boolean => { - const expiredTime = new Date().getTime() - expireAfterSeconds * 1000; + const expiredTime = new Date().getTime() - expireAfterSeconds * 1000; - return new Date(time).getTime() < expiredTime; + return new Date(time).getTime() < expiredTime; }; export const getExpiredStatuses = (statuses: Status[]): Status[] => - statuses.filter((status) => isExpired(status.time, statusesExpire)); + statuses.filter((status) => isExpired(status.time, statusesExpire)); export const getStuckStatuses = (statuses: Status[]): Status[] => - statuses.filter((status) => { - for (let process of status.processes) { - for (let stage of process.stages) { - if (stage.state === 'running' && isExpired(stage.time, statusesTimeout)) { - return true; - } - - for (let step of stage.steps) { - if (step.state === 'running' && isExpired(step.time, statusesTimeout)) { - return true; - } - } - } - - if ( - process.state === 'warning' && - isExpired(process.time, statusesTimeout) && - // when there is a pending stage, the process is not stuck running - !process.stages.find((stage) => stage.state === 'pending') - ) { - return true; - } - } - - return false; - }); + statuses.filter((status) => { + for (let process of status.processes) { + for (let stage of process.stages) { + if (stage.state === 'running' && isExpired(stage.time, statusesTimeout)) { + return true; + } + + for (let step of stage.steps) { + if (step.state === 'running' && isExpired(step.time, statusesTimeout)) { + return true; + } + } + } + + if ( + process.state === 'warning' && + isExpired(process.time, statusesTimeout) && + // when there is a pending stage, the process is not stuck running + !process.stages.find((stage) => stage.state === 'pending') + ) { + return true; + } + } + + return false; + }); export const fixStatusStates = (status: Status): Status => { - const processes = status.processes - // Sort processes by creation time - .sort( - (processA: Process, processB: Process): number => - new Date(processB.time).getTime() - new Date(processA.time).getTime() - ) - // Remove all processes that are not the latest or not warning - .filter((process, index) => index === 0 || process.state === 'warning'); - - return { - ...status, - state: determineStatusState(processes), - processes, - }; + const processes = status.processes + // Sort processes by creation time + .sort( + (processA: Process, processB: Process): number => + new Date(processB.time).getTime() - new Date(processA.time).getTime() + ) + // Remove all processes that are not the latest or not warning + .filter((process, index) => index === 0 || process.state === 'warning'); + + return { + ...status, + state: determineStatusState(processes), + processes, + }; }; export const fixStuckStatus = (status: Status): Status => ({ - ...status, - processes: status.processes.map((process) => { - const stages = process.stages.map((stage) => { - const steps = stage.steps.map((step) => { - if (step.state === 'running' && isExpired(step.time, statusesTimeout)) { - step.state = 'timeout'; - } - - return step; - }); - - return { - ...stage, - steps, - state: stage.state === 'running' && isExpired(stage.time, statusesTimeout) ? 'timeout' : stage.state, - }; - }); - - return { - ...process, - stages, - state: determineTimeoutProcessState(stages), - }; - }), + ...status, + processes: status.processes.map((process) => { + const stages = process.stages.map((stage) => { + const steps = stage.steps.map((step) => { + if (step.state === 'running' && isExpired(step.time, statusesTimeout)) { + step.state = 'timeout'; + } + + return step; + }); + + return { + ...stage, + steps, + state: stage.state === 'running' && isExpired(stage.time, statusesTimeout) ? 'timeout' : stage.state, + }; + }); + + return { + ...process, + stages, + state: determineTimeoutProcessState(stages), + }; + }), }); const determineStatusState = (processes: Process[]): State => { - if (processes.find((processes) => processes.state === 'error')) { - return 'error'; - } + if (processes.find((processes) => processes.state === 'error')) { + return 'error'; + } - if (processes.find((processes) => processes.state === 'warning')) { - return 'warning'; - } + if (processes.find((processes) => processes.state === 'warning')) { + return 'warning'; + } - if (processes.find((processes) => processes.state === 'success')) { - return 'success'; - } + if (processes.find((processes) => processes.state === 'success')) { + return 'success'; + } - return 'info'; + return 'info'; }; export const isOldProcess = (status: Status, processId: number): boolean => { - if (status.processes.length === 0) { - return false; - } + if (status.processes.length === 0) { + return false; + } - const latestProcessId = status.processes[0].id; + const latestProcessId = status.processes[0].id; - return processId < latestProcessId; + return processId < latestProcessId; }; const determineTimeoutProcessState = (stages: Stage[]): State => { - if (stages.find((stage) => ['running'].includes(stage.state))) { - return 'warning'; - } + if (stages.find((stage) => ['running'].includes(stage.state))) { + return 'warning'; + } - return 'error'; + return 'error'; }; diff --git a/backend/status/manager.ts b/backend/status/manager.ts index b6e98c08..037e6c4f 100644 --- a/backend/status/manager.ts +++ b/backend/status/manager.ts @@ -5,109 +5,109 @@ import StatusEvents from './events'; import { fixStatusStates, fixStuckStatus, getExpiredStatuses, getStuckStatuses } from './helper'; class StatusManager { - statuses: Status[] = []; + statuses: Status[] = []; - setStatuses(statuses: Status[]) { - this.statuses = statuses; - } + setStatuses(statuses: Status[]) { + this.statuses = statuses; + } - getStatuses(): Status[] { - return this.statuses; - } + getStatuses(): Status[] { + return this.statuses; + } - getStatus(id: string): Status | null { - const status = this.statuses.find((status) => status.id === id); + getStatus(id: string): Status | null { + const status = this.statuses.find((status) => status.id === id); - if (!status) { - return null; - } + if (!status) { + return null; + } - return status; - } + return status; + } - deleteStatus(statusId: string) { - const statuses = this.statuses.filter((status) => status.id !== statusId); + deleteStatus(statusId: string) { + const statuses = this.statuses.filter((status) => status.id !== statusId); - this.statuses = statuses; + this.statuses = statuses; - // TODO: fix dependency, storage manager should listen instead - StorageManager.saveStatuses(statuses); + // TODO: fix dependency, storage manager should listen instead + StorageManager.saveStatuses(statuses); - StatusEvents.emit(StatusEvents.event.deleteStatus, statusId); - } + StatusEvents.emit(StatusEvents.event.deleteStatus, statusId); + } - deleteAllStatuses() { - this.statuses = []; + deleteAllStatuses() { + this.statuses = []; - // TODO: fix dependency, storage manager should listen instead - StorageManager.saveStatuses([]); + // TODO: fix dependency, storage manager should listen instead + StorageManager.saveStatuses([]); - StatusEvents.emit(StatusEvents.event.deleteAllStatuses); - } + StatusEvents.emit(StatusEvents.event.deleteAllStatuses); + } - setStatus(status: Status): void { - status = fixStatusStates(status); - let replacedStatus = false; + setStatus(status: Status): void { + status = fixStatusStates(status); + let replacedStatus = false; - const statuses = [ - ...this.statuses.map((existingStatus) => { - if (existingStatus.id === status.id) { - const isStateChanged = existingStatus.state !== status.state; + const statuses = [ + ...this.statuses.map((existingStatus) => { + if (existingStatus.id === status.id) { + const isStateChanged = existingStatus.state !== status.state; - console.log( - `[status/manager] Replaced existing status ${status.id}${ - isStateChanged ? ` with new state ${status.state}` : '' - }.` - ); - StatusEvents.emit(StatusEvents.event.patchStatus, status); + console.log( + `[status/manager] Replaced existing status ${status.id}${ + isStateChanged ? ` with new state ${status.state}` : '' + }.` + ); + StatusEvents.emit(StatusEvents.event.patchStatus, status); - if (isStateChanged) { - StatusEvents.emit(StatusEvents.event.statusStateChange, status); - } + if (isStateChanged) { + StatusEvents.emit(StatusEvents.event.statusStateChange, status); + } - replacedStatus = true; - return status; - } + replacedStatus = true; + return status; + } - return existingStatus; - }), - ]; + return existingStatus; + }), + ]; - if (!replacedStatus) { - statuses.push(status); - console.log(`[status/manager] Added new status ${status.id} with state ${status.state}.`); - StatusEvents.emit(StatusEvents.event.newStatus, status); - } + if (!replacedStatus) { + statuses.push(status); + console.log(`[status/manager] Added new status ${status.id} with state ${status.state}.`); + StatusEvents.emit(StatusEvents.event.newStatus, status); + } - this.statuses = statuses; + this.statuses = statuses; - // TODO: fix dependency, storage manager should listen instead - // beware of the race condition that events are emit before the statuses are set on the manager - StorageManager.saveStatuses(statuses); - } + // TODO: fix dependency, storage manager should listen instead + // beware of the race condition that events are emit before the statuses are set on the manager + StorageManager.saveStatuses(statuses); + } - init(): void { - console.log('[status/manager] Init.'); + init(): void { + console.log('[status/manager] Init.'); - this.statusHealthCheck(); + this.statusHealthCheck(); - // Do a status health check every minute - setInterval(() => this.statusHealthCheck(), 1000 * 60); - } + // Do a status health check every minute + setInterval(() => this.statusHealthCheck(), 1000 * 60); + } - statusHealthCheck() { - const expiredStatuses = getExpiredStatuses(this.statuses); - for (let expiredStatus of expiredStatuses) { - console.log(`[status/manager] Deleted status ${expiredStatus.id} because it expired.`); - this.deleteStatus(expiredStatus.id); - } + statusHealthCheck() { + const expiredStatuses = getExpiredStatuses(this.statuses); + for (let expiredStatus of expiredStatuses) { + console.log(`[status/manager] Deleted status ${expiredStatus.id} because it expired.`); + this.deleteStatus(expiredStatus.id); + } - const stuckStatuses = getStuckStatuses(this.statuses); - for (let stuckStatus of stuckStatuses) { - console.log(`[status/manager] Status ${stuckStatus.id} was stuck, patched timeout state.`); - this.setStatus(fixStuckStatus(stuckStatus)); - } - } + const stuckStatuses = getStuckStatuses(this.statuses); + for (let stuckStatus of stuckStatuses) { + console.log(`[status/manager] Status ${stuckStatus.id} was stuck, patched timeout state.`); + this.setStatus(fixStuckStatus(stuckStatus)); + } + } } export default new StatusManager(); diff --git a/backend/storage/manager.ts b/backend/storage/manager.ts index 6365be51..b122deb9 100644 --- a/backend/storage/manager.ts +++ b/backend/storage/manager.ts @@ -7,68 +7,68 @@ import JsonStorage from './type/json'; import StorageType from './type'; class StorageManager { - storage: StorageType | null = null; + storage: StorageType | null = null; - storages = { - json: JsonStorage, - firebase: FirebaseStorage, - }; + storages = { + json: JsonStorage, + firebase: FirebaseStorage, + }; - init() { - console.log('[storage/manager] Init.'); + init() { + console.log('[storage/manager] Init.'); - this.determineStorageType(); - } + this.determineStorageType(); + } - determineStorageType() { - const desiredStorageType = process.env.STORAGE_TYPE || 'json'; + determineStorageType() { + const desiredStorageType = process.env.STORAGE_TYPE || 'json'; - if (!(desiredStorageType in this.storages)) { - console.log( - `[storage/manager] STORAGE_TYPE ${desiredStorageType} is not valid. Please select 1 of: ${Object.keys( - this.storages - ).join(', ')}.` - ); - process.exit(1); - } + if (!(desiredStorageType in this.storages)) { + console.log( + `[storage/manager] STORAGE_TYPE ${desiredStorageType} is not valid. Please select 1 of: ${Object.keys( + this.storages + ).join(', ')}.` + ); + process.exit(1); + } - this.storage = this.storages[desiredStorageType]; + this.storage = this.storages[desiredStorageType]; - if (!this.storage.validateEnvironment()) { - console.log(`[storage/manager] Could not set up status persistence for type ${desiredStorageType}.`); - process.exit(1); - } + if (!this.storage.validateEnvironment()) { + console.log(`[storage/manager] Could not set up status persistence for type ${desiredStorageType}.`); + process.exit(1); + } - console.log(`[storage/manager] Using storage type ${this.storage.name}.`); - } + console.log(`[storage/manager] Using storage type ${this.storage.name}.`); + } - async loadStatuses(): Promise { - return this.storage.loadStatuses(); - } + async loadStatuses(): Promise { + return this.storage.loadStatuses(); + } - async loadSettings(): Promise { - return this.storage.loadSettings(); - } + async loadSettings(): Promise { + return this.storage.loadSettings(); + } - async loadModules(): Promise { - return this.storage.loadModules(); - } + async loadModules(): Promise { + return this.storage.loadModules(); + } - saveSettings(settings: ServerSettings) { - if (!this.storage) { - console.log('[storage/manager] No storage was defined, no settings saved.'); - } + saveSettings(settings: ServerSettings) { + if (!this.storage) { + console.log('[storage/manager] No storage was defined, no settings saved.'); + } - this.storage.saveSettings(settings); - } + this.storage.saveSettings(settings); + } - saveStatuses(statuses: Status[]) { - if (!this.storage) { - console.log('[storage/manager] No storage was defined, no statuses saved.'); - } + saveStatuses(statuses: Status[]) { + if (!this.storage) { + console.log('[storage/manager] No storage was defined, no statuses saved.'); + } - this.storage.saveStatuses(statuses); - } + this.storage.saveStatuses(statuses); + } } export default new StorageManager(); diff --git a/backend/storage/type.ts b/backend/storage/type.ts index 72f72a96..e0734f79 100644 --- a/backend/storage/type.ts +++ b/backend/storage/type.ts @@ -3,21 +3,21 @@ import { ModuleSettings } from 'types/module'; import Status from 'types/status'; abstract class StorageType { - abstract name: string; + abstract name: string; - abstract validateEnvironment(): boolean; + abstract validateEnvironment(): boolean; - abstract loadStatuses(): Promise; + abstract loadStatuses(): Promise; - abstract loadSettings(): Promise; + abstract loadSettings(): Promise; - abstract loadModules(): Promise; + abstract loadModules(): Promise; - // eslint-disable-next-line no-unused-vars - abstract saveStatuses(statuses: Status[]): void; + // eslint-disable-next-line no-unused-vars + abstract saveStatuses(statuses: Status[]): void; - // eslint-disable-next-line no-unused-vars - abstract saveSettings(settings: ServerSettings): void; + // eslint-disable-next-line no-unused-vars + abstract saveSettings(settings: ServerSettings): void; } export default StorageType; diff --git a/backend/storage/type/firebase.ts b/backend/storage/type/firebase.ts index 7f19dbb9..c48fff86 100644 --- a/backend/storage/type/firebase.ts +++ b/backend/storage/type/firebase.ts @@ -7,122 +7,122 @@ import { ModuleSettings } from 'types/module'; import Status from 'types/status'; class FirebaseStorage extends StorageType { - name = 'firebase'; - database = null; - - validateEnvironment(): boolean { - if (!process.env.FIREBASE_URL) { - console.info('[storage/type/firebase] Missing FIREBASE_URL, which is required for STORAGE_TYPE=firebase.'); - return false; - } - - if (!process.env.FIREBASE_KEY_FILE) { - console.info( - '[storage/type/firebase] Missing FIREBASE_KEY_FILE, which is required for STORAGE_TYPE=firebase.' - ); - return false; - } - - FirebaseAdmin.initializeApp({ - databaseURL: process.env.FIREBASE_URL, - credential: FirebaseAdmin.credential.cert(process.env.FIREBASE_KEY_FILE), - }); - - this.database = FirebaseAdmin.database(); - return true; - } - - async load(key: string): Promise { - const dataResponse = await this.database.ref(key).once('value'); - - return FirebaseDataParser.convertObjectArraysToArrays(dataResponse.toJSON()); - } - - save(key: string, data: any) { - try { - return this.database.ref(key).set(data); - } catch (error) { - console.error(`[storage/type/firebase] ${error}`); - } - } - - async loadSettings(): Promise { - try { - return this.load('settings'); - } catch (error) { - console.error(`[storage/type/firebase] ${error}`); - console.log(`[storage/type/firebase] Returning default settings`); - return {}; - } - } - - async loadModules(): Promise { - try { - const modules: ModuleSettings = await this.load('modules'); - - if (!('triggers' in modules)) { - modules.triggers = []; - } - - if (!('events' in modules)) { - modules.events = []; - } - - return modules; - } catch (error) { - console.error(`[storage/type/firebase] ${error}`); - console.log(`[storage/type/firebase] Returning default settings`); - return { - triggers: [], - events: [], - }; - } - } - - async loadStatuses(): Promise { - const statuses = await this.load('statuses'); - - if (!statuses) { - return []; - } - - // Make sure that everything that should be an array of a status, is an array. - return this.fixStatusArrays(statuses); - } - - saveSettings(settings: ServerSettings): void { - this.save('settings', settings); - } - - saveStatuses(statuses: Status[]): void { - this.save('statuses', statuses); - } - - fixStatusArrays(statuses: Status[]): Status[] { - return statuses.map((status) => { - if (!status.processes) { - status.processes = []; - } - - status.processes.map((process) => { - if (!process.stages) { - process.stages = []; - } - - process.stages.map((stage) => { - if (!stage.steps) { - stage.steps = []; - } - - return stage; - }); - - return process; - }); - - return status; - }); - } + name = 'firebase'; + database = null; + + validateEnvironment(): boolean { + if (!process.env.FIREBASE_URL) { + console.info('[storage/type/firebase] Missing FIREBASE_URL, which is required for STORAGE_TYPE=firebase.'); + return false; + } + + if (!process.env.FIREBASE_KEY_FILE) { + console.info( + '[storage/type/firebase] Missing FIREBASE_KEY_FILE, which is required for STORAGE_TYPE=firebase.' + ); + return false; + } + + FirebaseAdmin.initializeApp({ + databaseURL: process.env.FIREBASE_URL, + credential: FirebaseAdmin.credential.cert(process.env.FIREBASE_KEY_FILE), + }); + + this.database = FirebaseAdmin.database(); + return true; + } + + async load(key: string): Promise { + const dataResponse = await this.database.ref(key).once('value'); + + return FirebaseDataParser.convertObjectArraysToArrays(dataResponse.toJSON()); + } + + save(key: string, data: any) { + try { + return this.database.ref(key).set(data); + } catch (error) { + console.error(`[storage/type/firebase] ${error}`); + } + } + + async loadSettings(): Promise { + try { + return this.load('settings'); + } catch (error) { + console.error(`[storage/type/firebase] ${error}`); + console.log(`[storage/type/firebase] Returning default settings`); + return {}; + } + } + + async loadModules(): Promise { + try { + const modules: ModuleSettings = await this.load('modules'); + + if (!('triggers' in modules)) { + modules.triggers = []; + } + + if (!('events' in modules)) { + modules.events = []; + } + + return modules; + } catch (error) { + console.error(`[storage/type/firebase] ${error}`); + console.log(`[storage/type/firebase] Returning default settings`); + return { + triggers: [], + events: [], + }; + } + } + + async loadStatuses(): Promise { + const statuses = await this.load('statuses'); + + if (!statuses) { + return []; + } + + // Make sure that everything that should be an array of a status, is an array. + return this.fixStatusArrays(statuses); + } + + saveSettings(settings: ServerSettings): void { + this.save('settings', settings); + } + + saveStatuses(statuses: Status[]): void { + this.save('statuses', statuses); + } + + fixStatusArrays(statuses: Status[]): Status[] { + return statuses.map((status) => { + if (!status.processes) { + status.processes = []; + } + + status.processes.map((process) => { + if (!process.stages) { + process.stages = []; + } + + process.stages.map((stage) => { + if (!stage.steps) { + stage.steps = []; + } + + return stage; + }); + + return process; + }); + + return status; + }); + } } export default new FirebaseStorage(); diff --git a/backend/storage/type/json.ts b/backend/storage/type/json.ts index e9e3226d..f3930597 100644 --- a/backend/storage/type/json.ts +++ b/backend/storage/type/json.ts @@ -6,116 +6,116 @@ import { ModuleSettings } from 'types/module'; import Status from 'types/status'; class JsonStorage extends StorageType { - name = 'json'; - storagePath = 'storage'; - settingsFile = `${this.storagePath}/settings.json`; - statusesFile = `${this.storagePath}/statuses.json`; - modulesFile = `${this.storagePath}/modules.json`; - - validateEnvironment() { - this.createStorageFolder(); - - console.log('[storage/type/json] Ready to store.'); - - return true; - } - - async loadSettings(): Promise { - console.log('[storage/type/json] Loading settings...'); - let settings = {}; - - if (!FileSystem.existsSync(this.settingsFile)) { - console.log('[storage/type/json] No settings file exists yet.'); - return settings; - } - - try { - settings = JSON.parse(String(FileSystem.readFileSync(this.settingsFile))); - } catch (error) { - console.log('[storage/type/json] Failed to load settings, manual check required.'); - process.exit(1); - return settings; - } - - console.log('[storage/type/json] Settings loaded.'); - return settings; - } - - async loadStatuses(): Promise { - console.log('[storage/type/json] Loading statuses...'); - - let statuses = []; - - if (!FileSystem.existsSync(this.statusesFile)) { - console.log('[storage/type/json] No status file exists yet.'); - return statuses; - } - - try { - statuses = JSON.parse(String(FileSystem.readFileSync(this.statusesFile))); - } catch (error) { - console.log('[storage/type/json] Failed to load statuses, manual check required.'); - process.exit(1); - return statuses; - } - - console.log('[storage/type/json] Statuses loaded.'); - return statuses; - } - - async loadModules(): Promise { - console.log('[storage/type/json] Loading modules...'); - - let moduleSettings: ModuleSettings = { - triggers: [], - events: [], - }; - - if (!FileSystem.existsSync(this.modulesFile)) { - console.log('[storage/type/json] No modules file exists yet.'); - return moduleSettings; - } - - try { - moduleSettings = JSON.parse(String(FileSystem.readFileSync(this.modulesFile))); - } catch (error) { - console.log('[storage/type/json] Failed to load modules, manual check required.'); - process.exit(1); - return moduleSettings; - } - - console.log('[storage/type/json] Modules loaded.'); - return moduleSettings; - } - - saveSettings(settings: ServerSettings): void { - this.createStorageFolder(); - - try { - FileSystem.writeFileSync(this.settingsFile, JSON.stringify(settings, null, 4)); - } catch (error) { - console.log('[storage/type/json] Failed to save the settings.'); - console.log(error); - } - } - - saveStatuses(statuses: Status[]): void { - this.createStorageFolder(); - - try { - FileSystem.writeFileSync(this.statusesFile, JSON.stringify(statuses, null, 4)); - } catch (error) { - console.log('[storage/type/json] Failed to save the statuses.'); - console.log(error); - } - } - - createStorageFolder() { - if (!FileSystem.existsSync(this.storagePath)) { - console.log('[storage/type/json] Created storage folder as it did not exist yet.'); - FileSystem.mkdirSync(this.storagePath); - } - } + name = 'json'; + storagePath = 'storage'; + settingsFile = `${this.storagePath}/settings.json`; + statusesFile = `${this.storagePath}/statuses.json`; + modulesFile = `${this.storagePath}/modules.json`; + + validateEnvironment() { + this.createStorageFolder(); + + console.log('[storage/type/json] Ready to store.'); + + return true; + } + + async loadSettings(): Promise { + console.log('[storage/type/json] Loading settings...'); + let settings = {}; + + if (!FileSystem.existsSync(this.settingsFile)) { + console.log('[storage/type/json] No settings file exists yet.'); + return settings; + } + + try { + settings = JSON.parse(String(FileSystem.readFileSync(this.settingsFile))); + } catch (error) { + console.log('[storage/type/json] Failed to load settings, manual check required.'); + process.exit(1); + return settings; + } + + console.log('[storage/type/json] Settings loaded.'); + return settings; + } + + async loadStatuses(): Promise { + console.log('[storage/type/json] Loading statuses...'); + + let statuses = []; + + if (!FileSystem.existsSync(this.statusesFile)) { + console.log('[storage/type/json] No status file exists yet.'); + return statuses; + } + + try { + statuses = JSON.parse(String(FileSystem.readFileSync(this.statusesFile))); + } catch (error) { + console.log('[storage/type/json] Failed to load statuses, manual check required.'); + process.exit(1); + return statuses; + } + + console.log('[storage/type/json] Statuses loaded.'); + return statuses; + } + + async loadModules(): Promise { + console.log('[storage/type/json] Loading modules...'); + + let moduleSettings: ModuleSettings = { + triggers: [], + events: [], + }; + + if (!FileSystem.existsSync(this.modulesFile)) { + console.log('[storage/type/json] No modules file exists yet.'); + return moduleSettings; + } + + try { + moduleSettings = JSON.parse(String(FileSystem.readFileSync(this.modulesFile))); + } catch (error) { + console.log('[storage/type/json] Failed to load modules, manual check required.'); + process.exit(1); + return moduleSettings; + } + + console.log('[storage/type/json] Modules loaded.'); + return moduleSettings; + } + + saveSettings(settings: ServerSettings): void { + this.createStorageFolder(); + + try { + FileSystem.writeFileSync(this.settingsFile, JSON.stringify(settings, null, 4)); + } catch (error) { + console.log('[storage/type/json] Failed to save the settings.'); + console.log(error); + } + } + + saveStatuses(statuses: Status[]): void { + this.createStorageFolder(); + + try { + FileSystem.writeFileSync(this.statusesFile, JSON.stringify(statuses, null, 4)); + } catch (error) { + console.log('[storage/type/json] Failed to save the statuses.'); + console.log(error); + } + } + + createStorageFolder() { + if (!FileSystem.existsSync(this.storagePath)) { + console.log('[storage/type/json] Created storage folder as it did not exist yet.'); + FileSystem.mkdirSync(this.storagePath); + } + } } export default new JsonStorage(); diff --git a/backend/tsconfig.json b/backend/tsconfig.json index f28cface..c4dce5fc 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,18 +1,18 @@ { - "compilerOptions": { - "target": "esnext", - "module": "CommonJS", - "outDir": "../app", - "moduleResolution": "node", - "baseUrl": "../", - "esModuleInterop": true, - "preserveWatchOutput": true, - "jsx": "preserve", - "allowJs": false, - "paths": { - "backend/*": ["./backend/*"], - "types/*": ["./types/*"] - } - }, - "include": ["../backend/**/*.ts", "../types/**/*.ts"] + "compilerOptions": { + "target": "esnext", + "module": "CommonJS", + "outDir": "../app", + "moduleResolution": "node", + "baseUrl": "../", + "esModuleInterop": true, + "preserveWatchOutput": true, + "jsx": "preserve", + "allowJs": false, + "paths": { + "backend/*": ["./backend/*"], + "types/*": ["./types/*"] + } + }, + "include": ["../backend/**/*.ts", "../types/**/*.ts"] } diff --git a/cypress.json b/cypress.json index 239de11c..24d38e4b 100644 --- a/cypress.json +++ b/cypress.json @@ -1,4 +1,4 @@ { - "baseUrl": "http://localhost:3030", - "video": false + "baseUrl": "http://localhost:3030", + "video": false } diff --git a/docs/style.css b/docs/style.css index b266508b..d463f3d3 100644 --- a/docs/style.css +++ b/docs/style.css @@ -6,65 +6,65 @@ h2, h3, h4, h5 { - font-family: 'Montserrat', sans-serif !important; + font-family: 'Montserrat', sans-serif !important; } .wy-nav-content { - background: #fff !important; - max-width: 1020px !important; + background: #fff !important; + max-width: 1020px !important; } .wy-breadcrumbs-aside, .wy-side-nav-search { - display: none !important; + display: none !important; } .wy-nav-side { - padding: 20px 0 0 !important; - background: #fff !important; - height: 100% !important; + padding: 20px 0 0 !important; + background: #fff !important; + height: 100% !important; } .wy-menu-vertical { - border: 0 !important; + border: 0 !important; } .wy-menu-vertical li.current { - background: transparent !important; + background: transparent !important; } .wy-nav-side a { - color: #333333 !important; - border: 0 !important; - border-radius: 0 15px 15px 0; - transition: background-color 300ms; + color: #333333 !important; + border: 0 !important; + border-radius: 0 15px 15px 0; + transition: background-color 300ms; } .wy-nav-side a:hover { - background: #deead3 !important; + background: #deead3 !important; } .wy-nav-side a.current { - background: #8ac159 !important; + background: #8ac159 !important; } div[role='note'] { - display: none; + display: none; } img { - border-radius: 5px; + border-radius: 5px; } footer { - display: none; + display: none; } .wy-nav-content-wrap { - background: #fff !important; + background: #fff !important; } .wy-nav-top { - background: #333 !important; - line-height: 30px; + background: #333 !important; + line-height: 30px; } diff --git a/frontend/App/App.tsx b/frontend/App/App.tsx index a4ed062e..e48d493e 100644 --- a/frontend/App/App.tsx +++ b/frontend/App/App.tsx @@ -9,19 +9,19 @@ import Statuses from './Statuses'; import Toolbar from './Toolbar'; const App = (): ReactElement => { - const { socketConnected } = useSocket(); + const { socketConnected } = useSocket(); - document.title = `${window.location.host} | CIMonitor`; + document.title = `${window.location.host} | CIMonitor`; - return ( - <> - - - - - - - ); + return ( + <> + + + + + + + ); }; export default App; diff --git a/frontend/App/Favicon/Favicon.tsx b/frontend/App/Favicon/Favicon.tsx index f88c7c95..d7156c84 100644 --- a/frontend/App/Favicon/Favicon.tsx +++ b/frontend/App/Favicon/Favicon.tsx @@ -10,23 +10,23 @@ import WarningIcon from './icon/warning.png'; import { State } from '/types/status'; const getIcon = (state: State) => { - if (state === 'error') { - return ErrorIcon; - } + if (state === 'error') { + return ErrorIcon; + } - if (state === 'warning') { - return WarningIcon; - } + if (state === 'warning') { + return WarningIcon; + } - return SuccessIcon; + return SuccessIcon; }; const Favicon = (): ReactElement => { - const globalState = useSelector(getGlobalState); + const globalState = useSelector(getGlobalState); - document.getElementById('favicon').setAttribute('href', getIcon(globalState)); + document.getElementById('favicon').setAttribute('href', getIcon(globalState)); - return null; + return null; }; export default Favicon; diff --git a/frontend/App/SettingsPanel/About/About.tsx b/frontend/App/SettingsPanel/About/About.tsx index 3c9f20b1..67311b36 100644 --- a/frontend/App/SettingsPanel/About/About.tsx +++ b/frontend/App/SettingsPanel/About/About.tsx @@ -6,18 +6,18 @@ import Contributors from './Contributors'; import Version from './Version'; const About = (): ReactElement => { - return ( - -

- CIMonitor is a dashboard where all your CI statuses come together. Check if all tests have passed, and - if deployments are successful. Never miss a failed build again. Let webhooks push the latest updates to - CIMonitor, and it will create a neat overview of those statuses. -

- -

A big thank you to all the contributors of the project:

- -
- ); + return ( + +

+ CIMonitor is a dashboard where all your CI statuses come together. Check if all tests have passed, and + if deployments are successful. Never miss a failed build again. Let webhooks push the latest updates to + CIMonitor, and it will create a neat overview of those statuses. +

+ +

A big thank you to all the contributors of the project:

+ +
+ ); }; export default About; diff --git a/frontend/App/SettingsPanel/About/Contributors/Contributors.style.tsx b/frontend/App/SettingsPanel/About/Contributors/Contributors.style.tsx index 2885d513..f5657f61 100644 --- a/frontend/App/SettingsPanel/About/Contributors/Contributors.style.tsx +++ b/frontend/App/SettingsPanel/About/Contributors/Contributors.style.tsx @@ -3,42 +3,42 @@ import styled from 'styled-components'; import { stateColor, textMutedColor } from '/frontend/style/colors'; export const Contributor = styled.div` - border-top: 2px solid #f0f0f0; - padding: 1rem 0; - display: flex; - align-items: center; + border-top: 2px solid #f0f0f0; + padding: 1rem 0; + display: flex; + align-items: center; `; export const Image = styled.img` - background: #f0f0f0; - border-radius: 50%; - width: 5rem; - height: 5rem; + background: #f0f0f0; + border-radius: 50%; + width: 5rem; + height: 5rem; `; export const Details = styled.div` - padding-left: 1rem; - line-height: 1.3; + padding-left: 1rem; + line-height: 1.3; `; export const ProfileLink = styled.a` - display: block; - text-decoration: none; + display: block; + text-decoration: none; `; export const Name = styled.div` - font-size: 1.3rem; + font-size: 1.3rem; `; export const Location = styled.div` - color: ${textMutedColor}; + color: ${textMutedColor}; `; export const Handle = styled.span` - color: ${textMutedColor}; + color: ${textMutedColor}; `; export const Contributions = styled.a` - display: block; - color: ${stateColor.success}; + display: block; + color: ${stateColor.success}; `; diff --git a/frontend/App/SettingsPanel/About/Contributors/Contributors.tsx b/frontend/App/SettingsPanel/About/Contributors/Contributors.tsx index ed53bad9..447d00f2 100644 --- a/frontend/App/SettingsPanel/About/Contributors/Contributors.tsx +++ b/frontend/App/SettingsPanel/About/Contributors/Contributors.tsx @@ -9,48 +9,48 @@ import { getContributors } from '/frontend/store/cache/selectors'; import { Contributor as ContributorType } from '/types/cimonitor'; const Contributors = (): ReactElement => { - const contributors = useSelector(getContributors); - - useEffect(() => { - fetchContributors(); - }, []); - - if (contributors.length === 0) { - return <>Fetching contributors...; - } - - const getContributions = (contributor: ContributorType): string => { - if (contributor.username === 'rick-nu') { - return 'Creator and maintainer'; - } - - return `${contributor.commits} contribution${contributor.commits > 1 ? 's' : ''}`; - }; - - return ( - <> - {contributors.map((contributor) => ( - - - {contributor.name - -
- - {contributor.name || contributor.username}{' '} - {contributor.name && @{contributor.username}} - - {contributor.location && {contributor.location}} - - {getContributions(contributor)} - -
-
- ))} - - ); + const contributors = useSelector(getContributors); + + useEffect(() => { + fetchContributors(); + }, []); + + if (contributors.length === 0) { + return <>Fetching contributors...; + } + + const getContributions = (contributor: ContributorType): string => { + if (contributor.username === 'rick-nu') { + return 'Creator and maintainer'; + } + + return `${contributor.commits} contribution${contributor.commits > 1 ? 's' : ''}`; + }; + + return ( + <> + {contributors.map((contributor) => ( + + + {contributor.name + +
+ + {contributor.name || contributor.username}{' '} + {contributor.name && @{contributor.username}} + + {contributor.location && {contributor.location}} + + {getContributions(contributor)} + +
+
+ ))} + + ); }; export default Contributors; diff --git a/frontend/App/SettingsPanel/About/Version/Version.tsx b/frontend/App/SettingsPanel/About/Version/Version.tsx index e48db59f..bd0b26ad 100644 --- a/frontend/App/SettingsPanel/About/Version/Version.tsx +++ b/frontend/App/SettingsPanel/About/Version/Version.tsx @@ -6,47 +6,47 @@ import { fetchVersion } from '/frontend/store/cache/fetch'; import { getVersion } from '/frontend/store/cache/selectors'; const isNewest = (currentVersion: string, latestVersion: string): boolean => { - const currentVersionSplit = currentVersion.split('.'); - const latestVersionSplit = latestVersion.split('.'); + const currentVersionSplit = currentVersion.split('.'); + const latestVersionSplit = latestVersion.split('.'); - for (let versionPart in currentVersionSplit) { - if (parseInt(currentVersionSplit[versionPart]) > parseInt(latestVersionSplit[versionPart])) { - return true; - } + for (let versionPart in currentVersionSplit) { + if (parseInt(currentVersionSplit[versionPart]) > parseInt(latestVersionSplit[versionPart])) { + return true; + } - if (parseInt(currentVersionSplit[versionPart]) < parseInt(latestVersionSplit[versionPart])) { - return false; - } - } + if (parseInt(currentVersionSplit[versionPart]) < parseInt(latestVersionSplit[versionPart])) { + return false; + } + } - return true; + return true; }; const Version = (): ReactElement => { - const version = useSelector(getVersion); - - useEffect(() => { - fetchVersion(); - }, []); - - if (version === null) { - return Checking for the latest version...; - } - - if (version.latest === null) { - return Not able to fetch the latest version right now.; - } - - if (isNewest('PACKAGE_VERSION', version.latest)) { - return You are on the latest version.; - } - - return ( - - A newer version {version.latest} is available. You're currently running PACKAGE_VERSION, consider - upgrading. - - ); + const version = useSelector(getVersion); + + useEffect(() => { + fetchVersion(); + }, []); + + if (version === null) { + return Checking for the latest version...; + } + + if (version.latest === null) { + return Not able to fetch the latest version right now.; + } + + if (isNewest('PACKAGE_VERSION', version.latest)) { + return You are on the latest version.; + } + + return ( + + A newer version {version.latest} is available. You're currently running PACKAGE_VERSION, consider + upgrading. + + ); }; export default Version; diff --git a/frontend/App/SettingsPanel/Customization/Customization.style.tsx b/frontend/App/SettingsPanel/Customization/Customization.style.tsx index cd810a32..012d9011 100644 --- a/frontend/App/SettingsPanel/Customization/Customization.style.tsx +++ b/frontend/App/SettingsPanel/Customization/Customization.style.tsx @@ -3,27 +3,27 @@ import styled from 'styled-components'; import { textMutedColor } from '/frontend/style/colors'; export const Setting = styled.div` - border-top: 2px solid #f0f0f0; - padding: 1rem 0; - display: flex; - align-items: center; - gap: 1rem; + border-top: 2px solid #f0f0f0; + padding: 1rem 0; + display: flex; + align-items: center; + gap: 1rem; `; export const About = styled.div` - flex-grow: 1; - line-height: 1.4; + flex-grow: 1; + line-height: 1.4; `; export const Title = styled.div` - font-size: 1.4rem; + font-size: 1.4rem; `; export const Description = styled.div` - color: ${textMutedColor}; - font-size: 1rem; + color: ${textMutedColor}; + font-size: 1rem; `; export const Tool = styled.div` - flex-shrink: 0; + flex-shrink: 0; `; diff --git a/frontend/App/SettingsPanel/Customization/Customization.tsx b/frontend/App/SettingsPanel/Customization/Customization.tsx index c0cd94d4..0b23e5b0 100644 --- a/frontend/App/SettingsPanel/Customization/Customization.tsx +++ b/frontend/App/SettingsPanel/Customization/Customization.tsx @@ -11,68 +11,68 @@ import { setSizeModifier, toggleShowCompleted, toggleShowUserAvatars } from '/fr import { getSizeModifier, isShowingCompleted, isShowingUserAvatars } from '/frontend/store/settings/selectors'; const Customization = (): ReactElement => { - const showCompleted = useSelector(isShowingCompleted); - const sizeModifier = useSelector(getSizeModifier); - const showUserAvatars = useSelector(isShowingUserAvatars); - const dispatch = useDispatch(); + const showCompleted = useSelector(isShowingCompleted); + const sizeModifier = useSelector(getSizeModifier); + const showUserAvatars = useSelector(isShowingUserAvatars); + const dispatch = useDispatch(); - const server = ; + const server = ; - return ( - -

- You can customize your dashboard with the settings below. Note that all settings with a {server} are - server settings, and are changed for everyone. -

- - - Show completed steps - - Do you want to see all successfully completed steps that your status went trough? - - - - dispatch(toggleShowCompleted())} enabled={showCompleted} /> - - - - - Show user avatars - Do you want to see the user avatar images? - - - dispatch(toggleShowUserAvatars())} enabled={showUserAvatars} /> - - - - - Status size modifier - - When displaying statuses on a centralized monitor, you probably want to increase the size of the - text. This allows you to make statuses easier to read from a distance. - - - - dispatch(setSizeModifier(modifier))} - /> - - - - - Remove statuses after - - {server} Automatically remove statuses older than x days. Not yet customisable in this version. - - - 7 days - -
- ); + return ( + +

+ You can customize your dashboard with the settings below. Note that all settings with a {server} are + server settings, and are changed for everyone. +

+ + + Show completed steps + + Do you want to see all successfully completed steps that your status went trough? + + + + dispatch(toggleShowCompleted())} enabled={showCompleted} /> + + + + + Show user avatars + Do you want to see the user avatar images? + + + dispatch(toggleShowUserAvatars())} enabled={showUserAvatars} /> + + + + + Status size modifier + + When displaying statuses on a centralized monitor, you probably want to increase the size of the + text. This allows you to make statuses easier to read from a distance. + + + + dispatch(setSizeModifier(modifier))} + /> + + + + + Remove statuses after + + {server} Automatically remove statuses older than x days. Not yet customisable in this version. + + + 7 days + +
+ ); }; export default Customization; diff --git a/frontend/App/SettingsPanel/Documentation/Documentation.style.tsx b/frontend/App/SettingsPanel/Documentation/Documentation.style.tsx index fd5e64eb..1ddc7e74 100644 --- a/frontend/App/SettingsPanel/Documentation/Documentation.style.tsx +++ b/frontend/App/SettingsPanel/Documentation/Documentation.style.tsx @@ -1,8 +1,8 @@ import styled from 'styled-components'; export const Frame = styled.iframe` - border: 0; - outline: 0; - width: 100%; - flex-grow: 1; + border: 0; + outline: 0; + width: 100%; + flex-grow: 1; `; diff --git a/frontend/App/SettingsPanel/Documentation/Documentation.tsx b/frontend/App/SettingsPanel/Documentation/Documentation.tsx index d31e28d1..f87cd369 100644 --- a/frontend/App/SettingsPanel/Documentation/Documentation.tsx +++ b/frontend/App/SettingsPanel/Documentation/Documentation.tsx @@ -3,7 +3,7 @@ import { ReactElement } from 'react'; import { Frame } from './Documentation.style'; const Documentation = (): ReactElement => { - return ; + return ; }; export default Documentation; diff --git a/frontend/App/SettingsPanel/SettingsPanel.style.tsx b/frontend/App/SettingsPanel/SettingsPanel.style.tsx index 4edd2cd9..8159b5f4 100644 --- a/frontend/App/SettingsPanel/SettingsPanel.style.tsx +++ b/frontend/App/SettingsPanel/SettingsPanel.style.tsx @@ -3,78 +3,78 @@ import styled, { css } from 'styled-components'; import { stateColor, stateDarkColor, textColor } from '/frontend/style/colors'; export const Overlay = styled.div` - position: fixed; - z-index: 50; - top: 0; - left: 0; - height: 100vh; - width: 100%; - display: flex; - justify-content: center; - align-items: center; - background: rgba(255, 255, 255, 0.3); - border: 0; - outline: 0; + position: fixed; + z-index: 50; + top: 0; + left: 0; + height: 100vh; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + background: rgba(255, 255, 255, 0.3); + border: 0; + outline: 0; `; export const Frame = styled.div` - background: #fff; - color: ${textColor}; - width: 65rem; - max-width: 100%; - margin: 1rem 1rem; - height: calc(100vh - 15rem); - max-height: 65rem; - border-radius: 1rem; - overflow: hidden; - display: flex; - flex-direction: column; + background: #fff; + color: ${textColor}; + width: 65rem; + max-width: 100%; + margin: 1rem 1rem; + height: calc(100vh - 15rem); + max-height: 65rem; + border-radius: 1rem; + overflow: hidden; + display: flex; + flex-direction: column; `; export const TitleBar = styled.div` - display: flex; - background: #333; - height: 3rem; - align-items: center; - flex-shrink: 0; + display: flex; + background: #333; + height: 3rem; + align-items: center; + flex-shrink: 0; `; export const Title = styled.div` - color: #fff; - padding: 0 1rem; - flex-grow: 1; + color: #fff; + padding: 0 1rem; + flex-grow: 1; `; export const Close = styled.button` - font-size: 1.5rem; - color: #fff; - align-self: stretch; - padding: 0 0.5rem; - cursor: pointer; + font-size: 1.5rem; + color: #fff; + align-self: stretch; + padding: 0 0.5rem; + cursor: pointer; `; export const Tabs = styled.div` - display: flex; - flex-wrap: wrap; - background: ${stateColor.success}; + display: flex; + flex-wrap: wrap; + background: ${stateColor.success}; `; type TabProps = { - active: boolean; + active: boolean; }; export const Tab = styled.button` - padding: 0.8rem 1rem; + padding: 0.8rem 1rem; - ${(props) => - props.active && - css` - background: ${stateDarkColor.success}; - `} + ${(props) => + props.active && + css` + background: ${stateDarkColor.success}; + `} `; export const Content = styled.div` - flex-grow: 1; - overflow: auto; - padding: 1rem; + flex-grow: 1; + overflow: auto; + padding: 1rem; `; diff --git a/frontend/App/SettingsPanel/SettingsPanel.tsx b/frontend/App/SettingsPanel/SettingsPanel.tsx index fce0ef9f..fe55c2d9 100644 --- a/frontend/App/SettingsPanel/SettingsPanel.tsx +++ b/frontend/App/SettingsPanel/SettingsPanel.tsx @@ -14,70 +14,70 @@ import Documentation from './Documentation'; import Statuses from './Statuses'; type SettingsTab = { - icon: string; - name: string; - content: ReactElement; + icon: string; + name: string; + content: ReactElement; }; const tabs: SettingsTab[] = [ - { - icon: 'help_outline', - name: 'About', - content: , - }, - { - icon: 'rule', - name: 'Statuses', - content: , - }, - { - icon: 'brush', - name: 'Customization', - content: , - }, - { - icon: 'subject', - name: 'Documentation', - content: , - }, + { + icon: 'help_outline', + name: 'About', + content: , + }, + { + icon: 'rule', + name: 'Statuses', + content: , + }, + { + icon: 'brush', + name: 'Customization', + content: , + }, + { + icon: 'subject', + name: 'Documentation', + content: , + }, ]; const SettingsPanel = (): ReactElement => { - let open = useSelector(isSettingsPanelOpen); - const noStatuses = useSelector(hasNoStatuses); - const dispatch = useDispatch(); - const [activeTab, setActiveTab] = useState(tabs[0].icon); + let open = useSelector(isSettingsPanelOpen); + const noStatuses = useSelector(hasNoStatuses); + const dispatch = useDispatch(); + const [activeTab, setActiveTab] = useState(tabs[0].icon); - if (noStatuses) { - open = true; - } + if (noStatuses) { + open = true; + } - if (!open) { - return null; - } + if (!open) { + return null; + } - return ( - - - - CIMonitor version PACKAGE_VERSION - {!noStatuses && ( - dispatch(closeSettingsPanel())}> - - - )} - - - {tabs.map((tab) => ( - setActiveTab(tab.icon)}> - {tab.name} - - ))} - - {tabs.find((tab) => tab.icon === activeTab).content} - - - ); + return ( + + + + CIMonitor version PACKAGE_VERSION + {!noStatuses && ( + dispatch(closeSettingsPanel())}> + + + )} + + + {tabs.map((tab) => ( + setActiveTab(tab.icon)}> + {tab.name} + + ))} + + {tabs.find((tab) => tab.icon === activeTab).content} + + + ); }; export default SettingsPanel; diff --git a/frontend/App/SettingsPanel/Statuses/Statuses.style.tsx b/frontend/App/SettingsPanel/Statuses/Statuses.style.tsx index 0a05ab3d..118dc04d 100644 --- a/frontend/App/SettingsPanel/Statuses/Statuses.style.tsx +++ b/frontend/App/SettingsPanel/Statuses/Statuses.style.tsx @@ -5,80 +5,80 @@ import { stateColor, stateLightColor } from '/frontend/style/colors'; import { State } from '/types/status'; export const DeleteButton = styled.button` - padding: 0.5rem; - transition: background-color 300ms; - border-radius: 0.3rem; - flex-shrink: 0; + padding: 0.5rem; + transition: background-color 300ms; + border-radius: 0.3rem; + flex-shrink: 0; - &:hover { - background: ${stateColor.error}; - } + &:hover { + background: ${stateColor.error}; + } `; type StatusProps = { - state: State; + state: State; }; export const Intro = styled.div` - flex-grow: 1; - line-height: 1.4; + flex-grow: 1; + line-height: 1.4; `; export const Details = styled.div` - flex-grow: 1; - padding-left: 1rem; - line-height: 1.4; + flex-grow: 1; + padding-left: 1rem; + line-height: 1.4; `; export const Status = styled.div` - position: relative; - padding: 1rem; - margin: 0 -1rem; - border-top: 2px solid #f0f0f0; - display: flex; - align-items: center; - transition: background-color 300ms; + position: relative; + padding: 1rem; + margin: 0 -1rem; + border-top: 2px solid #f0f0f0; + display: flex; + align-items: center; + transition: background-color 300ms; - ${Details}::before { - content: ' '; - position: absolute; - top: 0.75rem; - left: 1rem; - height: auto; - bottom: 0.75rem; - width: 0.2rem; - border-radius: 0.1rem; - background: ${(props) => stateColor[props.state]}; - } + ${Details}::before { + content: ' '; + position: absolute; + top: 0.75rem; + left: 1rem; + height: auto; + bottom: 0.75rem; + width: 0.2rem; + border-radius: 0.1rem; + background: ${(props) => stateColor[props.state]}; + } - &:hover { - background: ${(props) => stateLightColor[props.state]}; - } + &:hover { + background: ${(props) => stateLightColor[props.state]}; + } `; export const Header = styled.div` - margin-bottom: 1rem; - display: flex; - align-items: center; - gap: 1rem; + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 1rem; `; export const Title = styled.h1` - margin-bottom: 0; - flex-grow: 1; + margin-bottom: 0; + flex-grow: 1; `; export const Project = styled.div` - font-size: 1.3rem; + font-size: 1.3rem; `; export const Boxes = styled.div` - display: flex; - flex-wrap: wrap; + display: flex; + flex-wrap: wrap; `; export const Box = styled.div` - font-size: 16px; - flex-shrink: 0; - margin-right: 0.5rem; + font-size: 16px; + flex-shrink: 0; + margin-right: 0.5rem; `; diff --git a/frontend/App/SettingsPanel/Statuses/Statuses.tsx b/frontend/App/SettingsPanel/Statuses/Statuses.tsx index e6cff401..43ab4dba 100644 --- a/frontend/App/SettingsPanel/Statuses/Statuses.tsx +++ b/frontend/App/SettingsPanel/Statuses/Statuses.tsx @@ -11,56 +11,56 @@ import Icon from '/frontend/components/Icon'; import { getStatuses } from '/frontend/store/status/selectors'; const Statuses = (): ReactElement => { - const statuses = useSelector(getStatuses); + const statuses = useSelector(getStatuses); - return ( - -
- - Statuses - Be aware that removing statuses is happening on all connected CIMonitors. - - {statuses.length > 0 && ( - deleteAllStatuses()}> - delete all - - )} -
- {statuses.length === 0 && ( - - There are no statuses yet to display. Trigger a webhook to push now statuses to your dashboard. - - )} - {statuses.map((status) => ( - -
- {status.project} - - - {status.source} - - {status.branch && ( - - {status.branch} - - )} - {status.tag && ( - - {status.tag} - - )} - - - - -
- deleteStatus(status.id)}> - delete - -
- ))} -
- ); + return ( + +
+ + Statuses + Be aware that removing statuses is happening on all connected CIMonitors. + + {statuses.length > 0 && ( + deleteAllStatuses()}> + delete all + + )} +
+ {statuses.length === 0 && ( + + There are no statuses yet to display. Trigger a webhook to push now statuses to your dashboard. + + )} + {statuses.map((status) => ( + +
+ {status.project} + + + {status.source} + + {status.branch && ( + + {status.branch} + + )} + {status.tag && ( + + {status.tag} + + )} + + + + +
+ deleteStatus(status.id)}> + delete + +
+ ))} +
+ ); }; export default Statuses; diff --git a/frontend/App/SocketConnection/SocketConnection.style.tsx b/frontend/App/SocketConnection/SocketConnection.style.tsx index 177850fa..18938c01 100644 --- a/frontend/App/SocketConnection/SocketConnection.style.tsx +++ b/frontend/App/SocketConnection/SocketConnection.style.tsx @@ -1,14 +1,14 @@ import styled from 'styled-components'; export const WarningBar = styled.div` - position: fixed; - z-index: 150; - top: 0; - left: 0; - width: 100%; - font-size: 4rem; - background: rgba(208, 0, 0, 0.8); - color: white; - padding: 1rem; - text-align: center; + position: fixed; + z-index: 150; + top: 0; + left: 0; + width: 100%; + font-size: 4rem; + background: rgba(208, 0, 0, 0.8); + color: white; + padding: 1rem; + text-align: center; `; diff --git a/frontend/App/SocketConnection/SocketConnection.tsx b/frontend/App/SocketConnection/SocketConnection.tsx index fb6a2504..21a6e3d2 100644 --- a/frontend/App/SocketConnection/SocketConnection.tsx +++ b/frontend/App/SocketConnection/SocketConnection.tsx @@ -3,15 +3,15 @@ import { ReactElement } from 'react'; import { WarningBar } from './SocketConnection.style'; type Props = { - connected: boolean; + connected: boolean; }; const SocketConnection = ({ connected }: Props): ReactElement | null => { - if (connected) { - return null; - } + if (connected) { + return null; + } - return Connection lost; + return Connection lost; }; export default SocketConnection; diff --git a/frontend/App/Statuses/Mock/Mock.style.tsx b/frontend/App/Statuses/Mock/Mock.style.tsx index 233c7f7f..7f931c58 100644 --- a/frontend/App/Statuses/Mock/Mock.style.tsx +++ b/frontend/App/Statuses/Mock/Mock.style.tsx @@ -5,16 +5,16 @@ import { stateColor } from '/frontend/style/colors'; import { State } from '/types/status'; export const Mocks = styled.div` - height: 100vh; - overflow: hidden; + height: 100vh; + overflow: hidden; `; type MockProps = { - state: State; + state: State; }; export const Mock = styled.div` - height: 10rem; - background-color: ${(props) => stateColor[props.state]}; - margin-bottom: 1rem; + height: 10rem; + background-color: ${(props) => stateColor[props.state]}; + margin-bottom: 1rem; `; diff --git a/frontend/App/Statuses/Mock/Mock.tsx b/frontend/App/Statuses/Mock/Mock.tsx index c1d05095..ff07da5f 100644 --- a/frontend/App/Statuses/Mock/Mock.tsx +++ b/frontend/App/Statuses/Mock/Mock.tsx @@ -5,49 +5,49 @@ import { Mock, Mocks } from './Mock.style'; import { State } from '/types/status'; const randomState = (): State => { - const random = 100 * Math.random(); + const random = 100 * Math.random(); - if (random > 20) { - return 'success'; - } + if (random > 20) { + return 'success'; + } - if (random > 10) { - return 'warning'; - } + if (random > 10) { + return 'warning'; + } - if (random > 5) { - return 'error'; - } + if (random > 5) { + return 'error'; + } - return 'info'; + return 'info'; }; const getRandomMocks = (): ReactElement[] => { - const mocks = []; + const mocks = []; - for (let mockCount = 1; mockCount <= 20; mockCount++) { - mocks.push(); - } + for (let mockCount = 1; mockCount <= 20; mockCount++) { + mocks.push(); + } - return mocks; + return mocks; }; const MockStatuses = (): ReactElement => { - const [mocks, setMocks] = useState(getRandomMocks()); + const [mocks, setMocks] = useState(getRandomMocks()); - useEffect(() => { - const interval = setInterval(() => setMocks(getRandomMocks()), 10000); + useEffect(() => { + const interval = setInterval(() => setMocks(getRandomMocks()), 10000); - return () => clearInterval(interval); - }, []); + return () => clearInterval(interval); + }, []); - return ( - - {mocks.map((mock, index) => ( - {mock} - ))} - - ); + return ( + + {mocks.map((mock, index) => ( + {mock} + ))} + + ); }; export default MockStatuses; diff --git a/frontend/App/Statuses/Status/Merge/Merge.tsx b/frontend/App/Statuses/Status/Merge/Merge.tsx index dd71c4cf..6d2db2a2 100644 --- a/frontend/App/Statuses/Status/Merge/Merge.tsx +++ b/frontend/App/Statuses/Status/Merge/Merge.tsx @@ -5,28 +5,28 @@ import { Box, LinkBox } from '/frontend/App/Statuses/Status/Status.style'; import Icon from '/frontend/components/Icon'; type Props = { - title?: string; - url?: string; + title?: string; + url?: string; }; const Merge = ({ title, url }: Props): ReactElement | null => { - if (!title && !url) { - return null; - } + if (!title && !url) { + return null; + } - if (url) { - return ( - - {title || 'request'} - - ); - } + if (url) { + return ( + + {title || 'request'} + + ); + } - return ( - - {title} - - ); + return ( + + {title} + + ); }; export default Merge; diff --git a/frontend/App/Statuses/Status/Process/Process.style.tsx b/frontend/App/Statuses/Status/Process/Process.style.tsx index b877467c..5bff8b45 100644 --- a/frontend/App/Statuses/Status/Process/Process.style.tsx +++ b/frontend/App/Statuses/Status/Process/Process.style.tsx @@ -7,122 +7,122 @@ import { ellipsis } from '/frontend/style/text'; import { State, StepState } from '/types/status'; type ProcessContainerProps = { - state: State; + state: State; }; export const ProcessContainer = styled.div` - padding: 0 0.75rem 0.75rem 0.75rem; - background: ${(props) => stateColor[props.state]}; + padding: 0 0.75rem 0.75rem 0.75rem; + background: ${(props) => stateColor[props.state]}; `; export const Stages = styled.div` - display: flex; - margin-top: 0.4rem; - font-size: 1.15em; - flex-wrap: wrap; - border-radius: 0.5rem; - overflow: hidden; - - ${fromSize.small(css` - flex-wrap: nowrap; - `)} + display: flex; + margin-top: 0.4rem; + font-size: 1.15em; + flex-wrap: wrap; + border-radius: 0.5rem; + overflow: hidden; + + ${fromSize.small(css` + flex-wrap: nowrap; + `)} `; type StageProps = { - state: StepState; - processState: State; + state: StepState; + processState: State; }; export const Stage = styled.div` - background: ${(props) => stateDarkColor[props.state] || stateDarkColor.success}; - padding: 0.3rem 0.5rem; - ${ellipsis}; + background: ${(props) => stateDarkColor[props.state] || stateDarkColor.success}; + padding: 0.3rem 0.5rem; + ${ellipsis}; - ${fromSize.small(css` - width: auto; - `)} + ${fromSize.small(css` + width: auto; + `)} - ${(props) => - ['running'].includes(props.state) && - css` - background: ${stateDarkColor['warning']}; - `} + ${(props) => + ['running'].includes(props.state) && + css` + background: ${stateDarkColor['warning']}; + `} ${(props) => - ['pending'].includes(props.state) && - css` - background: ${opacity(stateDarkColor[props.processState], 0.5)}; - `} + ['pending'].includes(props.state) && + css` + background: ${opacity(stateDarkColor[props.processState], 0.5)}; + `} ${(props) => - ['failed', 'timeout'].includes(props.state) && - css` - background: ${stateDarkColor['error']}; - `} + ['failed', 'timeout'].includes(props.state) && + css` + background: ${stateDarkColor['error']}; + `} `; type StepProps = { - state: StepState; - processState: State; + state: StepState; + processState: State; }; export const Step = styled.div` - background: ${(props) => stateDarkColor[props.state]}; - padding: 0.3rem 0.5rem 0.3rem 2.3rem; - ${ellipsis}; - - ${fromSize.small(css` - border-radius: 0.5rem; - margin: 0.5rem 0.5rem 0 0; - padding: 0.3rem 0.5rem; - `)}; - - ${(props) => - ['running', 'soft-failed'].includes(props.state) && - css` - background: ${stateDarkColor.warning}; - `} - - ${(props) => - ['pending', 'skipped'].includes(props.state) && - css` - background: ${opacity(stateDarkColor[props.processState], 0.5)}; - `} + background: ${(props) => stateDarkColor[props.state]}; + padding: 0.3rem 0.5rem 0.3rem 2.3rem; + ${ellipsis}; + + ${fromSize.small(css` + border-radius: 0.5rem; + margin: 0.5rem 0.5rem 0 0; + padding: 0.3rem 0.5rem; + `)}; + + ${(props) => + ['running', 'soft-failed'].includes(props.state) && + css` + background: ${stateDarkColor.warning}; + `} + + ${(props) => + ['pending', 'skipped'].includes(props.state) && + css` + background: ${opacity(stateDarkColor[props.processState], 0.5)}; + `} ${(props) => - ['failed', 'timeout'].includes(props.state) && - css` - background: ${stateDarkColor.error}; - `} + ['failed', 'timeout'].includes(props.state) && + css` + background: ${stateDarkColor.error}; + `} `; export const StageContainer = styled.div` - flex-grow: 1; - flex-shrink: 1; - min-width: 4rem; - width: 100%; - - ${fromSize.small(css` - width: auto; - - &:first-child { - ${Stage} { - border-bottom-left-radius: 0.5rem; - } - } - - &:last-child { - ${Stage} { - border-bottom-right-radius: 0.5rem; - } - - ${Step} { - margin-right: 0; - } - } - `)} + flex-grow: 1; + flex-shrink: 1; + min-width: 4rem; + width: 100%; + + ${fromSize.small(css` + width: auto; + + &:first-child { + ${Stage} { + border-bottom-left-radius: 0.5rem; + } + } + + &:last-child { + ${Stage} { + border-bottom-right-radius: 0.5rem; + } + + ${Step} { + margin-right: 0; + } + } + `)} `; export const Details = styled.div` - ${ellipsis}; + ${ellipsis}; `; diff --git a/frontend/App/Statuses/Status/Process/Process.tsx b/frontend/App/Statuses/Status/Process/Process.tsx index bcdd4cfa..f951becd 100644 --- a/frontend/App/Statuses/Status/Process/Process.tsx +++ b/frontend/App/Statuses/Status/Process/Process.tsx @@ -9,60 +9,60 @@ import { isShowingCompleted } from '/frontend/store/settings/selectors'; import { Process as ProcessType, Stage as StageType, State, Step as StepType, StepState } from '/types/status'; const getStateIcon = (state: StepState, processState: State = 'warning') => { - const icons = { - running: 'autorenew', - success: 'done', - failed: 'clear', - error: 'clear', - warning: 'warning_amber', - 'soft-failed': 'report_problem', - pending: processState === 'warning' ? 'update' : 'skip_next', - created: 'push_pin', - skipped: 'skip_next', - timeout: 'alarm', - }; + const icons = { + running: 'autorenew', + success: 'done', + failed: 'clear', + error: 'clear', + warning: 'warning_amber', + 'soft-failed': 'report_problem', + pending: processState === 'warning' ? 'update' : 'skip_next', + created: 'push_pin', + skipped: 'skip_next', + timeout: 'alarm', + }; - return icons[state] || 'info'; + return icons[state] || 'info'; }; type Props = { - process: ProcessType; + process: ProcessType; }; const Process = ({ process }: Props): ReactElement => { - const showCompleted = useSelector(isShowingCompleted); + const showCompleted = useSelector(isShowingCompleted); - const renderStep = (step: StepType): ReactElement | null => { - if (['success', 'skipped'].includes(step.state) && !showCompleted) { - return null; - } + const renderStep = (step: StepType): ReactElement | null => { + if (['success', 'skipped'].includes(step.state) && !showCompleted) { + return null; + } - return ( - - {step.title} - - ); - }; + return ( + + {step.title} + + ); + }; - const renderStage = (stage: StageType): ReactElement => ( - - - {stage.title} - - {stage.steps && stage.steps.map((step) => renderStep(step))} - - ); + const renderStage = (stage: StageType): ReactElement => ( + + + {stage.title} + + {stage.steps && stage.steps.map((step) => renderStep(step))} + + ); - return ( - -
- {process.title} -
- {process.stages && process.stages.length > 0 && ( - {process.stages.map((stage) => renderStage(stage))} - )} -
- ); + return ( + +
+ {process.title} +
+ {process.stages && process.stages.length > 0 && ( + {process.stages.map((stage) => renderStage(stage))} + )} +
+ ); }; export default Process; diff --git a/frontend/App/Statuses/Status/ProjectImage/ProjectImage.style.tsx b/frontend/App/Statuses/Status/ProjectImage/ProjectImage.style.tsx index eaf433b2..9423cb77 100644 --- a/frontend/App/Statuses/Status/ProjectImage/ProjectImage.style.tsx +++ b/frontend/App/Statuses/Status/ProjectImage/ProjectImage.style.tsx @@ -1,11 +1,11 @@ import styled from 'styled-components'; export const Container = styled.div` - margin-right: 1rem; + margin-right: 1rem; - img { - border-radius: 0.25rem; - max-width: 6rem; - max-height: 6rem; - } + img { + border-radius: 0.25rem; + max-width: 6rem; + max-height: 6rem; + } `; diff --git a/frontend/App/Statuses/Status/ProjectImage/ProjectImage.tsx b/frontend/App/Statuses/Status/ProjectImage/ProjectImage.tsx index b9c8c3a3..ccda60ea 100644 --- a/frontend/App/Statuses/Status/ProjectImage/ProjectImage.tsx +++ b/frontend/App/Statuses/Status/ProjectImage/ProjectImage.tsx @@ -3,22 +3,22 @@ import { ReactElement, useState } from 'react'; import { Container } from './ProjectImage.style'; type Props = { - url: string; - alt: string; + url: string; + alt: string; }; const ProjectImage = ({ url, alt }: Props): ReactElement | null => { - const [isNotLoading, setNotLoading] = useState(false); + const [isNotLoading, setNotLoading] = useState(false); - if (isNotLoading) { - return null; - } + if (isNotLoading) { + return null; + } - return ( - - {alt} setNotLoading(true)} /> - - ); + return ( + + {alt} setNotLoading(true)} /> + + ); }; export default ProjectImage; diff --git a/frontend/App/Statuses/Status/Source/Source.tsx b/frontend/App/Statuses/Status/Source/Source.tsx index f2758c57..c1d614b6 100644 --- a/frontend/App/Statuses/Status/Source/Source.tsx +++ b/frontend/App/Statuses/Status/Source/Source.tsx @@ -9,37 +9,37 @@ import ReadTheDocs from './icon/readthedocs.svg'; import Icon from '/frontend/components/Icon'; type Props = { - type: string; - url?: string | null; + type: string; + url?: string | null; }; const Source = ({ type, url }: Props): ReactElement => { - const getIcon = (type: string): ReactElement => { - switch (type) { - case 'gitlab': - return ; - case 'github': - return ; - case 'readthedocs': - return ; - default: - return ( - <> - {type} - - ); - } - }; - - if (url) { - return ( - - {getIcon(type)} - - ); - } - - return {getIcon(type)}; + const getIcon = (type: string): ReactElement => { + switch (type) { + case 'gitlab': + return ; + case 'github': + return ; + case 'readthedocs': + return ; + default: + return ( + <> + {type} + + ); + } + }; + + if (url) { + return ( + + {getIcon(type)} + + ); + } + + return {getIcon(type)}; }; export default Source; diff --git a/frontend/App/Statuses/Status/Status.style.tsx b/frontend/App/Statuses/Status/Status.style.tsx index b3572a54..4f3a0202 100644 --- a/frontend/App/Statuses/Status/Status.style.tsx +++ b/frontend/App/Statuses/Status/Status.style.tsx @@ -7,101 +7,101 @@ import { ellipsis, ellipsisLeft } from '/frontend/style/text'; import { State } from '/types/status'; export const Boxes = styled.div` - display: flex; - gap: 0.5rem; - flex-wrap: wrap; + display: flex; + gap: 0.5rem; + flex-wrap: wrap; `; const boxBase = css` - position: relative; - font-size: 1.15em; - padding: 0.3rem 0.5rem; - border-radius: 0.25rem; - min-width: 1.6em; - min-height: 2rem; - ${ellipsis}; - - svg { - position: absolute; - top: 50%; - left: 50%; - margin: -0.5em 0 0 -0.5em; - height: 1em; - width: 1em; - transform: scale(1.2); - } + position: relative; + font-size: 1.15em; + padding: 0.3rem 0.5rem; + border-radius: 0.25rem; + min-width: 1.6em; + min-height: 2rem; + ${ellipsis}; + + svg { + position: absolute; + top: 50%; + left: 50%; + margin: -0.5em 0 0 -0.5em; + height: 1em; + width: 1em; + transform: scale(1.2); + } `; export const Box = styled.div` - ${boxBase}; + ${boxBase}; `; export const LinkBox = styled.a` - text-decoration: none; - color: ${textColor}; - ${boxBase}; + text-decoration: none; + color: ${textColor}; + ${boxBase}; `; export const Details = styled.div` - flex-grow: 1; - flex-shrink: 1; - min-width: 5rem; + flex-grow: 1; + flex-shrink: 1; + min-width: 5rem; `; export const Project = styled.h1` - margin-bottom: 0.5rem; - font-size: 1.5em; - ${ellipsisLeft}; + margin-bottom: 0.5rem; + font-size: 1.5em; + ${ellipsisLeft}; - ${fromSize.small(css` - font-size: 2em; - `)} + ${fromSize.small(css` + font-size: 2em; + `)} `; type StatusProps = { - state: State; + state: State; }; export const Container = styled.div` - background: ${(props) => stateColor[props.state]}; - color: ${textColor}; - margin-bottom: 1rem; - max-width: 100%; - overflow: hidden; - - ${Box}, ${LinkBox} { - background: ${(props) => stateDarkColor[props.state]}; - - svg { - fill: ${textColor}; - } - } - - ${LinkBox}:hover { - background: ${textColor} !important; - color: ${(props) => stateDarkColor[props.state]}; - - svg { - fill: ${(props) => stateDarkColor[props.state]}; - } - } - - &:last-child { - margin-bottom: 0; - } + background: ${(props) => stateColor[props.state]}; + color: ${textColor}; + margin-bottom: 1rem; + max-width: 100%; + overflow: hidden; + + ${Box}, ${LinkBox} { + background: ${(props) => stateDarkColor[props.state]}; + + svg { + fill: ${textColor}; + } + } + + ${LinkBox}:hover { + background: ${textColor} !important; + color: ${(props) => stateDarkColor[props.state]}; + + svg { + fill: ${(props) => stateDarkColor[props.state]}; + } + } + + &:last-child { + margin-bottom: 0; + } `; export const Body = styled.div` - padding: 0.75rem; - display: flex; + padding: 0.75rem; + display: flex; `; export const UserImage = styled.div` - flex-shrink: 0; + flex-shrink: 0; - img { - border-radius: 50%; - width: 6rem; - height: 6rem; - } + img { + border-radius: 50%; + width: 6rem; + height: 6rem; + } `; diff --git a/frontend/App/Statuses/Status/Status.tsx b/frontend/App/Statuses/Status/Status.tsx index b2b88001..b4e70ea4 100644 --- a/frontend/App/Statuses/Status/Status.tsx +++ b/frontend/App/Statuses/Status/Status.tsx @@ -16,58 +16,58 @@ import User from './User'; import Status from '/types/status'; type Props = { - status: Status; + status: Status; }; const pettyUrl = (url: string) => - String(url) - // strip http(s):// - .replace(/^http[s]?:\/\//, '') - // strip / on the end - .replace(/\/$/, ''); + String(url) + // strip http(s):// + .replace(/^http[s]?:\/\//, '') + // strip / on the end + .replace(/\/$/, ''); const Statuses = ({ status }: Props): ReactElement => { - const showUserAvatar = useSelector(isShowingUserAvatars); + const showUserAvatar = useSelector(isShowingUserAvatars); - return ( - - - {status.projectImage && } -
- {status.project} - - - {status.branch && ( - - {status.branch} - - )} - {status.tag && ( - - {status.tag} - - )} - - {status.url && ( - - {pettyUrl(status.url)} - - )} - - - - - -
- {status.userImage && showUserAvatar && ( - - User - - )} - - {status.processes && status.processes.map((process) => )} -
- ); + return ( + + + {status.projectImage && } +
+ {status.project} + + + {status.branch && ( + + {status.branch} + + )} + {status.tag && ( + + {status.tag} + + )} + + {status.url && ( + + {pettyUrl(status.url)} + + )} + + + + + +
+ {status.userImage && showUserAvatar && ( + + User + + )} + + {status.processes && status.processes.map((process) => )} +
+ ); }; export default Statuses; diff --git a/frontend/App/Statuses/Status/TimePassed/TimePassed.tsx b/frontend/App/Statuses/Status/TimePassed/TimePassed.tsx index b1b3527f..d486b7b9 100644 --- a/frontend/App/Statuses/Status/TimePassed/TimePassed.tsx +++ b/frontend/App/Statuses/Status/TimePassed/TimePassed.tsx @@ -1,45 +1,45 @@ import { ReactElement, useEffect, useState } from 'react'; type Props = { - since: string; + since: string; }; const getTimePassed = (since: string): string => { - const timePassed = Math.round((new Date().getTime() - new Date(since).getTime()) / 1000); + const timePassed = Math.round((new Date().getTime() - new Date(since).getTime()) / 1000); - if (timePassed < 10) { - return 'just now'; - } + if (timePassed < 10) { + return 'just now'; + } - if (timePassed < 60) { - return 'last minute'; - } + if (timePassed < 60) { + return 'last minute'; + } - if (timePassed < 60 * 60) { - const minutes = Math.floor(timePassed / 60); - return `${minutes} min${minutes > 1 ? 's' : ''} ago`; - } + if (timePassed < 60 * 60) { + const minutes = Math.floor(timePassed / 60); + return `${minutes} min${minutes > 1 ? 's' : ''} ago`; + } - if (timePassed < 60 * 60 * 24) { - const hours = Math.floor(timePassed / (60 * 60)); - return `${hours} hour${hours > 1 ? 's' : ''} ago`; - } + if (timePassed < 60 * 60 * 24) { + const hours = Math.floor(timePassed / (60 * 60)); + return `${hours} hour${hours > 1 ? 's' : ''} ago`; + } - const days = Math.floor(timePassed / (60 * 60 * 24)); - return `${days} day${days > 1 ? 's' : ''} ago`; + const days = Math.floor(timePassed / (60 * 60 * 24)); + return `${days} day${days > 1 ? 's' : ''} ago`; }; const TimePassed = ({ since }: Props): ReactElement => { - const [timePassed, setTimePassed] = useState(getTimePassed(since)); + const [timePassed, setTimePassed] = useState(getTimePassed(since)); - useEffect(() => { - setTimePassed(getTimePassed(since)); - const interval = setInterval(() => setTimePassed(getTimePassed(since)), 5000); + useEffect(() => { + setTimePassed(getTimePassed(since)); + const interval = setInterval(() => setTimePassed(getTimePassed(since)), 5000); - return () => clearInterval(interval); - }, [since]); + return () => clearInterval(interval); + }, [since]); - return <>{timePassed}; + return <>{timePassed}; }; export default TimePassed; diff --git a/frontend/App/Statuses/Status/User/User.tsx b/frontend/App/Statuses/Status/User/User.tsx index 212be457..3546d14c 100644 --- a/frontend/App/Statuses/Status/User/User.tsx +++ b/frontend/App/Statuses/Status/User/User.tsx @@ -5,28 +5,28 @@ import { Box, LinkBox } from '/frontend/App/Statuses/Status/Status.style'; import Icon from '/frontend/components/Icon'; type Props = { - username?: string; - url?: string; + username?: string; + url?: string; }; const User = ({ username, url }: Props): ReactElement | null => { - if (!username && !url) { - return null; - } + if (!username && !url) { + return null; + } - if (url) { - return ( - - {username || 'User'} - - ); - } + if (url) { + return ( + + {username || 'User'} + + ); + } - return ( - - {username} - - ); + return ( + + {username} + + ); }; export default User; diff --git a/frontend/App/Statuses/Statuses.style.tsx b/frontend/App/Statuses/Statuses.style.tsx index e4c7a669..a11e8dbe 100644 --- a/frontend/App/Statuses/Statuses.style.tsx +++ b/frontend/App/Statuses/Statuses.style.tsx @@ -1,15 +1,15 @@ import styled from 'styled-components'; export const List = styled.div` - height: 100vh; - overflow: auto; - padding-bottom: 4.2rem; + height: 100vh; + overflow: auto; + padding-bottom: 4.2rem; - // Hide scrollbar in FireFox - scrollbar-width: none; + // Hide scrollbar in FireFox + scrollbar-width: none; - // Hide scrollbar in webkit browsers - &::-webkit-scrollbar { - display: none; - } + // Hide scrollbar in webkit browsers + &::-webkit-scrollbar { + display: none; + } `; diff --git a/frontend/App/Statuses/Statuses.tsx b/frontend/App/Statuses/Statuses.tsx index 390500fb..746e5522 100644 --- a/frontend/App/Statuses/Statuses.tsx +++ b/frontend/App/Statuses/Statuses.tsx @@ -10,20 +10,20 @@ import MockStatuses from './Mock'; import Status from './Status'; const Statuses = (): ReactElement => { - const statuses = useSelector(getStatuses); - const sizeModifier = useSelector(getSizeModifier); + const statuses = useSelector(getStatuses); + const sizeModifier = useSelector(getSizeModifier); - if (!statuses || statuses.length === 0) { - return ; - } + if (!statuses || statuses.length === 0) { + return ; + } - return ( - - {statuses.map((status) => ( - - ))} - - ); + return ( + + {statuses.map((status) => ( + + ))} + + ); }; export default Statuses; diff --git a/frontend/App/Toolbar/Toolbar.style.tsx b/frontend/App/Toolbar/Toolbar.style.tsx index 681e39c8..722d5430 100644 --- a/frontend/App/Toolbar/Toolbar.style.tsx +++ b/frontend/App/Toolbar/Toolbar.style.tsx @@ -10,98 +10,98 @@ export const brandSize = 5.5; const brandHeight = 3.5; export const Container = styled.div` - position: fixed; - z-index: 100; - bottom: 0; - left: 50%; - width: ${actionWidth}rem; - height: ${actionHeight}rem; - background: #111; - color: #222; - margin-left: -${actionWidth / 2}rem; - text-align: center; - border-radius: ${radius}rem ${radius}rem 0 0; - border-bottom: 0; + position: fixed; + z-index: 100; + bottom: 0; + left: 50%; + width: ${actionWidth}rem; + height: ${actionHeight}rem; + background: #111; + color: #222; + margin-left: -${actionWidth / 2}rem; + text-align: center; + border-radius: ${radius}rem ${radius}rem 0 0; + border-bottom: 0; `; export const Buttons = styled.div` - position: absolute; - z-index: 120; - display: flex; - width: 100%; - height: ${actionHeight}rem; - top: 0; - left: 0; - justify-content: space-between; + position: absolute; + z-index: 120; + display: flex; + width: 100%; + height: ${actionHeight}rem; + top: 0; + left: 0; + justify-content: space-between; `; export const Button = styled.button` - background: transparent; - color: #fff; - border: 0; - outline: 0; - width: ${(actionWidth - brandSize) / 2}rem; - font-size: ${iconSize}rem; - cursor: pointer; - transition: background 100ms; + background: transparent; + color: #fff; + border: 0; + outline: 0; + width: ${(actionWidth - brandSize) / 2}rem; + font-size: ${iconSize}rem; + cursor: pointer; + transition: background 100ms; - svg { - fill: #fff; - height: 1.5rem; - transform: translateY(0.15rem); - } + svg { + fill: #fff; + height: 1.5rem; + transform: translateY(0.15rem); + } - &:first-child { - border-top-left-radius: ${radius}rem; - } + &:first-child { + border-top-left-radius: ${radius}rem; + } - &:last-child { - border-top-right-radius: ${radius}rem; - } + &:last-child { + border-top-right-radius: ${radius}rem; + } - &:hover { - background: rgba(255, 255, 255, 0.05); - } + &:hover { + background: rgba(255, 255, 255, 0.05); + } `; export const Brand = styled.div` - position: fixed; - z-index: 110; - width: ${brandSize}rem; - height: ${brandSize}rem; - background: #fff; - bottom: 0; - left: 50%; - margin-left: -${brandSize / 2}rem; - border-radius: ${radius}rem ${radius}rem 0 0; - display: flex; - justify-content: center; - align-items: center; + position: fixed; + z-index: 110; + width: ${brandSize}rem; + height: ${brandSize}rem; + background: #fff; + bottom: 0; + left: 50%; + margin-left: -${brandSize / 2}rem; + border-radius: ${radius}rem ${radius}rem 0 0; + display: flex; + justify-content: center; + align-items: center; - &::after, - &::before { - width: ${radius}rem; - height: ${radius}rem; - background: transparent; - bottom: 0; - position: absolute; - content: ''; - box-shadow: 0 0 0 50px #fff; - clip: rect(0, ${radius}rem, ${radius}rem, 0); - display: block; - } + &::after, + &::before { + width: ${radius}rem; + height: ${radius}rem; + background: transparent; + bottom: 0; + position: absolute; + content: ''; + box-shadow: 0 0 0 50px #fff; + clip: rect(0, ${radius}rem, ${radius}rem, 0); + display: block; + } - &::after { - right: -${radius}rem; - border-bottom-left-radius: 100%; - } + &::after { + right: -${radius}rem; + border-bottom-left-radius: 100%; + } - &::before { - left: -${radius}rem; - border-bottom-right-radius: 100%; - } + &::before { + left: -${radius}rem; + border-bottom-right-radius: 100%; + } - svg { - height: ${brandHeight}rem; - } + svg { + height: ${brandHeight}rem; + } `; diff --git a/frontend/App/Toolbar/Toolbar.tsx b/frontend/App/Toolbar/Toolbar.tsx index 2cabc7e3..874e378f 100644 --- a/frontend/App/Toolbar/Toolbar.tsx +++ b/frontend/App/Toolbar/Toolbar.tsx @@ -13,29 +13,29 @@ import { closeSettingsPanel, toggleSettingsPanel } from '/frontend/store/setting import { getGlobalState, hasNoStatuses } from '/frontend/store/status/selectors'; const Toolbar = (): ReactElement => { - const dashboardState = useSelector(getGlobalState); - const dispatch = useDispatch(); - const noStatuses = useSelector(hasNoStatuses); + const dashboardState = useSelector(getGlobalState); + const dispatch = useDispatch(); + const noStatuses = useSelector(hasNoStatuses); - const light = { - success: , - warning: , - error: , - }; + const light = { + success: , + warning: , + error: , + }; - return ( - - - - - - {light[dashboardState]} - - ); + return ( + + + + + + {light[dashboardState]} + + ); }; export default Toolbar; diff --git a/frontend/api/cimonitor.ts b/frontend/api/cimonitor.ts index f471b53b..4eb3ab0c 100644 --- a/frontend/api/cimonitor.ts +++ b/frontend/api/cimonitor.ts @@ -3,12 +3,12 @@ import axios from 'axios'; export { deleteAllStatuses, deleteStatus } from './cimonitor/status'; export const CIMonitorApi = () => - axios.create({ - headers: { - accept: 'application/json', - 'content-type': 'application/json', - }, - baseURL: '/', - }); + axios.create({ + headers: { + accept: 'application/json', + 'content-type': 'application/json', + }, + baseURL: '/', + }); export default CIMonitorApi; diff --git a/frontend/components/Alert/Alert.style.tsx b/frontend/components/Alert/Alert.style.tsx index df12e3f1..f8b8646c 100644 --- a/frontend/components/Alert/Alert.style.tsx +++ b/frontend/components/Alert/Alert.style.tsx @@ -5,26 +5,26 @@ import { stateColor } from '/frontend/style/colors'; import { State } from '/types/status'; type BoxProps = { - state: State; + state: State; }; export const Container = styled.div` - border-radius: 0.5rem; - display: flex; - gap: 0.5rem; - padding: 1rem; - margin-bottom: 1rem; - background: ${(props) => stateColor[props.state]}; - align-items: center; + border-radius: 0.5rem; + display: flex; + gap: 0.5rem; + padding: 1rem; + margin-bottom: 1rem; + background: ${(props) => stateColor[props.state]}; + align-items: center; - ul:last-child, - p:last-child { - margin-bottom: 0; - } + ul:last-child, + p:last-child { + margin-bottom: 0; + } `; export const IconSpace = styled.div``; export const Message = styled.div` - flex-grow: 1; + flex-grow: 1; `; diff --git a/frontend/components/Alert/Alert.tsx b/frontend/components/Alert/Alert.tsx index 93e4ef24..160fec6d 100644 --- a/frontend/components/Alert/Alert.tsx +++ b/frontend/components/Alert/Alert.tsx @@ -7,33 +7,33 @@ import Icon from '/frontend/components/Icon'; import { State } from '/types/status'; type Props = { - children: ReactNode; - state?: State; + children: ReactNode; + state?: State; }; const getIcon = (state: State): string => { - if (state === 'success') { - return 'check_circle_outline'; - } + if (state === 'success') { + return 'check_circle_outline'; + } - if (state === 'warning') { - return 'warning_amber'; - } + if (state === 'warning') { + return 'warning_amber'; + } - if (state === 'error') { - return 'highlight_off'; - } + if (state === 'error') { + return 'highlight_off'; + } - return 'help_outline'; + return 'help_outline'; }; const Alert = ({ children, state = 'info' }: Props): ReactElement => ( - - - - - {children} - + + + + + {children} + ); export default Alert; diff --git a/frontend/components/Icon/Icon.tsx b/frontend/components/Icon/Icon.tsx index 5a9a2d7f..86b296cd 100644 --- a/frontend/components/Icon/Icon.tsx +++ b/frontend/components/Icon/Icon.tsx @@ -6,35 +6,35 @@ import { stateColor } from '/frontend/style/colors'; import { State } from '/types/status'; type Props = { - icon: string; - state?: State; - title?: string; + icon: string; + state?: State; + title?: string; }; type SpanProps = { - state?: State; + state?: State; }; const Span = styled.span` - ${(props) => - props.state && - css` - color: ${stateColor[props.state]}; - `} + ${(props) => + props.state && + css` + color: ${stateColor[props.state]}; + `} `; const Icon = ({ icon, state, title }: Props): ReactElement => { - const classes = ['icon']; + const classes = ['icon']; - if (['autorenew'].includes(icon)) { - classes.push('spin'); - } + if (['autorenew'].includes(icon)) { + classes.push('spin'); + } - return ( - - {icon} - - ); + return ( + + {icon} + + ); }; export default Icon; diff --git a/frontend/components/Modifier/Modifier.style.tsx b/frontend/components/Modifier/Modifier.style.tsx index 33e4b116..7a39c722 100644 --- a/frontend/components/Modifier/Modifier.style.tsx +++ b/frontend/components/Modifier/Modifier.style.tsx @@ -3,25 +3,25 @@ import styled from 'styled-components'; import { stateColor, stateLightColor, textColor } from '/frontend/style/colors'; export const Container = styled.div` - display: flex; - align-items: center; + display: flex; + align-items: center; `; export const Button = styled.button` - width: 2rem; - height: 2rem; - background: ${stateColor.success}; - color: ${textColor}; - border-radius: 50%; + width: 2rem; + height: 2rem; + background: ${stateColor.success}; + color: ${textColor}; + border-radius: 50%; - &:disabled { - background: ${stateLightColor.warning}; - color: ${stateColor.warning}; - cursor: default; - } + &:disabled { + background: ${stateLightColor.warning}; + color: ${stateColor.warning}; + cursor: default; + } `; export const Value = styled.div` - width: 2.5rem; - text-align: center; + width: 2.5rem; + text-align: center; `; diff --git a/frontend/components/Modifier/Modifier.tsx b/frontend/components/Modifier/Modifier.tsx index e3ec76f2..feb5c2ae 100644 --- a/frontend/components/Modifier/Modifier.tsx +++ b/frontend/components/Modifier/Modifier.tsx @@ -5,42 +5,42 @@ import { Button, Container, Value } from './Modifier.style'; import Icon from '/frontend/components/Icon'; type Props = { - value: number; - min: number; - max: number; - step: number; - // eslint-disable-next-line no-unused-vars - onChange: (modifier: number) => void; + value: number; + min: number; + max: number; + step: number; + // eslint-disable-next-line no-unused-vars + onChange: (modifier: number) => void; }; const Modifier = ({ value, step, min, max, onChange }: Props): ReactElement => { - const increase = (): void => { - let newValue = value + step; + const increase = (): void => { + let newValue = value + step; - newValue = newValue > max ? max : newValue; + newValue = newValue > max ? max : newValue; - onChange(Math.round(newValue * 100) / 100); - }; + onChange(Math.round(newValue * 100) / 100); + }; - const decrease = (): void => { - let newValue = value - step; + const decrease = (): void => { + let newValue = value - step; - newValue = newValue < min ? min : newValue; + newValue = newValue < min ? min : newValue; - onChange(Math.round(newValue * 100) / 100); - }; + onChange(Math.round(newValue * 100) / 100); + }; - return ( - - - {String(value)} - - - ); + return ( + + + {String(value)} + + + ); }; export default Modifier; diff --git a/frontend/components/Toggle/Toggle.style.tsx b/frontend/components/Toggle/Toggle.style.tsx index 5347f8f5..e188c361 100644 --- a/frontend/components/Toggle/Toggle.style.tsx +++ b/frontend/components/Toggle/Toggle.style.tsx @@ -3,50 +3,50 @@ import styled, { css } from 'styled-components'; import { stateColor, textMutedColor } from '/frontend/style/colors'; export const Switch = styled.div` - position: absolute; - top: 0.2rem; - right: 2.1rem; - width: 1.25rem; - height: 1.25rem; - border-radius: 50%; - background: ${textMutedColor}; - transition: right 300ms, background-color 300ms; + position: absolute; + top: 0.2rem; + right: 2.1rem; + width: 1.25rem; + height: 1.25rem; + border-radius: 50%; + background: ${textMutedColor}; + transition: right 300ms, background-color 300ms; `; type ButtonProps = { - enabled: boolean; + enabled: boolean; }; export const Button = styled.button` - position: relative; - border: 0.15rem solid ${textMutedColor}; - width: 4rem; - height: 2rem; - border-radius: 1rem; - transition: border-color 300ms; - margin-bottom: 0.8rem; - - &::after { - content: 'off'; - position: absolute; - top: 1.8rem; - left: 0; - width: 4rem; - text-align: center; - } - - ${(props) => - props.enabled && - css` - border-color: ${stateColor.success}; - - &::after { - content: 'on'; - } - - ${Switch} { - background: ${stateColor.success}; - right: 0.2rem; - } - `} + position: relative; + border: 0.15rem solid ${textMutedColor}; + width: 4rem; + height: 2rem; + border-radius: 1rem; + transition: border-color 300ms; + margin-bottom: 0.8rem; + + &::after { + content: 'off'; + position: absolute; + top: 1.8rem; + left: 0; + width: 4rem; + text-align: center; + } + + ${(props) => + props.enabled && + css` + border-color: ${stateColor.success}; + + &::after { + content: 'on'; + } + + ${Switch} { + background: ${stateColor.success}; + right: 0.2rem; + } + `} `; diff --git a/frontend/components/Toggle/Toggle.tsx b/frontend/components/Toggle/Toggle.tsx index 682f8714..f28cd8a0 100644 --- a/frontend/components/Toggle/Toggle.tsx +++ b/frontend/components/Toggle/Toggle.tsx @@ -3,16 +3,16 @@ import { ReactElement } from 'react'; import { Button, Switch } from './Toggle.style'; type Props = { - onToggle: () => void; - enabled: boolean; + onToggle: () => void; + enabled: boolean; }; const Toggle = ({ enabled, onToggle }: Props): ReactElement => { - return ( - - ); + return ( + + ); }; export default Toggle; diff --git a/frontend/dashboard.tsx b/frontend/dashboard.tsx index b7200c04..5e0d8e92 100644 --- a/frontend/dashboard.tsx +++ b/frontend/dashboard.tsx @@ -8,10 +8,10 @@ import store from './store'; console.log('[frontend] init dashboard.'); render( - - - - - , - document.getElementById('cimonitor') + + + + + , + document.getElementById('cimonitor') ); diff --git a/frontend/globals.css b/frontend/globals.css index 27cd842c..9383a202 100644 --- a/frontend/globals.css +++ b/frontend/globals.css @@ -1,49 +1,49 @@ * { - padding: 0; - margin: 0; - box-sizing: border-box; + padding: 0; + margin: 0; + box-sizing: border-box; } html, body { - font-size: 18px; + font-size: 18px; } body { - background: #333; - color: #fff; - font-family: 'Fredoka', sans-serif; - min-height: 100vh; - line-height: 1.1; + background: #333; + color: #fff; + font-family: 'Fredoka', sans-serif; + min-height: 100vh; + line-height: 1.1; } @font-face { - font-family: 'Fredoka'; - src: url('fonts/fredoka.ttf') format('truetype'); + font-family: 'Fredoka'; + src: url('fonts/fredoka.ttf') format('truetype'); } @font-face { - font-family: 'material-icons-round'; - src: url('fonts/material-icons-round.otf') format('opentype'); + font-family: 'material-icons-round'; + src: url('fonts/material-icons-round.otf') format('opentype'); } .icon { - font-family: 'material-icons-round'; - font-weight: normal; - font-style: normal; - font-size: 1.2em; - vertical-align: -0.2em; - display: inline-block; + font-family: 'material-icons-round'; + font-weight: normal; + font-style: normal; + font-size: 1.2em; + vertical-align: -0.2em; + display: inline-block; } .icon.spin { - animation: spin-ani 3s linear infinite; + animation: spin-ani 3s linear infinite; } @keyframes spin-ani { - 100% { - transform: rotate(360deg); - } + 100% { + transform: rotate(360deg); + } } h1, @@ -51,19 +51,19 @@ h2, h3, h4, h5 { - font-weight: normal; - margin-bottom: 1rem; + font-weight: normal; + margin-bottom: 1rem; } p { - margin-bottom: 1rem; - line-height: 1.4; + margin-bottom: 1rem; + line-height: 1.4; } ul { - margin-left: 2rem; - margin-bottom: 1rem; - line-height: 1.3; + margin-left: 2rem; + margin-bottom: 1rem; + line-height: 1.3; } input, @@ -71,13 +71,13 @@ textarea, select, option, button { - font-family: 'Fredoka', sans-serif; - font-size: 1rem; + font-family: 'Fredoka', sans-serif; + font-size: 1rem; } button { - cursor: pointer; - border: 0; - outline: 0; - background: transparent; + cursor: pointer; + border: 0; + outline: 0; + background: transparent; } diff --git a/frontend/hooks/useSocket.ts b/frontend/hooks/useSocket.ts index 531b6edf..8d17fab0 100644 --- a/frontend/hooks/useSocket.ts +++ b/frontend/hooks/useSocket.ts @@ -7,35 +7,35 @@ import { addStatus, deleteStatus, patchStatus, setAllStatus } from '/frontend/st import { socketEvent } from '/types/cimonitor'; type UseSocketOutput = { - socketConnected: boolean; + socketConnected: boolean; }; const useSocket = (): UseSocketOutput => { - const [socketConnected, setSocketConnected] = useState(false); - const dispatch = useDispatch(); - - useEffect(() => { - const socket = io(); - - socket.on(socketEvent.connect, () => setSocketConnected(true)); - socket.on(socketEvent.disconnect, () => setSocketConnected(false)); - socket.on(socketEvent.allStatuses, (statuses) => dispatch(setAllStatus(statuses))); - socket.on(socketEvent.patchStatus, (status) => dispatch(patchStatus(status))); - socket.on(socketEvent.newStatus, (status) => dispatch(addStatus(status))); - socket.on(socketEvent.deleteStatus, (statusId) => dispatch(deleteStatus(statusId))); - - // Refresh all statuses once a day - const requestStatusesInterval = setInterval(() => socket.emit(socketEvent.requestAllStatuses), 60000 * 60 * 24); - - return () => { - socket.disconnect(); - clearInterval(requestStatusesInterval); - }; - }, []); - - return { - socketConnected, - }; + const [socketConnected, setSocketConnected] = useState(false); + const dispatch = useDispatch(); + + useEffect(() => { + const socket = io(); + + socket.on(socketEvent.connect, () => setSocketConnected(true)); + socket.on(socketEvent.disconnect, () => setSocketConnected(false)); + socket.on(socketEvent.allStatuses, (statuses) => dispatch(setAllStatus(statuses))); + socket.on(socketEvent.patchStatus, (status) => dispatch(patchStatus(status))); + socket.on(socketEvent.newStatus, (status) => dispatch(addStatus(status))); + socket.on(socketEvent.deleteStatus, (statusId) => dispatch(deleteStatus(statusId))); + + // Refresh all statuses once a day + const requestStatusesInterval = setInterval(() => socket.emit(socketEvent.requestAllStatuses), 60000 * 60 * 24); + + return () => { + socket.disconnect(); + clearInterval(requestStatusesInterval); + }; + }, []); + + return { + socketConnected, + }; }; export default useSocket; diff --git a/frontend/index.html b/frontend/index.html index 8ea2e5e0..9fc9745e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,25 +1,25 @@ - - - CIMonitor - - - - - - - - - - - -
- + + + CIMonitor + + + + + + + + + + + +
+ diff --git a/frontend/store/cache/actions.ts b/frontend/store/cache/actions.ts index 83d798f9..ba992f28 100644 --- a/frontend/store/cache/actions.ts +++ b/frontend/store/cache/actions.ts @@ -3,11 +3,11 @@ import { SetContributorsAction, SetVersionAction } from './types'; import { Contributor, Version } from '/types/cimonitor'; export const setContributors = (contributors: Contributor[]): SetContributorsAction => ({ - type: 'cache-contributors-set', - contributors, + type: 'cache-contributors-set', + contributors, }); export const setVersion = (version: Version): SetVersionAction => ({ - type: 'cache-version-set', - version, + type: 'cache-version-set', + version, }); diff --git a/frontend/store/cache/fetch.ts b/frontend/store/cache/fetch.ts index 2c447695..68471ce3 100644 --- a/frontend/store/cache/fetch.ts +++ b/frontend/store/cache/fetch.ts @@ -7,43 +7,43 @@ import { setContributors, setVersion } from './actions'; import { Contributor, Version } from '/types/cimonitor'; export const fetchVersion = async () => { - const { cache }: RootState = store.getState(); + const { cache }: RootState = store.getState(); - if (cache.lastVersionCheck > new Date().getTime() - 60000 * 5) { - console.log(`[store/cache/fetch] Serving latest version from cache.`); - return cache.version; - } + if (cache.lastVersionCheck > new Date().getTime() - 60000 * 5) { + console.log(`[store/cache/fetch] Serving latest version from cache.`); + return cache.version; + } - const response = await axios.get('/version'); + const response = await axios.get('/version'); - const version: Version = response.data; + const version: Version = response.data; - store.dispatch(setVersion(version)); + store.dispatch(setVersion(version)); - console.log(`[store/cache/fetch] Fetched latest version ${version.latest || version.server} from the backend.`); + console.log(`[store/cache/fetch] Fetched latest version ${version.latest || version.server} from the backend.`); - return version; + return version; }; export const fetchContributors = async (): Promise => { - const { cache }: RootState = store.getState(); + const { cache }: RootState = store.getState(); - if (cache.lastContributorCheck > new Date().getTime() - 60000 * 5) { - console.log(`[store/cache/fetch] Serving ${cache.contributors.length} contributors from cache.`); - return cache.contributors; - } + if (cache.lastContributorCheck > new Date().getTime() - 60000 * 5) { + console.log(`[store/cache/fetch] Serving ${cache.contributors.length} contributors from cache.`); + return cache.contributors; + } - try { - const response = await axios.get('/contributors'); + try { + const response = await axios.get('/contributors'); - const contributors: Contributor[] = response.data; + const contributors: Contributor[] = response.data; - store.dispatch(setContributors(contributors)); + store.dispatch(setContributors(contributors)); - console.log(`[store/cache/fetch] Fetched ${contributors.length} contributors from the backend.`); + console.log(`[store/cache/fetch] Fetched ${contributors.length} contributors from the backend.`); - return contributors; - } catch (error) { - return []; - } + return contributors; + } catch (error) { + return []; + } }; diff --git a/frontend/store/cache/reducer.ts b/frontend/store/cache/reducer.ts index ad51a1fb..574aa11e 100644 --- a/frontend/store/cache/reducer.ts +++ b/frontend/store/cache/reducer.ts @@ -1,29 +1,29 @@ import { ActionTypes, StateType } from './types'; const defaultState: StateType = { - version: null, - lastVersionCheck: 0, - contributors: [], - lastContributorCheck: 0, + version: null, + lastVersionCheck: 0, + contributors: [], + lastContributorCheck: 0, }; const reducer = (state = defaultState, action: ActionTypes): StateType => { - switch (action.type) { - case 'cache-contributors-set': - return { - ...state, - contributors: action.contributors, - lastContributorCheck: new Date().getTime(), - }; - case 'cache-version-set': - return { - ...state, - version: action.version, - lastVersionCheck: new Date().getTime(), - }; - default: - return state; - } + switch (action.type) { + case 'cache-contributors-set': + return { + ...state, + contributors: action.contributors, + lastContributorCheck: new Date().getTime(), + }; + case 'cache-version-set': + return { + ...state, + version: action.version, + lastVersionCheck: new Date().getTime(), + }; + default: + return state; + } }; export default reducer; diff --git a/frontend/store/cache/types.ts b/frontend/store/cache/types.ts index ea728d81..4dbadeb0 100644 --- a/frontend/store/cache/types.ts +++ b/frontend/store/cache/types.ts @@ -1,20 +1,20 @@ import { Contributor, Version } from '/types/cimonitor'; export type StateType = { - version: Version | null; - lastVersionCheck: number; - contributors: Contributor[]; - lastContributorCheck: number; + version: Version | null; + lastVersionCheck: number; + contributors: Contributor[]; + lastContributorCheck: number; }; export type SetVersionAction = { - type: 'cache-version-set'; - version: Version; + type: 'cache-version-set'; + version: Version; }; export type SetContributorsAction = { - type: 'cache-contributors-set'; - contributors: Contributor[]; + type: 'cache-contributors-set'; + contributors: Contributor[]; }; export type ActionTypes = SetVersionAction | SetContributorsAction; diff --git a/frontend/store/settings/actions.ts b/frontend/store/settings/actions.ts index 52f908cc..bc42cf15 100644 --- a/frontend/store/settings/actions.ts +++ b/frontend/store/settings/actions.ts @@ -1,28 +1,28 @@ import { - CloseSettingsPanelAction, - SetSizeModifierAction, - ToggleSettingsPanelAction, - ToggleShowCompletedAction, - ToggleShowUserAvatarsAction, + CloseSettingsPanelAction, + SetSizeModifierAction, + ToggleSettingsPanelAction, + ToggleShowCompletedAction, + ToggleShowUserAvatarsAction, } from './types'; export const toggleShowCompleted = (): ToggleShowCompletedAction => ({ - type: 'settings-show-completed-toggle', + type: 'settings-show-completed-toggle', }); export const toggleShowUserAvatars = (): ToggleShowUserAvatarsAction => ({ - type: 'settings-show-user-avatars-toggle', + type: 'settings-show-user-avatars-toggle', }); export const toggleSettingsPanel = (): ToggleSettingsPanelAction => ({ - type: 'settings-panel-toggle', + type: 'settings-panel-toggle', }); export const closeSettingsPanel = (): CloseSettingsPanelAction => ({ - type: 'settings-panel-close', + type: 'settings-panel-close', }); export const setSizeModifier = (sizeModifier: number): SetSizeModifierAction => ({ - type: 'settings-size-modifier-set', - sizeModifier, + type: 'settings-size-modifier-set', + sizeModifier, }); diff --git a/frontend/store/settings/reducer.ts b/frontend/store/settings/reducer.ts index 39e2c701..7ad99756 100644 --- a/frontend/store/settings/reducer.ts +++ b/frontend/store/settings/reducer.ts @@ -1,42 +1,42 @@ import { ActionTypes, StateType } from './types'; const defaultState: StateType = { - open: false, - showCompleted: false, - sizeModifier: 1, - showUserAvatars: true, + open: false, + showCompleted: false, + sizeModifier: 1, + showUserAvatars: true, }; const reducer = (state = defaultState, action: ActionTypes): StateType => { - switch (action.type) { - case 'settings-panel-toggle': - return { - ...state, - open: !state.open, - }; - case 'settings-panel-close': - return { - ...state, - open: false, - }; - case 'settings-show-completed-toggle': - return { - ...state, - showCompleted: !state.showCompleted, - }; - case 'settings-size-modifier-set': - return { - ...state, - sizeModifier: action.sizeModifier, - }; - case 'settings-show-user-avatars-toggle': - return { - ...state, - showUserAvatars: !state.showUserAvatars, - }; - default: - return state; - } + switch (action.type) { + case 'settings-panel-toggle': + return { + ...state, + open: !state.open, + }; + case 'settings-panel-close': + return { + ...state, + open: false, + }; + case 'settings-show-completed-toggle': + return { + ...state, + showCompleted: !state.showCompleted, + }; + case 'settings-size-modifier-set': + return { + ...state, + sizeModifier: action.sizeModifier, + }; + case 'settings-show-user-avatars-toggle': + return { + ...state, + showUserAvatars: !state.showUserAvatars, + }; + default: + return state; + } }; export default reducer; diff --git a/frontend/store/settings/selectors.ts b/frontend/store/settings/selectors.ts index 98d15ee2..f2e167db 100644 --- a/frontend/store/settings/selectors.ts +++ b/frontend/store/settings/selectors.ts @@ -7,4 +7,4 @@ export const isShowingCompleted = (state: RootState): boolean => state.setting.s export const isShowingUserAvatars = (state: RootState): boolean => state.setting.showUserAvatars; export const getSizeModifier = (state: RootState): number => - state.setting.sizeModifier ? state.setting.sizeModifier : 1; + state.setting.sizeModifier ? state.setting.sizeModifier : 1; diff --git a/frontend/store/settings/types.ts b/frontend/store/settings/types.ts index 5dceffe2..a16ea9bc 100644 --- a/frontend/store/settings/types.ts +++ b/frontend/store/settings/types.ts @@ -1,34 +1,34 @@ export type StateType = { - open: boolean; - showCompleted: boolean; - sizeModifier: number; - showUserAvatars: boolean; + open: boolean; + showCompleted: boolean; + sizeModifier: number; + showUserAvatars: boolean; }; export type SetSizeModifierAction = { - type: 'settings-size-modifier-set'; - sizeModifier: number; + type: 'settings-size-modifier-set'; + sizeModifier: number; }; export type ToggleShowCompletedAction = { - type: 'settings-show-completed-toggle'; + type: 'settings-show-completed-toggle'; }; export type ToggleSettingsPanelAction = { - type: 'settings-panel-toggle'; + type: 'settings-panel-toggle'; }; export type CloseSettingsPanelAction = { - type: 'settings-panel-close'; + type: 'settings-panel-close'; }; export type ToggleShowUserAvatarsAction = { - type: 'settings-show-user-avatars-toggle'; + type: 'settings-show-user-avatars-toggle'; }; export type ActionTypes = - | ToggleSettingsPanelAction - | CloseSettingsPanelAction - | ToggleShowCompletedAction - | SetSizeModifierAction - | ToggleShowUserAvatarsAction; + | ToggleSettingsPanelAction + | CloseSettingsPanelAction + | ToggleShowCompletedAction + | SetSizeModifierAction + | ToggleShowUserAvatarsAction; diff --git a/frontend/store/status/actions.ts b/frontend/store/status/actions.ts index 7fcf03a7..20b1536d 100644 --- a/frontend/store/status/actions.ts +++ b/frontend/store/status/actions.ts @@ -1,28 +1,28 @@ import { - AddStatusAction, - DeleteStatusAction, - PatchStatusAction, - SetAllStatusAction, + AddStatusAction, + DeleteStatusAction, + PatchStatusAction, + SetAllStatusAction, } from '/frontend/store/status/types'; import Status from '/types/status'; export const setAllStatus = (statuses: Status[]): SetAllStatusAction => ({ - type: 'status-set-all', - statuses, + type: 'status-set-all', + statuses, }); export const addStatus = (status: Status): AddStatusAction => ({ - type: 'status-add', - status, + type: 'status-add', + status, }); export const patchStatus = (status: Status): PatchStatusAction => ({ - type: 'status-patch', - status, + type: 'status-patch', + status, }); export const deleteStatus = (statusId: string): DeleteStatusAction => ({ - type: 'status-delete', - statusId, + type: 'status-delete', + statusId, }); diff --git a/frontend/store/status/reducer.ts b/frontend/store/status/reducer.ts index ea0f36a1..32b1c939 100644 --- a/frontend/store/status/reducer.ts +++ b/frontend/store/status/reducer.ts @@ -3,49 +3,49 @@ import { ActionTypes, StateType } from './types'; import Status from '/types/status'; export const defaultState: StateType = { - received: false, - statuses: [], + received: false, + statuses: [], }; const byTime = (StatusA: Status, StatusB: Status): number => - new Date(StatusB.time).getTime() - new Date(StatusA.time).getTime(); + new Date(StatusB.time).getTime() - new Date(StatusA.time).getTime(); const reducer = (state = defaultState, action: ActionTypes): StateType => { - switch (action.type) { - case 'status-set-all': - return { - ...state, - received: true, - statuses: action.statuses.sort(byTime), - }; - case 'status-add': - return { - ...state, - received: true, - statuses: [...state.statuses, action.status].sort(byTime), - }; - case 'status-patch': - return { - ...state, - received: true, - statuses: state.statuses - .map((status) => { - if (status.id === action.status.id) { - return action.status; - } + switch (action.type) { + case 'status-set-all': + return { + ...state, + received: true, + statuses: action.statuses.sort(byTime), + }; + case 'status-add': + return { + ...state, + received: true, + statuses: [...state.statuses, action.status].sort(byTime), + }; + case 'status-patch': + return { + ...state, + received: true, + statuses: state.statuses + .map((status) => { + if (status.id === action.status.id) { + return action.status; + } - return status; - }) - .sort(byTime), - }; - case 'status-delete': - return { - ...state, - statuses: state.statuses.filter((status) => status.id !== action.statusId), - }; - default: - return state; - } + return status; + }) + .sort(byTime), + }; + case 'status-delete': + return { + ...state, + statuses: state.statuses.filter((status) => status.id !== action.statusId), + }; + default: + return state; + } }; export default reducer; diff --git a/frontend/store/status/selectors.ts b/frontend/store/status/selectors.ts index 89fe9402..850b96d0 100644 --- a/frontend/store/status/selectors.ts +++ b/frontend/store/status/selectors.ts @@ -5,15 +5,15 @@ import Status, { State } from '/types/status'; export const getStatuses = (state: RootState): Status[] => state.status.statuses; export const getGlobalState = (state: RootState): State => { - if (state.status.statuses.find((status) => status.state === 'error')) { - return 'error'; - } + if (state.status.statuses.find((status) => status.state === 'error')) { + return 'error'; + } - if (state.status.statuses.find((status) => status.state === 'warning')) { - return 'warning'; - } + if (state.status.statuses.find((status) => status.state === 'warning')) { + return 'warning'; + } - return 'success'; + return 'success'; }; export const hasNoStatuses = (state: RootState): boolean => state.status.received && state.status.statuses.length === 0; diff --git a/frontend/store/status/types.ts b/frontend/store/status/types.ts index 111666ad..3c0e8642 100644 --- a/frontend/store/status/types.ts +++ b/frontend/store/status/types.ts @@ -1,28 +1,28 @@ import Status from '/types/status'; export type StateType = { - received: boolean; - statuses: Status[]; + received: boolean; + statuses: Status[]; }; export type SetAllStatusAction = { - type: 'status-set-all'; - statuses: Status[]; + type: 'status-set-all'; + statuses: Status[]; }; export type AddStatusAction = { - type: 'status-add'; - status: Status; + type: 'status-add'; + status: Status; }; export type PatchStatusAction = { - type: 'status-patch'; - status: Status; + type: 'status-patch'; + status: Status; }; export type DeleteStatusAction = { - type: 'status-delete'; - statusId: string; + type: 'status-delete'; + statusId: string; }; export type ActionTypes = SetAllStatusAction | AddStatusAction | PatchStatusAction | DeleteStatusAction; diff --git a/frontend/store/store.ts b/frontend/store/store.ts index 1ea9728a..107b3ecf 100644 --- a/frontend/store/store.ts +++ b/frontend/store/store.ts @@ -5,62 +5,62 @@ import SettingReducer from './settings/reducer'; import StatusReducer, { defaultState as defaultStatusState } from './status/reducer'; const reducers = combineReducers({ - status: StatusReducer, - setting: SettingReducer, - cache: CacheReducer, + status: StatusReducer, + setting: SettingReducer, + cache: CacheReducer, }); export type RootState = ReturnType; // Declare dev tools redux enhancer type declare global { - // eslint-disable-next-line @typescript-eslint/consistent-type-definitions,no-unused-vars - interface Window { - __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose; - } + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions,no-unused-vars + interface Window { + __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose; + } } // Determine Redux enhancers let composeEnhancers = compose; if ( - typeof window !== 'undefined' && - process.env.NODE_ENV !== 'production' && - window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ + typeof window !== 'undefined' && + process.env.NODE_ENV !== 'production' && + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'CIMonitor' }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'CIMonitor' }); } // Load current store state const stateKey = 'CIMonitor-state'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const getStoreState = (): any => { - const state = window.localStorage.getItem(stateKey); + const state = window.localStorage.getItem(stateKey); - if (state) { - return JSON.parse(state); - } + if (state) { + return JSON.parse(state); + } - return {}; + return {}; }; // Create Redux store with using saved state from local storage if present const store = createStore(reducers, getStoreState(), composeEnhancers()); const saveScopedState = (state: RootState): RootState => { - return { - ...state, - status: defaultStatusState, - setting: { - ...state.setting, - open: false, - }, - }; + return { + ...state, + status: defaultStatusState, + setting: { + ...state.setting, + open: false, + }, + }; }; store.subscribe((): void => { - window.localStorage.setItem(stateKey, JSON.stringify(saveScopedState(store.getState()))); + window.localStorage.setItem(stateKey, JSON.stringify(saveScopedState(store.getState()))); }); export default store; diff --git a/frontend/style/colors.ts b/frontend/style/colors.ts index da095ac8..6777beeb 100644 --- a/frontend/style/colors.ts +++ b/frontend/style/colors.ts @@ -1,29 +1,29 @@ export const stateColor = { - error: '#e57064', - warning: '#f8c147', - info: '#82c0fc', - success: '#8ac159', + error: '#e57064', + warning: '#f8c147', + info: '#82c0fc', + success: '#8ac159', }; export const stateDarkColor = { - error: '#d25245', - warning: '#d9a32c', - info: '#60a9ef', - success: '#69a633', + error: '#d25245', + warning: '#d9a32c', + info: '#60a9ef', + success: '#69a633', }; export const stateLightColor = { - error: '#fae1de', - warning: '#fdf6e5', - info: '#e4f1fd', - success: '#eefce2', + error: '#fae1de', + warning: '#fdf6e5', + info: '#e4f1fd', + success: '#eefce2', }; export const textColor = '#333'; export const textMutedColor = '#999'; export const opacity = (color: string, opacity: number) => { - const opacityHex = Math.round(opacity * 255).toString(16); + const opacityHex = Math.round(opacity * 255).toString(16); - return `${color}${opacityHex}`; + return `${color}${opacityHex}`; }; diff --git a/frontend/style/size.ts b/frontend/style/size.ts index 3f21fbb5..061a5c25 100644 --- a/frontend/style/size.ts +++ b/frontend/style/size.ts @@ -1,17 +1,17 @@ import { css } from 'styled-components'; type BreakPoints = { - small: number; - medium: number; - large: number; - huge: number; + small: number; + medium: number; + large: number; + huge: number; }; export const breakpoints: BreakPoints = { - small: 40, - medium: 60, - large: 80, - huge: 100, + small: 40, + medium: 60, + large: 80, + huge: 100, }; type BreakpointKeys = keyof BreakPoints; @@ -19,12 +19,12 @@ type BreakpointKeys = keyof BreakPoints; type Media = { [key in BreakpointKeys]: (cssString: ReturnType) => string }; export const fromSize: Media = Object.entries(breakpoints).reduce((acc, [label, size]: [string, number]) => { - return { - ...acc, - [label]: (content: string) => css` - @media (min-width: ${size}rem) { - ${content} - } - `, - }; + return { + ...acc, + [label]: (content: string) => css` + @media (min-width: ${size}rem) { + ${content} + } + `, + }; }, {}) as Media; diff --git a/frontend/style/text.ts b/frontend/style/text.ts index c1c0f921..42462938 100644 --- a/frontend/style/text.ts +++ b/frontend/style/text.ts @@ -1,13 +1,13 @@ import { css } from 'styled-components'; export const ellipsis = css` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; `; export const ellipsisLeft = css` - ${ellipsis}; - direction: rtl; - text-align: left; + ${ellipsis}; + direction: rtl; + text-align: left; `; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index d28838f0..4f679287 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,16 +1,16 @@ { - "compilerOptions": { - "target": "esnext", - "module": "CommonJS", - "moduleResolution": "node", - "baseUrl": "../", - "esModuleInterop": true, - "preserveWatchOutput": true, - "jsx": "preserve", - "allowJs": false, - "paths": { - "/*": ["./*"] - } - }, - "include": ["../types/**/*.ts", "../frontend/**/*.ts", "../frontend/**/*.tsx"] + "compilerOptions": { + "target": "esnext", + "module": "CommonJS", + "moduleResolution": "node", + "baseUrl": "../", + "esModuleInterop": true, + "preserveWatchOutput": true, + "jsx": "preserve", + "allowJs": false, + "paths": { + "/*": ["./*"] + } + }, + "include": ["../types/**/*.ts", "../frontend/**/*.ts", "../frontend/**/*.tsx"] } diff --git a/lint-staged.config.js b/lint-staged.config.js index 9e440202..a63fee31 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -1,25 +1,25 @@ function hasChangedFileInFolder(changedFiles, folder) { - return !!changedFiles.find((fileName) => fileName.includes('/' + folder + '/')); + return !!changedFiles.find((fileName) => fileName.includes('/' + folder + '/')); } module.exports = { - '*.{ts,tsx}': [ - 'eslint --fix', - (changedFiles) => { - var linters = []; - var typescriptChecks = ['frontend', 'backend']; + '*.{ts,tsx}': [ + 'eslint --fix', + (changedFiles) => { + var linters = []; + var typescriptChecks = ['frontend', 'backend']; - var haveTypesChanged = hasChangedFileInFolder(changedFiles, 'types'); + var haveTypesChanged = hasChangedFileInFolder(changedFiles, 'types'); - typescriptChecks.forEach((folder) => { - if (haveTypesChanged || hasChangedFileInFolder(changedFiles, folder)) { - linters.push('tsc --noEmit --project ./' + folder + '/tsconfig.json'); - } - }); + typescriptChecks.forEach((folder) => { + if (haveTypesChanged || hasChangedFileInFolder(changedFiles, folder)) { + linters.push('tsc --noEmit --project ./' + folder + '/tsconfig.json'); + } + }); - return linters; - }, - ], - '*.{json,css,html,js}': ['prettier --write'], - '*.md': ['prettier --write --tab-width 2'], + return linters; + }, + ], + '*.{json,css,html,js}': ['prettier --write'], + '*.md': ['prettier --write --tab-width 2'], }; diff --git a/package.json b/package.json index 0690ec46..5a6adeba 100644 --- a/package.json +++ b/package.json @@ -1,74 +1,74 @@ { - "name": "cimonitor", - "version": "4.6.1", - "description": "Monitors all your projects CI automatically", - "repository": "git@github.com:FuturePortal/CIMonitor.git", - "license": "MIT", - "author": { - "name": "Rick van der Staaij", - "url": "https://rick.nu" - }, - "contributors": [ - { - "name": "GitHub contributors", - "url": "https://github.com/FuturePortal/CIMonitor/graphs/contributors" - } - ], - "scripts": { - "start": "node app/backend/server.js", - "start-client": "node app/backend/module-client.js", - "build": "tsc --project ./backend/tsconfig.json && parcel build --dist-dir dashboard --no-source-maps frontend/index.html", - "server": "nodemon --watch backend --exec ts-node backend/server.ts", - "module-client": "nodemon --watch backend --exec ts-node backend/module-client.ts", - "dashboard": "parcel watch --dist-dir dashboard --hmr-port 3031 frontend/index.html", - "cypress-run": "cypress run", - "cypress-open": "cypress open", - "postinstall": "husky install" - }, - "dependencies": { - "axios": "^0.26.1", - "body-parser": "^1.19.2", - "dotenv": "^16.0.0", - "express": "^4.17.3", - "firebase-admin": "^10.0.2", - "socket.io": "^4.4.1", - "socket.io-client": "^4.4.1" - }, - "devDependencies": { - "@futureportal/parcel-transformer-package-version": "^1.0.0", - "@parcel/packager-raw-url": "2.3.2", - "@parcel/transformer-svg-react": "^2.3.2", - "@parcel/transformer-typescript-tsc": "^2.3.2", - "@parcel/transformer-webmanifest": "2.3.2", - "@types/express": "^4.17.13", - "@types/node": "^16", - "@types/react": "^17.0.39", - "@types/react-dom": "^17.0.11", - "@types/react-redux": "^7.1.23", - "@types/socket.io": "^3.0.2", - "@types/socket.io-client": "^3.0.0", - "@types/styled-components": "^5.1.24", - "@typescript-eslint/eslint-plugin": "^5.13.0", - "@typescript-eslint/parser": "^5.13.0", - "buffer": "^6.0.3", - "cypress": "^9.5.1", - "eslint": "^7.32.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.3", - "eslint-plugin-simple-import-sort": "^7.0.0", - "husky": "^7.0.0", - "lint-staged": "^12.3.4", - "nodemon": "^2.0.15", - "parcel": "^2.3.2", - "prettier": "^2.5.1", - "process": "^0.11.10", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-redux": "^7.2.6", - "styled-components": "^5.3.3", - "ts-node": "^10.7.0", - "typescript": "^4.5.5" - } + "name": "cimonitor", + "version": "4.6.1", + "description": "Monitors all your projects CI automatically", + "repository": "git@github.com:FuturePortal/CIMonitor.git", + "license": "MIT", + "author": { + "name": "Rick van der Staaij", + "url": "https://rick.nu" + }, + "contributors": [ + { + "name": "GitHub contributors", + "url": "https://github.com/FuturePortal/CIMonitor/graphs/contributors" + } + ], + "scripts": { + "start": "node app/backend/server.js", + "start-client": "node app/backend/module-client.js", + "build": "tsc --project ./backend/tsconfig.json && parcel build --dist-dir dashboard --no-source-maps frontend/index.html", + "server": "nodemon --watch backend --exec ts-node backend/server.ts", + "module-client": "nodemon --watch backend --exec ts-node backend/module-client.ts", + "dashboard": "parcel watch --dist-dir dashboard --hmr-port 3031 frontend/index.html", + "cypress-run": "cypress run", + "cypress-open": "cypress open", + "postinstall": "husky install" + }, + "dependencies": { + "axios": "^0.26.1", + "body-parser": "^1.19.2", + "dotenv": "^16.0.0", + "express": "^4.17.3", + "firebase-admin": "^10.0.2", + "socket.io": "^4.4.1", + "socket.io-client": "^4.4.1" + }, + "devDependencies": { + "@futureportal/parcel-transformer-package-version": "^1.0.0", + "@parcel/packager-raw-url": "2.3.2", + "@parcel/transformer-svg-react": "^2.3.2", + "@parcel/transformer-typescript-tsc": "^2.3.2", + "@parcel/transformer-webmanifest": "2.3.2", + "@types/express": "^4.17.13", + "@types/node": "^16", + "@types/react": "^17.0.39", + "@types/react-dom": "^17.0.11", + "@types/react-redux": "^7.1.23", + "@types/socket.io": "^3.0.2", + "@types/socket.io-client": "^3.0.0", + "@types/styled-components": "^5.1.24", + "@typescript-eslint/eslint-plugin": "^5.13.0", + "@typescript-eslint/parser": "^5.13.0", + "buffer": "^6.0.3", + "cypress": "^9.5.1", + "eslint": "^7.32.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-react": "^7.29.3", + "eslint-plugin-simple-import-sort": "^7.0.0", + "husky": "^7.0.0", + "lint-staged": "^12.3.4", + "nodemon": "^2.0.15", + "parcel": "^2.3.2", + "prettier": "^2.5.1", + "process": "^0.11.10", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-redux": "^7.2.6", + "styled-components": "^5.3.3", + "ts-node": "^10.7.0", + "typescript": "^4.5.5" + } } diff --git a/types/cimonitor.ts b/types/cimonitor.ts index 058bcdc1..d7a6bc87 100644 --- a/types/cimonitor.ts +++ b/types/cimonitor.ts @@ -1,28 +1,28 @@ export type Contributor = { - commits: number; - username: string; - profile: string; - image: string; - site?: string | null; - location?: string | null; - name?: string | null; - company?: string | null; + commits: number; + username: string; + profile: string; + image: string; + site?: string | null; + location?: string | null; + name?: string | null; + company?: string | null; }; export type Version = { - server: string; - latest: string | null; + server: string; + latest: string | null; }; export const socketEvent = { - connect: 'connect', - disconnect: 'disconnect', - allStatuses: 'status-all', - newStatus: 'status-new', - patchStatus: 'status-patch', - deleteStatus: 'status-delete', - statusStateChange: 'status-state-change', - requestAllStatuses: 'status-request', + connect: 'connect', + disconnect: 'disconnect', + allStatuses: 'status-all', + newStatus: 'status-new', + patchStatus: 'status-patch', + deleteStatus: 'status-delete', + statusStateChange: 'status-state-change', + requestAllStatuses: 'status-request', }; export type ServerSettings = {}; diff --git a/types/github.ts b/types/github.ts index 63a2e7ae..daad2ae0 100644 --- a/types/github.ts +++ b/types/github.ts @@ -3,124 +3,124 @@ export type GitHubStatus = 'queued' | 'completed' | 'in_progress'; export type GitHubConclusion = null | 'success' | 'failure' | 'skipped'; type GitHubRepository = { - id: number; - name: string; - full_name: string; - private: boolean; - html_url: string; - owner: { - name: string; - id: number; - avatar_url: string; - }; + id: number; + name: string; + full_name: string; + private: boolean; + html_url: string; + owner: { + name: string; + id: number; + avatar_url: string; + }; }; export type GitHubUser = { - login: string; - avatar_url: string; - html_url: string; - name: string | null; - company: string | null; - blog: string | null; - location: string | null; + login: string; + avatar_url: string; + html_url: string; + name: string | null; + company: string | null; + blog: string | null; + location: string | null; }; export type GitHubRelease = { - tag_name: string; - name: string; - target_commitish: string; + tag_name: string; + name: string; + target_commitish: string; }; type GitHubSender = { - login: string; - avatar_url: string; - html_url: string; + login: string; + avatar_url: string; + html_url: string; }; type GitHubOrganization = { - login: string; - id: number; - avatar_url: string; + login: string; + id: number; + avatar_url: string; }; export type GitHubPush = { - ref: string; - repository: GitHubRepository; - pusher: { - name: string; - email: string; - }; - organization: GitHubOrganization; - sender: GitHubSender; + ref: string; + repository: GitHubRepository; + pusher: { + name: string; + email: string; + }; + organization: GitHubOrganization; + sender: GitHubSender; }; export type GitHubWorkflowRun = { - workflow_run: { - id: number; - name: string; - head_branch: string; - status: GitHubStatus; - conclusion: GitHubConclusion; - actor: { - login: string; - }; - head_commit: { - message: string; - }; - }; - workflow: { - name: string; - }; - repository: GitHubRepository; - organization: GitHubOrganization; - sender: GitHubSender; + workflow_run: { + id: number; + name: string; + head_branch: string; + status: GitHubStatus; + conclusion: GitHubConclusion; + actor: { + login: string; + }; + head_commit: { + message: string; + }; + }; + workflow: { + name: string; + }; + repository: GitHubRepository; + organization: GitHubOrganization; + sender: GitHubSender; }; export type GitHubPullRequest = { - action: 'opened'; - number: number; - pull_request: { - html_url: string; - title: string; - state: 'open'; - number: number; - user: GitHubUser; - head: { - label: string; - ref: string; - user: GitHubUser; - }; - }; - repository: GitHubRepository; - organization: GitHubOrganization; - sender: GitHubSender; + action: 'opened'; + number: number; + pull_request: { + html_url: string; + title: string; + state: 'open'; + number: number; + user: GitHubUser; + head: { + label: string; + ref: string; + user: GitHubUser; + }; + }; + repository: GitHubRepository; + organization: GitHubOrganization; + sender: GitHubSender; }; export type GitHubContributor = { - total: number; - author: { - login: string; - avatar_url: string; - html_url: string; - }; + total: number; + author: { + login: string; + avatar_url: string; + html_url: string; + }; }; export type GitHubStep = { - name: string; - status: GitHubStatus; - conclusion: GitHubConclusion; + name: string; + status: GitHubStatus; + conclusion: GitHubConclusion; }; export type GitHubWorkflowJob = { - workflow_job: { - id: number; - run_id: number; - name: string; - status: GitHubStatus; - conclusion: GitHubConclusion; - steps: GitHubStep[]; - }; - repository: GitHubRepository; - organization: GitHubOrganization; - sender: GitHubSender; + workflow_job: { + id: number; + run_id: number; + name: string; + status: GitHubStatus; + conclusion: GitHubConclusion; + steps: GitHubStep[]; + }; + repository: GitHubRepository; + organization: GitHubOrganization; + sender: GitHubSender; }; diff --git a/types/gitlab.ts b/types/gitlab.ts index ddbab8ad..e3443af8 100644 --- a/types/gitlab.ts +++ b/types/gitlab.ts @@ -1,153 +1,153 @@ export type GitLabUser = { - id: number; - name: string; - username: string; - avatar_url: string; - email: string; + id: number; + name: string; + username: string; + avatar_url: string; + email: string; }; export type GitLabEnvironment = { - name: string; - action: string; + name: string; + action: string; }; export type GitLabProject = { - id: number; - name: string; - description: string; - web_url: string; - avatar_url: string; - git_ssh_url: string; - git_http_url: string; - namespace: string; - visibility_level: number; - path_with_namespace: string; - default_branch: string; - ci_config_path: null; + id: number; + name: string; + description: string; + web_url: string; + avatar_url: string; + git_ssh_url: string; + git_http_url: string; + namespace: string; + visibility_level: number; + path_with_namespace: string; + default_branch: string; + ci_config_path: null; }; export type GitLabOther = { - object_kind: 'other'; + object_kind: 'other'; }; export type GitLabPipeline = { - object_kind: 'pipeline'; - user: GitLabUser; - object_attributes: { - id: number; - ref: string | false; - tag: string | false; - sha: string; - before_sha: string; - source: string; - status: string; - detailed_status: string; - stages: string[]; - created_at: string; - finished_at: string | null; - duration: number | null; - queued_duration: number | null; - }; - project: GitLabProject; - commit: { - id: string; - message: string; - title: string; - timestamp: string; - url: string; - author: { - name: string; - email: string; - }; - }; + object_kind: 'pipeline'; + user: GitLabUser; + object_attributes: { + id: number; + ref: string | false; + tag: string | false; + sha: string; + before_sha: string; + source: string; + status: string; + detailed_status: string; + stages: string[]; + created_at: string; + finished_at: string | null; + duration: number | null; + queued_duration: number | null; + }; + project: GitLabProject; + commit: { + id: string; + message: string; + title: string; + timestamp: string; + url: string; + author: { + name: string; + email: string; + }; + }; }; export type GitLabDeployment = { - object_kind: 'deployment'; - status: string; - status_changed_at: string; - deployment_id: number; - deployable_id: number; - deployable_url: string; - environment: string; - short_sha: string; - user_url: string; - commit_url: string; - commit_title: string; - ref: string; - user: GitLabUser; - project: GitLabProject; + object_kind: 'deployment'; + status: string; + status_changed_at: string; + deployment_id: number; + deployable_id: number; + deployable_url: string; + environment: string; + short_sha: string; + user_url: string; + commit_url: string; + commit_title: string; + ref: string; + user: GitLabUser; + project: GitLabProject; }; export type GitLabMergeRequest = { - object_kind: 'merge_request'; - user: GitLabUser; - project: GitLabProject; - object_attributes: { - description: string; - id: number; - iid: number; - source_branch: string; - source_project_id: number; - target_branch: string; - target_project_id: number; - title: string; - url: string; - source: GitLabProject; - target: GitLabProject; - last_commit: { - title: string; - }; - state: 'opened'; - action: 'open'; - }; - assignees: GitLabUser[]; + object_kind: 'merge_request'; + user: GitLabUser; + project: GitLabProject; + object_attributes: { + description: string; + id: number; + iid: number; + source_branch: string; + source_project_id: number; + target_branch: string; + target_project_id: number; + title: string; + url: string; + source: GitLabProject; + target: GitLabProject; + last_commit: { + title: string; + }; + state: 'opened'; + action: 'open'; + }; + assignees: GitLabUser[]; }; export type GitLabBuild = { - object_kind: 'build'; - ref: string | false; - tag: string | false; - sha: string; - before_sha: string; - build_id: number; - build_name: string; - build_stage: string; - build_status: string; - build_created_at: string; - build_started_at: string | null; - build_finished_at: string | null; - build_duration: number | null; - build_queued_duration: number | null; - build_allow_failure: boolean; - build_failure_reason: string; - pipeline_id: number; - runner: string | null; - project_id: number; - project_name: string; - user: GitLabUser; - commit: { - id: number; - sha: string; - message: string; - author_name: string; - author_email: string; - author_url: string; - status: string; - duration: string | null; - started_at: string | null; - finished_at: string | null; - }; - repository: { - name: string; - url: string; - description: string; - homepage: string; - git_http_url: string; - git_ssh_url: string; - visibility_level: number; - }; - environment: GitLabEnvironment | null; + object_kind: 'build'; + ref: string | false; + tag: string | false; + sha: string; + before_sha: string; + build_id: number; + build_name: string; + build_stage: string; + build_status: string; + build_created_at: string; + build_started_at: string | null; + build_finished_at: string | null; + build_duration: number | null; + build_queued_duration: number | null; + build_allow_failure: boolean; + build_failure_reason: string; + pipeline_id: number; + runner: string | null; + project_id: number; + project_name: string; + user: GitLabUser; + commit: { + id: number; + sha: string; + message: string; + author_name: string; + author_email: string; + author_url: string; + status: string; + duration: string | null; + started_at: string | null; + finished_at: string | null; + }; + repository: { + name: string; + url: string; + description: string; + homepage: string; + git_http_url: string; + git_ssh_url: string; + visibility_level: number; + }; + environment: GitLabEnvironment | null; }; export type GitLabWebhook = GitLabBuild | GitLabPipeline | GitLabDeployment | GitLabOther | GitLabMergeRequest; diff --git a/types/index.d.ts b/types/index.d.ts index c5f69d70..f5bd0159 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,11 +1,11 @@ declare module '*.svg' { - import React = require('react'); - export const ReactComponent: React.FunctionComponent>; - const src: string; - export default src; + import React = require('react'); + export const ReactComponent: React.FunctionComponent>; + const src: string; + export default src; } declare module '*.png' { - const content: string; - export default content; + const content: string; + export default content; } diff --git a/types/module.ts b/types/module.ts index f09d69f2..52c203ba 100644 --- a/types/module.ts +++ b/types/module.ts @@ -1,33 +1,33 @@ import { State } from 'types/status'; export type GpioModule = { - type: 'gpio'; - pin: number; - mode: 'on' | 'off' | 'on-for' | 'off-for'; - duration?: number; + type: 'gpio'; + pin: number; + mode: 'on' | 'off' | 'on-for' | 'off-for'; + duration?: number; }; export type HttpModule = { - type: 'http'; - url: string; + type: 'http'; + url: string; }; export type ModuleConfig = GpioModule | HttpModule; export type ModuleTrigger = { - event: string; - status: { - state: State; - [key: string]: string; - }; + event: string; + status: { + state: State; + [key: string]: string; + }; }; export type ModuleEvent = { - name: string; - modules: ModuleConfig[]; + name: string; + modules: ModuleConfig[]; }; export type ModuleSettings = { - triggers: ModuleTrigger[]; - events: ModuleEvent[]; + triggers: ModuleTrigger[]; + events: ModuleEvent[]; }; diff --git a/types/readthedocs.ts b/types/readthedocs.ts index b327431c..57500a94 100644 --- a/types/readthedocs.ts +++ b/types/readthedocs.ts @@ -1,13 +1,13 @@ export type ReadTheDocsBuild = { - event: string; - name: string; - slug: string; - version: string; - commit: string; - build: string; - start_date: string; - build_url: string; - docs_url: string; + event: string; + name: string; + slug: string; + version: string; + commit: string; + build: string; + start_date: string; + build_url: string; + docs_url: string; }; export default ReadTheDocsBuild; diff --git a/types/status.ts b/types/status.ts index 1f3f0e70..ca036e9a 100644 --- a/types/status.ts +++ b/types/status.ts @@ -1,58 +1,58 @@ export type State = 'info' | 'warning' | 'error' | 'success'; export type StepState = - | 'created' - | 'pending' - | 'running' - | 'success' - | 'failed' - | 'soft-failed' - | 'skipped' - | 'timeout'; + | 'created' + | 'pending' + | 'running' + | 'success' + | 'failed' + | 'soft-failed' + | 'skipped' + | 'timeout'; export type Step = { - id: string; - title: string; - state: StepState; - time: string; - duration?: number; + id: string; + title: string; + state: StepState; + time: string; + duration?: number; }; export type Stage = { - id: string; - title?: string; - state: StepState; - steps: Step[]; - time: string; + id: string; + title?: string; + state: StepState; + steps: Step[]; + time: string; }; export type Process = { - id: number; - title: string; - state: State; - stages: Stage[]; - time: string; - duration?: number; + id: number; + title: string; + state: State; + stages: Stage[]; + time: string; + duration?: number; }; export type Status = { - id: string; - project: string; - state: State; - processes: Process[]; - time: string; - source: 'github' | 'gitlab' | 'readthedocs'; - sourceUrl?: string; - url?: string; - branch?: string; - tag?: string; - issue?: number; - projectImage?: string; - username?: string; - userImage?: string; - userUrl?: string; - mergeTitle?: string; - mergeUrl?: string; + id: string; + project: string; + state: State; + processes: Process[]; + time: string; + source: 'github' | 'gitlab' | 'readthedocs'; + sourceUrl?: string; + url?: string; + branch?: string; + tag?: string; + issue?: number; + projectImage?: string; + username?: string; + userImage?: string; + userUrl?: string; + mergeTitle?: string; + mergeUrl?: string; }; export default Status;