gitlab-radiator 3.4.0 → 3.4.2
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.
- package/package.json +19 -19
- package/public/client.js +2 -0
- package/public/client.js.LICENSE.txt +38 -0
- package/src/app.js +133 -86
- package/src/auth.js +19 -11
- package/src/config.js +33 -29
- package/src/gitlab/client.js +30 -19
- package/src/gitlab/index.js +127 -41
- package/src/gitlab/pipelines.js +292 -97
- package/src/gitlab/projects.js +97 -43
- package/src/gitlab/runners.js +63 -15
- package/src/index.js +4 -2
- package/.babelrc +0 -4
- package/.eslintrc +0 -13
- package/.github/workflows/test.yml +0 -20
- package/.nvmrc +0 -1
- package/build-npm +0 -19
- package/screenshot.png +0 -0
- package/src/client/.eslintrc +0 -21
- package/src/client/arguments.ts +0 -75
- package/src/client/gitlab-types.ts +0 -54
- package/src/client/groupedProjects.tsx +0 -40
- package/src/client/groups.tsx +0 -59
- package/src/client/index.tsx +0 -95
- package/src/client/info.tsx +0 -16
- package/src/client/jobs.tsx +0 -56
- package/src/client/projects.tsx +0 -51
- package/src/client/renderTimestamp.tsx +0 -45
- package/src/client/stages.tsx +0 -18
- package/src/dev-assets.js +0 -10
- package/test/gitlab/projects.test.js +0 -75
- package/test/gitlab-integration.js +0 -434
- package/tsconfig.json +0 -28
- package/webpack.common.js +0 -25
- package/webpack.dev.js +0 -13
- package/webpack.prod.js +0 -9
package/.babelrc
DELETED
package/.eslintrc
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
name: Run tests
|
|
2
|
-
|
|
3
|
-
on: [push, pull_request]
|
|
4
|
-
|
|
5
|
-
jobs:
|
|
6
|
-
build:
|
|
7
|
-
runs-on: ubuntu-latest
|
|
8
|
-
strategy:
|
|
9
|
-
matrix:
|
|
10
|
-
node-version: [12.x, 14.x]
|
|
11
|
-
steps:
|
|
12
|
-
- uses: actions/checkout@v2
|
|
13
|
-
- uses: actions/setup-node@v1
|
|
14
|
-
with:
|
|
15
|
-
node-version: ${{ matrix.node-version }}
|
|
16
|
-
- run: npm ci
|
|
17
|
-
- run: npm audit
|
|
18
|
-
- run: npm run eslint
|
|
19
|
-
- run: npm test
|
|
20
|
-
- run: npm run build
|
package/.nvmrc
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
16.17.0
|
package/build-npm
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
rm -fr build
|
|
4
|
-
mkdir -p build/src
|
|
5
|
-
|
|
6
|
-
# Copy static resources
|
|
7
|
-
cp -r public build
|
|
8
|
-
|
|
9
|
-
# Copy LICENSE, README and package.json
|
|
10
|
-
cp LICENSE package.json README.md build
|
|
11
|
-
|
|
12
|
-
# Copy bin script
|
|
13
|
-
cp -r bin build
|
|
14
|
-
|
|
15
|
-
# Bundle and minify client JS
|
|
16
|
-
npx webpack --config webpack.prod.js
|
|
17
|
-
|
|
18
|
-
# Transpile server
|
|
19
|
-
node_modules/.bin/babel src --ignore **/client/*.js,**/dev-assets.js --out-dir build/src
|
package/screenshot.png
DELETED
|
Binary file
|
package/src/client/.eslintrc
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"env": {
|
|
3
|
-
"browser": true,
|
|
4
|
-
"es6": true
|
|
5
|
-
},
|
|
6
|
-
"parser": "@typescript-eslint/parser",
|
|
7
|
-
"plugins": [
|
|
8
|
-
"@typescript-eslint"
|
|
9
|
-
],
|
|
10
|
-
"extends": [
|
|
11
|
-
"eslint:recommended",
|
|
12
|
-
"plugin:react/recommended",
|
|
13
|
-
"plugin:@typescript-eslint/eslint-recommended",
|
|
14
|
-
"plugin:@typescript-eslint/recommended"
|
|
15
|
-
],
|
|
16
|
-
"settings": {
|
|
17
|
-
"react": {
|
|
18
|
-
"version": "17.0"
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
package/src/client/arguments.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
interface ParsedQueryString {
|
|
2
|
-
[key: string]: string | undefined
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function argumentsFromDocumentUrl(): {override: {columns?: number, zoom?: number}, includedTags: string[] | null, screen: {id: number, total: number}} {
|
|
6
|
-
const args = parseQueryString(document.location.search)
|
|
7
|
-
return {
|
|
8
|
-
override: overrideArguments(args),
|
|
9
|
-
includedTags: tagArguments(args),
|
|
10
|
-
screen: screenArguments(args)
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function tagArguments(args: ParsedQueryString): string[] | null {
|
|
15
|
-
if (args.tags === undefined) {
|
|
16
|
-
return null
|
|
17
|
-
}
|
|
18
|
-
return args.tags
|
|
19
|
-
.split(',')
|
|
20
|
-
.map(t => t.toLowerCase().trim())
|
|
21
|
-
.filter(t => t)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function overrideArguments(args: ParsedQueryString): {columns?: number, zoom?: number} {
|
|
25
|
-
return {
|
|
26
|
-
...parseColumns(args),
|
|
27
|
-
...parseZoom(args)
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function parseColumns(args: ParsedQueryString) {
|
|
32
|
-
if (args.columns) {
|
|
33
|
-
const columns = Number(args.columns)
|
|
34
|
-
if (columns > 0 && columns <= 10) {
|
|
35
|
-
return {columns}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return {}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
function parseZoom(args: ParsedQueryString) {
|
|
43
|
-
if (args.zoom) {
|
|
44
|
-
const zoom = Number(args.zoom)
|
|
45
|
-
if (zoom > 0 && zoom <= 2) {
|
|
46
|
-
return {zoom}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return {}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function screenArguments(args: ParsedQueryString): {id: number, total: number} {
|
|
53
|
-
const matches = (/(\d)of(\d)/).exec(args.screen || '')
|
|
54
|
-
let id = matches ? Number(matches[1]) : 1
|
|
55
|
-
const total = matches ? Number(matches[2]) : 1
|
|
56
|
-
if (id > total) {
|
|
57
|
-
id = total
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
id,
|
|
61
|
-
total
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function parseQueryString(search: string): ParsedQueryString {
|
|
66
|
-
const entries = search
|
|
67
|
-
.slice(1)
|
|
68
|
-
.split('&')
|
|
69
|
-
.filter(parameter => parameter)
|
|
70
|
-
.map((parameter: string): [string, string | undefined] => {
|
|
71
|
-
const [key, value] = parameter.split('=')
|
|
72
|
-
return [key, value]
|
|
73
|
-
})
|
|
74
|
-
return Object.fromEntries(entries)
|
|
75
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
export interface GlobalState {
|
|
3
|
-
columns: number
|
|
4
|
-
error: string | null
|
|
5
|
-
groupSuccessfulProjects: boolean
|
|
6
|
-
projects: Project[] | null
|
|
7
|
-
projectsOrder: string[]
|
|
8
|
-
zoom: number
|
|
9
|
-
now: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface Project {
|
|
13
|
-
archived: false
|
|
14
|
-
group: string
|
|
15
|
-
id: number
|
|
16
|
-
name: string
|
|
17
|
-
nameWithoutNamespace: string
|
|
18
|
-
tags: string[]
|
|
19
|
-
url: string
|
|
20
|
-
default_branch: string
|
|
21
|
-
pipelines: Pipeline[]
|
|
22
|
-
maxNonFailedJobsVisible: number
|
|
23
|
-
status: 'success' | 'failed'
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface Pipeline {
|
|
27
|
-
commit: Commit | null
|
|
28
|
-
id: number
|
|
29
|
-
ref: string
|
|
30
|
-
stages: Stage[]
|
|
31
|
-
status: 'success' | 'failed'
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface Commit {
|
|
35
|
-
title: string
|
|
36
|
-
author: string
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface Stage {
|
|
40
|
-
jobs: Job[]
|
|
41
|
-
name: string
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface Job {
|
|
45
|
-
finishedAt: string | null
|
|
46
|
-
id: number
|
|
47
|
-
name: string
|
|
48
|
-
stage: string
|
|
49
|
-
startedAt: string | null
|
|
50
|
-
status: JobStatus
|
|
51
|
-
url: string
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export type JobStatus = 'created' | 'failed' | 'manual' | 'pending' | 'running' | 'skipped' | 'success'
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import _ from 'lodash'
|
|
2
|
-
import {Groups} from './groups'
|
|
3
|
-
import type {Project} from './gitlab-types'
|
|
4
|
-
import {Projects} from './projects'
|
|
5
|
-
import React from 'react'
|
|
6
|
-
|
|
7
|
-
export function GroupedProjects({projects, projectsOrder, groupSuccessfulProjects, zoom, columns, now, screen}: {projects: Project[], projectsOrder: string[], groupSuccessfulProjects: boolean, zoom: number, columns: number, now: number, screen: {id: number, total: number}}): JSX.Element {
|
|
8
|
-
if (groupSuccessfulProjects) {
|
|
9
|
-
return renderProjectsGrouped(projects, projectsOrder, zoom, columns, now, screen)
|
|
10
|
-
}
|
|
11
|
-
return renderProjects(projects, projectsOrder, zoom, columns, now, screen)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function renderProjectsGrouped(projects: Project[], projectsOrder: string[], zoom: number, columns: number, now: number, screen: {id: number, total: number}) {
|
|
15
|
-
const successfullProjects: Project[] = []
|
|
16
|
-
const otherProjects: Project[] = []
|
|
17
|
-
projects.forEach((project) => {
|
|
18
|
-
if (project.status === 'success') {
|
|
19
|
-
successfullProjects.push(project)
|
|
20
|
-
} else {
|
|
21
|
-
otherProjects.push(project)
|
|
22
|
-
}
|
|
23
|
-
})
|
|
24
|
-
const groupedProjects = _.groupBy(successfullProjects, 'group')
|
|
25
|
-
return <React.Fragment>
|
|
26
|
-
{renderProjects(otherProjects, projectsOrder, zoom, columns, now, screen)}
|
|
27
|
-
{renderGroupedProjects(groupedProjects, zoom, columns, now)}
|
|
28
|
-
</React.Fragment>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function renderProjects(projects: Project[], projectsOrder: string[], zoom: number, columns: number, now: number, screen: {id: number, total: number}) {
|
|
32
|
-
return <Projects now={now} zoom={zoom} columns={columns}
|
|
33
|
-
projects={projects || []} projectsOrder={projectsOrder}
|
|
34
|
-
screen={screen}/>
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function renderGroupedProjects(groupedProjects: {[groupname: string]: Project[]}, zoom: number, columns: number, now: number) {
|
|
38
|
-
return <Groups zoom={zoom} columns={columns} now={now}
|
|
39
|
-
groupedProjects={groupedProjects || []} />
|
|
40
|
-
}
|
package/src/client/groups.tsx
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import type {Pipeline, Project} from './gitlab-types'
|
|
2
|
-
import React from 'react'
|
|
3
|
-
import {renderTimestamp} from './renderTimestamp'
|
|
4
|
-
|
|
5
|
-
export function Groups({groupedProjects, now, zoom, columns}: {groupedProjects: {[groupname: string]: Project[]}, now: number, zoom: number, columns: number}): JSX.Element {
|
|
6
|
-
return <ol className="groups" style={zoomStyle(zoom)}>
|
|
7
|
-
{Object
|
|
8
|
-
.entries(groupedProjects)
|
|
9
|
-
.sort(([groupName1], [groupName2]) => groupName1.localeCompare(groupName2))
|
|
10
|
-
.map(([groupName, projects]) => <GroupElement columns={columns} groupName={groupName} key={groupName} projects={projects} now={now}/>)
|
|
11
|
-
}
|
|
12
|
-
</ol>
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function GroupElement({groupName, projects, now, columns}: {groupName: string, projects: Project[], now: number, columns: number}) {
|
|
16
|
-
const pipelines: (Pipeline & {project: string})[] = []
|
|
17
|
-
projects.forEach((project) => {
|
|
18
|
-
project.pipelines.forEach((pipeline) => {
|
|
19
|
-
pipelines.push({
|
|
20
|
-
...pipeline,
|
|
21
|
-
project: project.nameWithoutNamespace
|
|
22
|
-
})
|
|
23
|
-
})
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
return <li className={'group'} style={style(columns)}>
|
|
27
|
-
<h2>{groupName}</h2>
|
|
28
|
-
<div className={'group-info'}>{projects.length} Project{projects.length > 1 ? 's' : ''}</div>
|
|
29
|
-
<GroupInfoElement now={now} pipeline={pipelines[0]}/>
|
|
30
|
-
</li>
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function GroupInfoElement({now, pipeline}: {now: number, pipeline: (Pipeline & {project: string})}) {
|
|
34
|
-
return <div className="pipeline-info">
|
|
35
|
-
<div>
|
|
36
|
-
<span>{pipeline.commit ? pipeline.commit.author : '-'}</span>
|
|
37
|
-
<span>{pipeline.commit ? pipeline.project : '-'}</span>
|
|
38
|
-
</div>
|
|
39
|
-
<div>
|
|
40
|
-
<span>{renderTimestamp(pipeline.stages, now)}</span>
|
|
41
|
-
<span>on {pipeline.ref}</span>
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function zoomStyle(zoom: number) {
|
|
47
|
-
const widthPercentage = Math.round(100 / zoom)
|
|
48
|
-
return {
|
|
49
|
-
transform: `scale(${zoom})`,
|
|
50
|
-
width: `${widthPercentage}vmax`
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function style(columns: number) {
|
|
55
|
-
const widthPercentage = Math.round(90 / columns)
|
|
56
|
-
return {
|
|
57
|
-
width: `${widthPercentage}%`
|
|
58
|
-
}
|
|
59
|
-
}
|
package/src/client/index.tsx
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import 'core-js/stable'
|
|
2
|
-
import 'regenerator-runtime/runtime'
|
|
3
|
-
|
|
4
|
-
import type {GlobalState, Project} from './gitlab-types'
|
|
5
|
-
import {argumentsFromDocumentUrl} from './arguments'
|
|
6
|
-
import {createRoot} from 'react-dom/client'
|
|
7
|
-
import {GroupedProjects} from './groupedProjects'
|
|
8
|
-
import React from 'react'
|
|
9
|
-
|
|
10
|
-
class RadiatorApp extends React.Component<unknown, GlobalState> {
|
|
11
|
-
public args: {override: {columns?: number, zoom?: number}, includedTags: string[] | null, screen: {id: number, total: number}}
|
|
12
|
-
|
|
13
|
-
constructor(props: unknown) {
|
|
14
|
-
super(props)
|
|
15
|
-
this.state = {
|
|
16
|
-
columns: 1,
|
|
17
|
-
error: null,
|
|
18
|
-
groupSuccessfulProjects: false,
|
|
19
|
-
projects: null,
|
|
20
|
-
projectsOrder: [],
|
|
21
|
-
now: 0,
|
|
22
|
-
zoom: 1
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
this.args = argumentsFromDocumentUrl()
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
componentDidMount = () => {
|
|
29
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
-
const socket = (window as any).io()
|
|
31
|
-
socket.on('state', this.onServerStateUpdated.bind(this))
|
|
32
|
-
socket.on('disconnect', this.onDisconnect.bind(this))
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
render = () =>
|
|
36
|
-
<div>
|
|
37
|
-
{this.renderErrorMessage()}
|
|
38
|
-
{this.renderProgressMessage()}
|
|
39
|
-
|
|
40
|
-
{this.state.projects &&
|
|
41
|
-
<GroupedProjects now={this.state.now} zoom={this.state.zoom} columns={this.state.columns}
|
|
42
|
-
projects={this.state.projects} projectsOrder={this.state.projectsOrder}
|
|
43
|
-
groupSuccessfulProjects={this.state.groupSuccessfulProjects}
|
|
44
|
-
screen={this.args.screen}/>
|
|
45
|
-
}
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
renderErrorMessage = () =>
|
|
49
|
-
this.state.error && <div className="error">{this.state.error}</div>
|
|
50
|
-
|
|
51
|
-
renderProgressMessage = () => {
|
|
52
|
-
if (!this.state.projects) {
|
|
53
|
-
return <h2 className="loading">Fetching projects and CI pipelines from GitLab...</h2>
|
|
54
|
-
} else if (this.state.projects.length === 0) {
|
|
55
|
-
return <h2 className="loading">No projects with CI pipelines found.</h2>
|
|
56
|
-
}
|
|
57
|
-
return null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
onServerStateUpdated = (state: GlobalState) => {
|
|
61
|
-
this.setState({
|
|
62
|
-
...state,
|
|
63
|
-
...this.args.override,
|
|
64
|
-
projects: this.filterProjectsByTags(state.projects)
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
onDisconnect = () => this.setState({error: 'gitlab-radiator server is offline'})
|
|
69
|
-
|
|
70
|
-
filterProjectsByTags = (projects: Project[] | null) => {
|
|
71
|
-
if (projects === null) {
|
|
72
|
-
return null
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// No tag list specified, include all projects
|
|
76
|
-
if (!this.args.includedTags) {
|
|
77
|
-
return projects
|
|
78
|
-
}
|
|
79
|
-
// Empty tag list specified, include projects without tags
|
|
80
|
-
if (this.args.includedTags.length === 0) {
|
|
81
|
-
return projects.filter(project =>
|
|
82
|
-
project.tags.length === 0
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
// Tag list specified, include projects which have at least one of them
|
|
86
|
-
return projects.filter(project =>
|
|
87
|
-
project.tags.some(tag => this.args.includedTags?.includes(tag))
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const root = createRoot(document.getElementById('app')!)
|
|
93
|
-
root.render(<RadiatorApp/>);
|
|
94
|
-
|
|
95
|
-
module.hot?.accept()
|
package/src/client/info.tsx
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type {Pipeline} from './gitlab-types'
|
|
2
|
-
import React from 'react'
|
|
3
|
-
import {renderTimestamp} from './renderTimestamp'
|
|
4
|
-
|
|
5
|
-
export function Info({pipeline, now}: {pipeline: Pipeline, now: number}): JSX.Element {
|
|
6
|
-
return <div className="pipeline-info">
|
|
7
|
-
<div>
|
|
8
|
-
<span>{pipeline.commit ? pipeline.commit.author : '-'}</span>
|
|
9
|
-
<span>{pipeline.commit ? `'${pipeline.commit.title}'` : '-'}</span>
|
|
10
|
-
</div>
|
|
11
|
-
<div>
|
|
12
|
-
<span>{renderTimestamp(pipeline.stages, now)}</span>
|
|
13
|
-
<span>on {pipeline.ref}</span>
|
|
14
|
-
</div>
|
|
15
|
-
</div>
|
|
16
|
-
}
|
package/src/client/jobs.tsx
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import type {Job, JobStatus} from './gitlab-types'
|
|
2
|
-
import _ from 'lodash'
|
|
3
|
-
import React from 'react'
|
|
4
|
-
|
|
5
|
-
const NON_BREAKING_SPACE = '\xa0'
|
|
6
|
-
|
|
7
|
-
const JOB_STATES_IN_INTEREST_ORDER: JobStatus[] = [
|
|
8
|
-
'failed',
|
|
9
|
-
'running',
|
|
10
|
-
'created',
|
|
11
|
-
'pending',
|
|
12
|
-
'success',
|
|
13
|
-
'skipped'
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
export function Jobs({jobs, maxNonFailedJobsVisible}: {jobs: Job[], maxNonFailedJobsVisible: number}): JSX.Element {
|
|
17
|
-
const [failedJobs, nonFailedJobs] = _.partition(jobs, {status: 'failed'})
|
|
18
|
-
const filteredJobs = sortByOriginalOrder(
|
|
19
|
-
failedJobs.concat(
|
|
20
|
-
_.orderBy(nonFailedJobs, ({status}) => JOB_STATES_IN_INTEREST_ORDER.indexOf(status))
|
|
21
|
-
.slice(0, Math.max(0, maxNonFailedJobsVisible - failedJobs.length))
|
|
22
|
-
),
|
|
23
|
-
jobs
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
const hiddenJobs = jobs.filter(job => filteredJobs.indexOf(job) === -1)
|
|
27
|
-
const hiddenCountsByStatus = _.mapValues(
|
|
28
|
-
_.groupBy(hiddenJobs, 'status'),
|
|
29
|
-
jobsForStatus => jobsForStatus.length
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
const hiddenJobsText = _(hiddenCountsByStatus)
|
|
33
|
-
.toPairs()
|
|
34
|
-
.orderBy(([status]) => status)
|
|
35
|
-
.value()
|
|
36
|
-
.map(([status, count]) => `${count}${NON_BREAKING_SPACE}${status}`)
|
|
37
|
-
.join(', ')
|
|
38
|
-
|
|
39
|
-
return <ol className="jobs">
|
|
40
|
-
{filteredJobs.map(job => <JobElement job={job} key={job.id}/>)}
|
|
41
|
-
{
|
|
42
|
-
hiddenJobs.length > 0 ? <li className="hidden-jobs">+ {hiddenJobsText}</li> : null
|
|
43
|
-
}
|
|
44
|
-
</ol>
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function JobElement({job}: {job: Job}) {
|
|
48
|
-
return <li className={job.status}>
|
|
49
|
-
<a href={job.url} target="_blank" rel="noopener noreferrer">{job.name}</a>
|
|
50
|
-
{!job.url && job.name}
|
|
51
|
-
</li>
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function sortByOriginalOrder(filteredJobs: Job[], jobs: Job[]) {
|
|
55
|
-
return _.orderBy(filteredJobs, (job: Job) => jobs.indexOf(job))
|
|
56
|
-
}
|
package/src/client/projects.tsx
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import _ from 'lodash'
|
|
2
|
-
import {Info} from './info'
|
|
3
|
-
import type {Project} from './gitlab-types'
|
|
4
|
-
import React from 'react'
|
|
5
|
-
import {Stages} from './stages'
|
|
6
|
-
|
|
7
|
-
export function Projects({columns, now, projects, projectsOrder, screen, zoom}: {columns: number, now: number, projects: Project[], projectsOrder: string[], screen: {id: number, total: number}, zoom: number}): JSX.Element {
|
|
8
|
-
return <ol className="projects" style={zoomStyle(zoom)}>
|
|
9
|
-
{_.sortBy(projects, projectsOrder)
|
|
10
|
-
.filter(forScreen(screen, projects.length))
|
|
11
|
-
.map(project => <ProjectElement now={now} columns={columns} project={project} key={project.id}/>)
|
|
12
|
-
}
|
|
13
|
-
</ol>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function ProjectElement({columns, now, project}: {columns: number, now: number, project: Project}) {
|
|
17
|
-
const [pipeline] = project.pipelines
|
|
18
|
-
|
|
19
|
-
return <li className={`project ${project.status}`} style={style(columns)}>
|
|
20
|
-
<h2>
|
|
21
|
-
{project.url && <a href={`${project.url}/pipelines`} target="_blank" rel="noopener noreferrer">{project.name}</a>}
|
|
22
|
-
{!project.url && project.name}
|
|
23
|
-
</h2>
|
|
24
|
-
<Stages stages={pipeline.stages} maxNonFailedJobsVisible={project.maxNonFailedJobsVisible}/>
|
|
25
|
-
<Info now={now} pipeline={pipeline}/>
|
|
26
|
-
</li>
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function forScreen(screen: {id: number, total: number}, projectsCount: number) {
|
|
30
|
-
const perScreen = Math.ceil(projectsCount / screen.total)
|
|
31
|
-
const first = perScreen * (screen.id - 1)
|
|
32
|
-
const last = perScreen * screen.id
|
|
33
|
-
return (_project: Project, projectIndex: number) => projectIndex >= first && projectIndex < last
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function zoomStyle(zoom: number) {
|
|
37
|
-
const widthPercentage = Math.round(100 / zoom)
|
|
38
|
-
return {
|
|
39
|
-
transform: `scale(${zoom})`,
|
|
40
|
-
width: `${widthPercentage}vmax`
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function style(columns: number) {
|
|
45
|
-
const marginPx = 12
|
|
46
|
-
const widthPercentage = Math.floor(100 / columns)
|
|
47
|
-
return {
|
|
48
|
-
margin: `${marginPx}px`,
|
|
49
|
-
width: `calc(${widthPercentage}% - ${2 * marginPx}px)`
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import {formatDistance} from 'date-fns'
|
|
2
|
-
import type {Stage} from './gitlab-types'
|
|
3
|
-
|
|
4
|
-
export function renderTimestamp(stages: Stage[], now: number): string {
|
|
5
|
-
const timestamps = getTimestamps(stages)
|
|
6
|
-
|
|
7
|
-
if (timestamps.length === 0) {
|
|
8
|
-
return 'Pending...'
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const finished = timestamps
|
|
12
|
-
.map(t => t.finishedAt)
|
|
13
|
-
.filter((t): t is number => t !== null)
|
|
14
|
-
const inProgress = timestamps.length > finished.length
|
|
15
|
-
if (inProgress) {
|
|
16
|
-
const [timestamp] = timestamps.sort((a, b) => a.startedAt - b.startedAt)
|
|
17
|
-
return renderDistance('Started', timestamp.startedAt, now)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const [latestFinishedAt] = finished.sort((a, b) => b - a)
|
|
21
|
-
return renderDistance('Finished', latestFinishedAt, now)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function getTimestamps(stages: Stage[]): {startedAt: number, finishedAt: number | null}[] {
|
|
25
|
-
return stages
|
|
26
|
-
.flatMap(s => s.jobs)
|
|
27
|
-
.map(job => {
|
|
28
|
-
const startedAt = job.startedAt ? new Date(job.startedAt).valueOf() : null
|
|
29
|
-
const finishedAt = job.finishedAt ? new Date(job.finishedAt).valueOf() : null
|
|
30
|
-
return {
|
|
31
|
-
startedAt,
|
|
32
|
-
finishedAt
|
|
33
|
-
}
|
|
34
|
-
})
|
|
35
|
-
.filter((t): t is {startedAt: number, finishedAt: number | null} => t.startedAt !== null)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function renderDistance(predicate: string, timestamp: number, now: number) {
|
|
39
|
-
const distance = formatDate(timestamp, now)
|
|
40
|
-
return `${predicate} ${distance} ago`
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function formatDate(timestamp: number, now: number) {
|
|
44
|
-
return formatDistance(new Date(timestamp), new Date(now))
|
|
45
|
-
}
|
package/src/client/stages.tsx
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import {Jobs} from './jobs'
|
|
2
|
-
import React from 'react'
|
|
3
|
-
import type {Stage} from './gitlab-types'
|
|
4
|
-
|
|
5
|
-
export function Stages({stages, maxNonFailedJobsVisible}: {stages: Stage[], maxNonFailedJobsVisible: number}): JSX.Element {
|
|
6
|
-
return <ol className="stages">
|
|
7
|
-
{stages.map(stage =>
|
|
8
|
-
<StageElement stage={stage} maxNonFailedJobsVisible={maxNonFailedJobsVisible} key={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
|
-
}
|
package/src/dev-assets.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import config from '../webpack.dev.js'
|
|
2
|
-
import webpack from 'webpack'
|
|
3
|
-
import webpackDevMiddleware from 'webpack-dev-middleware'
|
|
4
|
-
import webpackHotMiddleware from 'webpack-hot-middleware'
|
|
5
|
-
|
|
6
|
-
export function bindDevAssets(app) {
|
|
7
|
-
const compiler = webpack(config)
|
|
8
|
-
app.use(webpackDevMiddleware(compiler))
|
|
9
|
-
app.use(webpackHotMiddleware(compiler))
|
|
10
|
-
}
|