gitlab-radiator 3.3.11 → 3.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.
@@ -0,0 +1,40 @@
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
+ }
@@ -0,0 +1,59 @@
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
+ }
@@ -0,0 +1,95 @@
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()
@@ -0,0 +1,16 @@
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
+ }
@@ -0,0 +1,56 @@
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">+&nbsp;{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
+ }
@@ -0,0 +1,51 @@
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
+ }
@@ -0,0 +1,45 @@
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
+ }
@@ -0,0 +1,18 @@
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/config.js CHANGED
@@ -1,59 +1,43 @@
1
- "use strict";
2
-
3
- var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
-
5
- Object.defineProperty(exports, "__esModule", {
6
- value: true
7
- });
8
- exports.config = void 0;
9
-
10
- var _assert = _interopRequireDefault(require("assert"));
11
-
12
- var _fs = _interopRequireDefault(require("fs"));
13
-
14
- var _os = _interopRequireDefault(require("os"));
15
-
16
- var _jsYaml = _interopRequireDefault(require("js-yaml"));
17
-
18
- var configFile = expandTilde(process.env.GITLAB_RADIATOR_CONFIG || '~/.gitlab-radiator.yml');
19
-
20
- var yamlContent = _fs.default.readFileSync(configFile, 'utf8');
21
-
22
- var config = validate(_jsYaml.default.load(yamlContent));
23
- exports.config = config;
24
- config.interval = Number(config.interval || 10) * 1000;
25
- config.port = Number(config.port || 3000);
26
- config.zoom = Number(config.zoom || 1.0);
27
- config.columns = Number(config.columns || 1);
28
- config.groupSuccessfulProjects = config.groupSuccessfulProjects || false;
29
- config.projectsOrder = config.projectsOrder || ['name'];
30
- config.gitlabs = config.gitlabs.map(gitlab => {
1
+ import assert from 'assert'
2
+ import fs from 'fs'
3
+ import os from 'os'
4
+ import yaml from 'js-yaml'
5
+
6
+ const configFile = expandTilde(process.env.GITLAB_RADIATOR_CONFIG || '~/.gitlab-radiator.yml')
7
+ const yamlContent = fs.readFileSync(configFile, 'utf8')
8
+ export const config = validate(yaml.load(yamlContent))
9
+
10
+ config.interval = Number(config.interval || 10) * 1000
11
+ config.port = Number(config.port || 3000)
12
+ config.zoom = Number(config.zoom || 1.0)
13
+ config.columns = Number(config.columns || 1)
14
+ config.groupSuccessfulProjects = config.groupSuccessfulProjects || false
15
+ config.projectsOrder = config.projectsOrder || ['name']
16
+ config.gitlabs = config.gitlabs.map((gitlab) => {
31
17
  return {
32
18
  url: gitlab.url,
33
19
  ignoreArchived: gitlab.ignoreArchived === undefined ? true : gitlab.ignoreArchived,
34
20
  maxNonFailedJobsVisible: Number(gitlab.maxNonFailedJobsVisible || 999999),
35
- ca: gitlab.caFile && _fs.default.existsSync(gitlab.caFile, 'utf-8') ? _fs.default.readFileSync(gitlab.caFile) : undefined,
21
+ ca: gitlab.caFile && fs.existsSync(gitlab.caFile, 'utf-8') ? fs.readFileSync(gitlab.caFile) : undefined,
36
22
  'access-token': gitlab['access-token'] || process.env.GITLAB_ACCESS_TOKEN,
37
23
  projects: {
38
24
  excludePipelineStatus: (gitlab.projects || {}).excludePipelineStatus || [],
39
25
  include: (gitlab.projects || {}).include || '',
40
26
  exclude: (gitlab.projects || {}).exclude || ''
41
27
  }
42
- };
43
- });
44
- config.colors = config.colors || {};
28
+ }
29
+ })
30
+ config.colors = config.colors || {}
45
31
 
46
32
  function expandTilde(path) {
47
- return path.replace(/^~($|\/|\\)/, "".concat(_os.default.homedir(), "$1"));
33
+ return path.replace(/^~($|\/|\\)/, `${os.homedir()}$1`)
48
34
  }
49
35
 
50
36
  function validate(cfg) {
51
- _assert.default.ok(cfg.gitlabs, 'Mandatory gitlab properties missing from configuration file');
52
-
53
- cfg.gitlabs.forEach(gitlab => {
54
- _assert.default.ok(gitlab.url, 'Mandatory gitlab url missing from configuration file');
55
-
56
- _assert.default.ok(gitlab['access-token'] || process.env.GITLAB_ACCESS_TOKEN, 'Mandatory gitlab access token missing from configuration (and none present at GITLAB_ACCESS_TOKEN env variable)');
57
- });
58
- return cfg;
59
- }
37
+ assert.ok(cfg.gitlabs, 'Mandatory gitlab properties missing from configuration file')
38
+ cfg.gitlabs.forEach((gitlab) => {
39
+ assert.ok(gitlab.url, 'Mandatory gitlab url missing from configuration file')
40
+ assert.ok(gitlab['access-token'] || process.env.GITLAB_ACCESS_TOKEN, 'Mandatory gitlab access token missing from configuration (and none present at GITLAB_ACCESS_TOKEN env variable)')
41
+ })
42
+ return cfg
43
+ }
@@ -0,0 +1,10 @@
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
+ }
@@ -1,49 +1,27 @@
1
- "use strict";
1
+ import axios from 'axios'
2
+ import https from 'https'
3
+ import url from 'url'
2
4
 
3
- var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
-
5
- Object.defineProperty(exports, "__esModule", {
6
- value: true
7
- });
8
- exports.gitlabRequest = gitlabRequest;
9
-
10
- var _axios = _interopRequireDefault(require("axios"));
11
-
12
- var _https = _interopRequireDefault(require("https"));
13
-
14
- var _url = _interopRequireDefault(require("url"));
15
-
16
- function gitlabRequest(path, params, gitlab) {
17
- return lazyClient(gitlab).get(path, {
18
- params
19
- });
5
+ export function gitlabRequest(path, params, gitlab) {
6
+ return lazyClient(gitlab).get(path, {params})
20
7
  }
21
8
 
22
- var clients = new Map();
9
+ const clients = new Map()
23
10
 
24
11
  function lazyClient(gitlab) {
25
- var gitlabUrl = gitlab.url;
26
-
12
+ const gitlabUrl = gitlab.url
27
13
  if (gitlabUrl === undefined) {
28
14
  // eslint-disable-next-line no-console
29
- console.log('Got undefined url for ' + JSON.stringify(gitlab));
15
+ console.log('Got undefined url for ' + JSON.stringify(gitlab))
30
16
  }
31
-
32
17
  if (!clients.get(gitlabUrl)) {
33
- var client = _axios.default.create({
34
- baseURL: _url.default.resolve(gitlabUrl, '/api/v4/'),
35
- headers: {
36
- 'PRIVATE-TOKEN': gitlab['access-token']
37
- },
38
- httpsAgent: new _https.default.Agent({
39
- keepAlive: true,
40
- ca: gitlab.ca
41
- }),
42
- timeout: 30 * 1000
43
- });
44
-
45
- clients.set(gitlabUrl, client);
18
+ const client = axios.create({
19
+ baseURL: url.resolve(gitlabUrl, '/api/v4/'),
20
+ headers: {'PRIVATE-TOKEN': gitlab['access-token']},
21
+ httpsAgent: new https.Agent({keepAlive: true, ca: gitlab.ca}),
22
+ timeout: 30 * 1000
23
+ })
24
+ clients.set(gitlabUrl, client)
46
25
  }
47
-
48
- return clients.get(gitlabUrl);
49
- }
26
+ return clients.get(gitlabUrl)
27
+ }