gitlab-radiator 5.2.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/package.json +6 -6
  2. package/public/client.js +2 -0
  3. package/public/client.js.LICENSE.txt +673 -0
  4. package/src/app.js +77 -0
  5. package/src/auth.js +18 -0
  6. package/src/common/browser-error.js +1 -0
  7. package/src/config.js +82 -0
  8. package/src/dev-assets.js +11 -0
  9. package/src/gitlab/client.js +20 -0
  10. package/src/gitlab/index.js +50 -0
  11. package/src/gitlab/pipelines.js +125 -0
  12. package/src/gitlab/projects.js +65 -0
  13. package/src/gitlab/runners.js +23 -0
  14. package/src/less.js +28 -0
  15. package/src/tsconfig.build.tsbuildinfo +1 -0
  16. package/.github/copilot-instructions.md +0 -55
  17. package/.github/workflows/test.yml +0 -22
  18. package/.nvmrc +0 -1
  19. package/build-npm +0 -22
  20. package/eslint.config.mjs +0 -31
  21. package/screenshot.png +0 -0
  22. package/src/app.ts +0 -85
  23. package/src/auth.ts +0 -20
  24. package/src/client/arguments.ts +0 -61
  25. package/src/client/groupedProjects.tsx +0 -18
  26. package/src/client/groups.tsx +0 -50
  27. package/src/client/index.tsx +0 -79
  28. package/src/client/info.tsx +0 -16
  29. package/src/client/jobs.tsx +0 -57
  30. package/src/client/projects.tsx +0 -82
  31. package/src/client/stages.tsx +0 -18
  32. package/src/client/timestamp.tsx +0 -37
  33. package/src/client/tsconfig.json +0 -24
  34. package/src/common/gitlab-types.d.ts +0 -58
  35. package/src/config.ts +0 -95
  36. package/src/dev-assets.ts +0 -15
  37. package/src/gitlab/client.ts +0 -34
  38. package/src/gitlab/index.ts +0 -60
  39. package/src/gitlab/pipelines.ts +0 -178
  40. package/src/gitlab/projects.ts +0 -87
  41. package/src/gitlab/runners.ts +0 -35
  42. package/src/less.ts +0 -33
  43. package/test/gitlab-integration.ts +0 -457
  44. package/tsconfig.build.json +0 -14
  45. package/tsconfig.json +0 -25
  46. package/webpack.common.cjs +0 -25
  47. package/webpack.dev.cjs +0 -13
  48. package/webpack.prod.cjs +0 -9
package/src/app.ts DELETED
@@ -1,85 +0,0 @@
1
- import {basicAuth} from './auth.ts'
2
- import compression from 'compression'
3
- import {config} from './config.ts'
4
- import express from 'express'
5
- import fs from 'fs'
6
- import {fetchOfflineRunners} from './gitlab/runners.ts'
7
- import http from 'http'
8
- import {serveLessAsCss} from './less.ts'
9
- import {Server} from 'socket.io'
10
- import {update} from './gitlab/index.ts'
11
- import type {GlobalState} from './common/gitlab-types.d.ts'
12
-
13
- const app = express()
14
- const httpServer = new http.Server(app)
15
- const socketIoServer = new Server(httpServer)
16
-
17
- if (process.env.NODE_ENV !== 'production' && fs.existsSync('./src/dev-assets.ts')) {
18
- const {bindDevAssets} = await import('./dev-assets.ts')
19
- bindDevAssets(app)
20
- }
21
-
22
- app.disable('x-powered-by')
23
- app.get('/client.css', serveLessAsCss)
24
- app.use(express.static('public'))
25
- app.use(compression())
26
- app.use(basicAuth(config.auth))
27
-
28
- httpServer.listen(config.port, () => {
29
- console.log(`Listening on port *:${config.port}`)
30
- })
31
-
32
- const globalState: Omit<GlobalState, 'now'> = {
33
- projects: null,
34
- error: null,
35
- zoom: config.zoom,
36
- projectsOrder: config.projectsOrder,
37
- columns: config.columns,
38
- horizontal: config.horizontal,
39
- rotateRunningPipelines: config.rotateRunningPipelines,
40
- groupSuccessfulProjects: config.groupSuccessfulProjects
41
- }
42
-
43
- socketIoServer.on('connection', (socket) => {
44
- socket.emit('state', withDate(globalState))
45
- })
46
-
47
- async function runUpdate() {
48
- try {
49
- globalState.projects = await update(config.gitlabs, config.rotateRunningPipelines > 0)
50
- globalState.error = await errorIfRunnerOffline()
51
- socketIoServer.emit('state', withDate(globalState))
52
- } catch (error: unknown) {
53
- const message = error instanceof Error ? error.message : String(error)
54
- console.error(message)
55
- globalState.error = `Failed to communicate with GitLab API: ${message}`
56
- socketIoServer.emit('state', withDate(globalState))
57
- }
58
- setTimeout(runUpdate, config.interval)
59
- }
60
-
61
- async function errorIfRunnerOffline() {
62
- const offlineRunnersPerGitlab = await Promise.all(config.gitlabs.map(fetchOfflineRunners))
63
- const {offline, totalCount} = offlineRunnersPerGitlab.reduce((acc, runner) => {
64
- return {
65
- offline: acc.offline.concat(runner.offline),
66
- totalCount: acc.totalCount + runner.totalCount
67
- }
68
- }, {offline: [], totalCount: 0})
69
-
70
- if (offline.length > 0) {
71
- const names = offline.map(r => r.name).sort().join(', ')
72
- const counts = offline.length === totalCount ? 'All' : `${offline.length}/${totalCount}`
73
- return `${counts} runners offline: ${names}`
74
- }
75
- return null
76
- }
77
-
78
- await runUpdate()
79
-
80
- function withDate(state: Omit<GlobalState, 'now'>): GlobalState {
81
- return {
82
- ...state,
83
- now: Date.now()
84
- }
85
- }
package/src/auth.ts DELETED
@@ -1,20 +0,0 @@
1
- import authenticate from 'basic-auth'
2
- import type {NextFunction, Request, Response} from 'express'
3
-
4
- export function basicAuth(auth: {username: string; password: string } | undefined) {
5
- if (!auth) {
6
- console.log('No authentication configured')
7
- return (req: Request, res: Response, next: NextFunction) => next()
8
- }
9
-
10
- console.log('HTTP basic auth enabled')
11
- return (req: Request, res: Response, next: NextFunction) => {
12
- const {name, pass} = authenticate(req) || ({} as {name?: string; pass?: string})
13
- if (auth.username === name && auth.password === pass) {
14
- next()
15
- } else {
16
- res.setHeader('WWW-Authenticate', 'Basic realm="gitlab-radiator"')
17
- res.status(401).end()
18
- }
19
- }
20
- }
@@ -1,61 +0,0 @@
1
- export function argumentsFromDocumentUrl(): {override: {columns?: number, zoom?: number}, includedTopics: string[] | null, screen: {id: number, total: number}} {
2
- const params = new URLSearchParams(document.location.search)
3
- return {
4
- override: overrideArguments(params),
5
- includedTopics: topicArguments(params),
6
- screen: screenArguments(params)
7
- }
8
- }
9
-
10
- function topicArguments(params: URLSearchParams): string[] | null {
11
- const topics = params.get('topics')
12
- if (topics === null) {
13
- return null
14
- }
15
- return topics
16
- .split(',')
17
- .map(t => t.toLowerCase().trim())
18
- .filter(t => t)
19
- }
20
-
21
- function overrideArguments(params: URLSearchParams): {columns?: number, zoom?: number} {
22
- return {
23
- ...parseColumns(params),
24
- ...parseZoom(params)
25
- }
26
- }
27
-
28
- function parseColumns(params: URLSearchParams) {
29
- const columnsStr = params.get('columns')
30
- if (columnsStr) {
31
- const columns = Number(columnsStr)
32
- if (columns > 0 && columns <= 10) {
33
- return {columns}
34
- }
35
- }
36
- return {}
37
- }
38
-
39
- function parseZoom(params: URLSearchParams) {
40
- const zoomStr = params.get('zoom')
41
- if (zoomStr) {
42
- const zoom = Number(zoomStr)
43
- if (zoom > 0 && zoom <= 2) {
44
- return {zoom}
45
- }
46
- }
47
- return {}
48
- }
49
-
50
- function screenArguments(params: URLSearchParams): {id: number, total: number} {
51
- const matches = (/(\d)of(\d)/).exec(params.get('screen') || '')
52
- let id = matches ? Number(matches[1]) : 1
53
- const total = matches ? Number(matches[2]) : 1
54
- if (id > total) {
55
- id = total
56
- }
57
- return {
58
- id,
59
- total
60
- }
61
- }
@@ -1,18 +0,0 @@
1
- import {Groups} from './groups'
2
- import {Projects} from './projects'
3
- import React from 'react'
4
- import type {Project, ProjectsOrder} from '../common/gitlab-types'
5
-
6
- export function GroupedProjects({projects, projectsOrder, groupSuccessfulProjects, zoom, columns, now, screen, rotateRunningPipelines}: {projects: Project[], projectsOrder: ProjectsOrder[], groupSuccessfulProjects: boolean, zoom: number, columns: number, now: number, screen: {id: number, total: number}, rotateRunningPipelines: number}) {
7
- if (groupSuccessfulProjects) {
8
- const successfullProjects = projects.filter(p => p.status === 'success')
9
- const otherProjects= projects.filter(p => p.status !== 'success')
10
- const groupedProjects = Object.groupBy(successfullProjects, p => p.group)
11
-
12
- return <React.Fragment>
13
- <Projects now={now} zoom={zoom} columns={columns} projects={otherProjects} projectsOrder={projectsOrder} screen={screen} rotateRunningPipelines={rotateRunningPipelines}/>
14
- <Groups now={now} zoom={zoom} columns={columns} groupedProjects={groupedProjects}/>
15
- </React.Fragment>
16
- }
17
- return <Projects now={now} zoom={zoom} columns={columns} projects={projects} projectsOrder={projectsOrder} screen={screen} rotateRunningPipelines={rotateRunningPipelines}/>
18
- }
@@ -1,50 +0,0 @@
1
- import React from 'react'
2
- import {Timestamp} from './timestamp'
3
- import {style, zoomStyle} from './projects'
4
- import type {Pipeline, Project} from '../common/gitlab-types'
5
-
6
- export function Groups({groupedProjects, now, zoom, columns}: {groupedProjects: Partial<Record<string, Project[]>>, now: number, zoom: number, columns: number}) {
7
- return <ol className="groups" style={zoomStyle(zoom)}>
8
- {Object
9
- .entries(groupedProjects)
10
- .sort(([groupName1], [groupName2]) => groupName1.localeCompare(groupName2))
11
- .map(([groupName, projects]) => <GroupElement columns={columns} groupName={groupName} key={groupName} projects={projects} now={now}/>)
12
- }
13
- </ol>
14
- }
15
-
16
- function GroupElement({groupName, projects, now, columns}: {groupName: string, projects: Project[] | undefined, now: number, columns: number}) {
17
- if (!projects) {
18
- return null
19
- }
20
-
21
- const pipelines: (Pipeline & {project: string})[] = []
22
- projects.forEach((project) => {
23
- project.pipelines.forEach((pipeline) => {
24
- pipelines.push({
25
- ...pipeline,
26
- project: project.nameWithoutNamespace
27
- })
28
- })
29
- })
30
-
31
- return <li className={'group'} style={style(columns)}>
32
- <h2>{groupName}</h2>
33
- <div className={'group-info'}>{projects.length} Project{projects.length > 1 ? 's' : ''}</div>
34
- <GroupInfoElement now={now} pipeline={pipelines[0]}/>
35
- </li>
36
- }
37
-
38
- function GroupInfoElement({now, pipeline}: {now: number, pipeline: (Pipeline & {project: string})}) {
39
- return <div className="pipeline-info">
40
- <div>
41
- <span>{pipeline.commit ? pipeline.commit.author : '-'}</span>
42
- <span>{pipeline.commit ? pipeline.project : '-'}</span>
43
- </div>
44
- <div>
45
- <Timestamp stages={pipeline.stages} now={now}/>
46
- <span>on {pipeline.ref}</span>
47
- </div>
48
- </div>
49
- }
50
-
@@ -1,79 +0,0 @@
1
- import 'core-js/stable'
2
- import 'regenerator-runtime/runtime'
3
-
4
- import {argumentsFromDocumentUrl} from './arguments'
5
- import {createRoot} from 'react-dom/client'
6
- import {GroupedProjects} from './groupedProjects'
7
- import React, {useCallback, useEffect, useMemo, useState} from 'react'
8
- import {io, Socket} from 'socket.io-client'
9
- import type {GlobalState, Project} from '../common/gitlab-types'
10
-
11
- function RadiatorApp() {
12
- const args = useMemo(() => argumentsFromDocumentUrl(), [])
13
- const [state, setState] = useState<GlobalState>({
14
- columns: 1,
15
- error: null,
16
- groupSuccessfulProjects: false,
17
- horizontal: false,
18
- rotateRunningPipelines: 0,
19
- projects: null,
20
- projectsOrder: [],
21
- now: 0,
22
- zoom: 1
23
- })
24
- const {now, zoom, columns, projects, projectsOrder, groupSuccessfulProjects, horizontal, rotateRunningPipelines} = state
25
- const projectsByTags = filterProjectsByTopics(projects, args.includedTopics)
26
-
27
- const onServerStateUpdated = useCallback((serverState: GlobalState) => {
28
- setState(() => ({
29
- ...serverState,
30
- ...args.override
31
- }))
32
- }, [args.override])
33
-
34
- const onDisconnect = useCallback(() => setState(prev => ({...prev, error: 'gitlab-radiator server is offline'})), [])
35
-
36
- useEffect(() => {
37
- const socket: Socket = io()
38
- socket.on('state', onServerStateUpdated)
39
- socket.on('disconnect', onDisconnect)
40
- return () => {
41
- socket.off('state', onServerStateUpdated)
42
- socket.off('disconnect', onDisconnect)
43
- socket.close()
44
- }
45
- }, [onServerStateUpdated, onDisconnect])
46
-
47
- return <div className={horizontal ? 'horizontal' : ''}>
48
- {state.error && <div className="error">{state.error}</div>}
49
- {!state.projects && <h2 className="loading">Fetching projects and CI pipelines from GitLab...</h2>}
50
- {state.projects?.length === 0 && <h2 className="loading">No projects with CI pipelines found.</h2>}
51
-
52
- {projectsByTags &&
53
- <GroupedProjects now={now} zoom={zoom} columns={columns}
54
- projects={projectsByTags} projectsOrder={projectsOrder}
55
- groupSuccessfulProjects={groupSuccessfulProjects}
56
- screen={args.screen}
57
- rotateRunningPipelines={rotateRunningPipelines}/>
58
- }
59
- </div>
60
- }
61
-
62
- function filterProjectsByTopics(projects: Project[] | null, includedTopics: string[] | null) {
63
- if (projects === null) {
64
- return null
65
- }
66
- if (!includedTopics) {
67
- return projects
68
- }
69
- if (includedTopics.length === 0) {
70
- return projects.filter(p => p.topics.length === 0)
71
- }
72
- return projects.filter(project => project.topics.some(tag => includedTopics?.includes(tag)))
73
- }
74
-
75
-
76
- const root = createRoot(document.getElementById('app')!)
77
- root.render(<RadiatorApp/>)
78
-
79
- module.hot?.accept()
@@ -1,16 +0,0 @@
1
- import React from 'react'
2
- import {Timestamp} from './timestamp'
3
- import type {Pipeline} from '../common/gitlab-types'
4
-
5
- export function Info({pipeline, now, commitAsTitle}: {pipeline: Pipeline, now: number, commitAsTitle: boolean}) {
6
- return <div className="pipeline-info">
7
- <div>
8
- <span>{pipeline.commit ? pipeline.commit.author : '-'}</span>
9
- <span>{commitAsTitle ? `Pipeline id: ${pipeline.id}` : (pipeline.commit ? `'${pipeline.commit.title}'` : '-')}</span>
10
- </div>
11
- <div>
12
- <Timestamp stages={pipeline.stages} now={now}/>
13
- <span>on {pipeline.ref}</span>
14
- </div>
15
- </div>
16
- }
@@ -1,57 +0,0 @@
1
- import React from 'react'
2
- import type {Job, JobStatus} from '../common/gitlab-types'
3
-
4
- const NON_BREAKING_SPACE = '\xa0'
5
-
6
- const JOB_STATES_IN_INTEREST_ORDER: JobStatus[] = [
7
- 'failed',
8
- 'running',
9
- 'created',
10
- 'pending',
11
- 'success',
12
- 'skipped'
13
- ]
14
-
15
- function interestOrder(a: Job, b: Job) {
16
- return JOB_STATES_IN_INTEREST_ORDER.indexOf(a.status) - JOB_STATES_IN_INTEREST_ORDER.indexOf(b.status)
17
- }
18
-
19
- export function Jobs({jobs, maxNonFailedJobsVisible}: {jobs: Job[], maxNonFailedJobsVisible: number}) {
20
- const failedJobs = jobs.filter(job => job.status === 'failed')
21
- const nonFailedJobs = jobs.filter(job => job.status !== 'failed').sort(interestOrder)
22
- const filteredJobs = sortByOriginalOrder(
23
- failedJobs.concat(
24
- nonFailedJobs.slice(0, Math.max(0, maxNonFailedJobsVisible - failedJobs.length))
25
- ),
26
- jobs
27
- )
28
-
29
- const hiddenJobs = jobs.filter(job => filteredJobs.indexOf(job) === -1)
30
- const hiddenCountsByStatus = Object.fromEntries(
31
- Object.entries(Object.groupBy(hiddenJobs, job => job.status))
32
- .map(([status, jobsForStatus]) => [status, jobsForStatus!.length])
33
- )
34
-
35
- const hiddenJobsText = Object.entries(hiddenCountsByStatus)
36
- .sort(([statusA], [statusB]) => statusA.localeCompare(statusB))
37
- .map(([status, count]: [string, number]) => `${count}${NON_BREAKING_SPACE}${status}`)
38
- .join(', ')
39
-
40
- return <ol className="jobs">
41
- {filteredJobs.map((job: Job) => <JobElement job={job} key={job.id}/>)}
42
- {
43
- hiddenJobs.length > 0 ? <li className="hidden-jobs">+&nbsp;{hiddenJobsText}</li> : null
44
- }
45
- </ol>
46
- }
47
-
48
- function JobElement({job}: {job: Job}) {
49
- return <li className={job.status}>
50
- <a href={job.url} target="_blank" rel="noopener noreferrer">{job.name}</a>
51
- {!job.url && job.name}
52
- </li>
53
- }
54
-
55
- function sortByOriginalOrder(filteredJobs: Job[], jobs: Job[]) {
56
- return [...filteredJobs].sort((a, b) => jobs.indexOf(a) - jobs.indexOf(b))
57
- }
@@ -1,82 +0,0 @@
1
- import {Info} from './info'
2
- import React, {useState, useEffect} from 'react'
3
- import {Stages} from './stages'
4
- import type {Project, ProjectsOrder} from '../common/gitlab-types'
5
-
6
- export function Projects({columns, now, projects, projectsOrder, screen, zoom, rotateRunningPipelines}: {columns: number, now: number, projects: Project[], projectsOrder: ProjectsOrder[], screen: {id: number, total: number}, zoom: number, rotateRunningPipelines: number}) {
7
- return <ol className="projects" style={zoomStyle(zoom)}>
8
- {sortByMultipleKeys(projects, projectsOrder)
9
- .filter(forScreen(screen, projects.length))
10
- .map(project => <ProjectElement now={now} columns={columns} rotateRunningPipelines={rotateRunningPipelines} project={project} key={project.id}/>)
11
- }
12
- </ol>
13
- }
14
-
15
- function sortByMultipleKeys(projects: Project[], keys: ProjectsOrder[]): Project[] {
16
- return [...projects].sort((a, b) => {
17
- for (const key of keys) {
18
- const result = key === 'id'
19
- ? a[key] - b[key]
20
- : a[key].localeCompare(b[key])
21
-
22
- if (result !== 0) {
23
- return result
24
- }
25
- }
26
- return 0
27
- })
28
- }
29
-
30
- function ProjectElement({columns, now, project, rotateRunningPipelines}: {columns: number, now: number, rotateRunningPipelines: number, project: Project}) {
31
- const [counter, setCounter] = useState<number>(0)
32
- const runningCount = project.pipelines.filter(p => p.status === 'running').length
33
- const isRotating = rotateRunningPipelines > 0 && runningCount > 1
34
-
35
- useEffect(() => {
36
- if (!isRotating) {
37
- return
38
- }
39
-
40
- const timer = setInterval(() => setCounter(previous => previous + 1), rotateRunningPipelines)
41
- return () => clearInterval(timer)
42
- }, [isRotating, rotateRunningPipelines, setCounter])
43
-
44
- const pipelineIndex = isRotating ? (counter % runningCount) : 0
45
- const indexLabel = isRotating ? `${pipelineIndex + 1}/${runningCount} `: ''
46
- const pipeline = project.pipelines[pipelineIndex]
47
-
48
- const h2Class = project.commitAsTitle ? 'commit_title' : ''
49
-
50
- return <li className={`project ${project.status}`} style={style(columns)}>
51
- <h2 className={`${h2Class}`}>
52
- {project.url && <a href={`${project.url}/pipelines`} target="_blank" rel="noopener noreferrer">{indexLabel}{project.commitAsTitle ? (pipeline.commit ? pipeline.commit.title : '-') : project.name}</a>}
53
- {!project.url && project.name}
54
- </h2>
55
- <Stages stages={pipeline.stages} maxNonFailedJobsVisible={project.maxNonFailedJobsVisible}/>
56
- <Info now={now} pipeline={pipeline} commitAsTitle={project.commitAsTitle}/>
57
- </li>
58
- }
59
-
60
- function forScreen(screen: {id: number, total: number}, projectsCount: number) {
61
- const perScreen = Math.ceil(projectsCount / screen.total)
62
- const first = perScreen * (screen.id - 1)
63
- const last = perScreen * screen.id
64
- return (_project: Project, projectIndex: number) => projectIndex >= first && projectIndex < last
65
- }
66
-
67
- export function zoomStyle(zoom: number) {
68
- const widthPercentage = Math.round(100 / zoom)
69
- return {
70
- transform: `scale(${zoom})`,
71
- width: `${widthPercentage}vmax`
72
- }
73
- }
74
-
75
- export function style(columns: number) {
76
- const marginPx = 12
77
- const widthPercentage = Math.floor(100 / columns)
78
- return {
79
- margin: `${marginPx}px`,
80
- width: `calc(${widthPercentage}% - ${2 * marginPx}px)`
81
- }
82
- }
@@ -1,18 +0,0 @@
1
- import {Jobs} from './jobs'
2
- import React from 'react'
3
- import type {Stage} from '../common/gitlab-types'
4
-
5
- export function Stages({stages, maxNonFailedJobsVisible}: {stages: Stage[], maxNonFailedJobsVisible: number}) {
6
- return <ol className="stages">
7
- {stages.map((stage, index) =>
8
- <StageElement stage={stage} maxNonFailedJobsVisible={maxNonFailedJobsVisible} key={`${index}-${stage.name}`}/>
9
- )}
10
- </ol>
11
- }
12
-
13
- function StageElement({stage, maxNonFailedJobsVisible}: {stage: Stage, maxNonFailedJobsVisible: number}) {
14
- return <li className="stage">
15
- <div className="name">{stage.name}</div>
16
- <Jobs jobs={stage.jobs} maxNonFailedJobsVisible={maxNonFailedJobsVisible}/>
17
- </li>
18
- }
@@ -1,37 +0,0 @@
1
- import moment from 'moment'
2
- import React from 'react'
3
- import type {Stage} from '../common/gitlab-types'
4
-
5
- export function Timestamp({stages, now}: {stages: Stage[], now: number}) {
6
- const timestamps = getTimestamps(stages)
7
- if (timestamps.length === 0) {
8
- return <span>Pending...</span>
9
- }
10
-
11
- const finished = timestamps
12
- .map(t => t.finishedAt)
13
- .filter((t): t is number => t !== null)
14
-
15
- const inProgress = timestamps.length > finished.length
16
- if (inProgress) {
17
- const [{startedAt}] = timestamps.sort((a, b) => a.startedAt - b.startedAt)
18
- return <span>Started {moment(startedAt).from(now)}</span>
19
- }
20
-
21
- const [latestFinishedAt] = finished.sort((a, b) => b - a)
22
- return <span>Finished {moment(latestFinishedAt).from(now)}</span>
23
- }
24
-
25
- function getTimestamps(stages: Stage[]): {startedAt: number, finishedAt: number | null}[] {
26
- return stages
27
- .flatMap(s => s.jobs)
28
- .map(job => ({
29
- startedAt: parseDate(job.startedAt),
30
- finishedAt: parseDate(job.finishedAt)
31
- }))
32
- .filter((t): t is {startedAt: number, finishedAt: number | null} => t.startedAt !== null)
33
- }
34
-
35
- function parseDate(value: string | null) {
36
- return value ? new Date(value).valueOf() : null
37
- }
@@ -1,24 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "outDir": "./dist/",
4
- "target": "es5",
5
- "lib": ["ES2024.Object", "dom"],
6
- "allowJs": true,
7
- "skipLibCheck": true,
8
- "esModuleInterop": true,
9
- "allowSyntheticDefaultImports": true,
10
- "strict": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "module": "esnext",
13
- "moduleResolution": "node",
14
- "isolatedModules": true,
15
- "resolveJsonModule": true,
16
- "jsx": "react",
17
- "sourceMap": true,
18
- "declaration": false,
19
- "noUnusedLocals": true,
20
- "noUnusedParameters": true,
21
- "incremental": true,
22
- "noFallthroughCasesInSwitch": true
23
- }
24
- }
@@ -1,58 +0,0 @@
1
- export interface GlobalState {
2
- columns: number
3
- error: string | null
4
- groupSuccessfulProjects: boolean
5
- horizontal: boolean
6
- rotateRunningPipelines: number
7
- projects: Project[] | null
8
- projectsOrder: Array<ProjectsOrder>
9
- zoom: number
10
- now: number
11
- }
12
-
13
- export interface Project {
14
- archived: boolean
15
- group: string
16
- id: number
17
- name: string
18
- nameWithoutNamespace: string
19
- topics: string[]
20
- url: string
21
- default_branch: string
22
- commitAsTitle: boolean
23
- pipelines: Pipeline[]
24
- maxNonFailedJobsVisible: number
25
- status: JobStatus
26
- }
27
-
28
- // Keys that represent either string or number values, and can be compared with < and >
29
- export type ProjectsOrder = keyof Pick<Project, 'status' | 'name' | 'id' | 'nameWithoutNamespace' | 'group'>
30
-
31
- export interface Pipeline {
32
- commit: Commit | null
33
- id: number
34
- ref: string
35
- stages: Stage[]
36
- status: JobStatus
37
- }
38
-
39
- export interface Commit {
40
- title: string
41
- author: string
42
- }
43
-
44
- export interface Stage {
45
- jobs: Job[]
46
- name: string
47
- }
48
-
49
- export interface Job {
50
- finishedAt: string | null
51
- id: number
52
- name: string
53
- startedAt: string | null
54
- status: JobStatus
55
- url: string
56
- }
57
-
58
- export type JobStatus = 'canceled' | 'created' | 'failed' | 'manual' | 'pending' | 'running' | 'skipped' | 'success'