gitlab-radiator 3.3.8 → 3.3.9

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/.babelrc ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "presets": ["@babel/env", "@babel/react"],
3
+ "plugins": ["@babel/plugin-transform-runtime"]
4
+ }
package/.eslintrc ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "env": {
3
+ "es6": true,
4
+ "mocha": true,
5
+ "node": true
6
+ },
7
+ "parser": "@babel/eslint-parser",
8
+ "plugins": [
9
+ "mocha",
10
+ "react"
11
+ ],
12
+ "extends": ["eslint:recommended"]
13
+ }
@@ -0,0 +1,20 @@
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 ADDED
@@ -0,0 +1 @@
1
+ 14.17.0
package/build-npm ADDED
@@ -0,0 +1,19 @@
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/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "email": "heikki.pora@gmail.com"
6
6
  },
7
7
  "description": "The missing GitLab build radiator view",
8
- "version": "3.3.8",
8
+ "version": "3.3.9",
9
9
  "license": "MIT",
10
10
  "bin": {
11
11
  "gitlab-radiator": "bin/gitlab-radiator"
@@ -38,52 +38,52 @@
38
38
  }
39
39
  ],
40
40
  "dependencies": {
41
- "@babel/runtime": "7.15.4",
42
- "axios": "0.23.0",
41
+ "@babel/runtime": "7.16.3",
42
+ "axios": "0.24.0",
43
43
  "basic-auth": "2.0.1",
44
44
  "compression": "1.7.4",
45
- "date-fns": "2.25.0",
45
+ "date-fns": "2.27.0",
46
46
  "esm": "3.2.25",
47
47
  "express": "4.17.1",
48
48
  "js-yaml": "4.1.0",
49
49
  "less-middleware": "3.1.0",
50
50
  "lodash": "4.17.21",
51
- "socket.io": "4.2.0"
51
+ "socket.io": "4.4.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@babel/cli": "7.15.7",
55
- "@babel/core": "7.15.8",
56
- "@babel/node": "7.15.8",
57
- "@babel/plugin-transform-runtime": "7.15.8",
58
- "@babel/preset-env": "7.15.8",
59
- "@babel/preset-react": "7.14.5",
60
- "@babel/register": "7.15.3",
61
- "@types/lodash": "4.14.175",
62
- "@types/react": "17.0.28",
63
- "@types/react-dom": "17.0.9",
64
- "@types/webpack-env": "1.16.2",
65
- "@typescript-eslint/eslint-plugin": "5.0.0",
66
- "@typescript-eslint/parser": "5.0.0",
67
- "babel-eslint": "10.1.0",
68
- "babel-loader": "8.2.2",
54
+ "@babel/cli": "7.16.0",
55
+ "@babel/core": "7.16.0",
56
+ "@babel/node": "7.16.0",
57
+ "@babel/plugin-transform-runtime": "7.16.4",
58
+ "@babel/preset-env": "7.16.4",
59
+ "@babel/preset-react": "7.16.0",
60
+ "@babel/register": "7.16.0",
61
+ "@types/lodash": "4.14.177",
62
+ "@types/react": "17.0.37",
63
+ "@types/react-dom": "17.0.11",
64
+ "@types/webpack-env": "1.16.3",
65
+ "@typescript-eslint/eslint-plugin": "5.5.0",
66
+ "@typescript-eslint/parser": "5.5.0",
67
+ "@babel/eslint-parser": "7.16.3",
68
+ "babel-loader": "8.2.3",
69
69
  "chai": "4.3.4",
70
- "css-loader": "6.4.0",
71
- "eslint": "7.32.0",
70
+ "css-loader": "6.5.1",
71
+ "eslint": "8.3.0",
72
72
  "eslint-plugin-mocha": "9.0.0",
73
- "eslint-plugin-react": "7.26.1",
73
+ "eslint-plugin-react": "7.27.1",
74
74
  "less": "4.1.2",
75
- "less-loader": "10.1.0",
76
- "mocha": "9.1.2",
75
+ "less-loader": "10.2.0",
76
+ "mocha": "9.1.3",
77
77
  "normalize.css": "8.0.1",
78
78
  "react": "17.0.2",
79
79
  "react-dom": "17.0.2",
80
- "sinon": "11.1.2",
81
- "style-loader": "3.3.0",
80
+ "sinon": "12.0.1",
81
+ "style-loader": "3.3.1",
82
82
  "ts-loader": "9.2.6",
83
- "typescript": "4.4.3",
84
- "webpack": "5.58.1",
85
- "webpack-cli": "4.9.0",
86
- "webpack-dev-middleware": "5.2.1",
83
+ "typescript": "4.5.2",
84
+ "webpack": "5.64.4",
85
+ "webpack-cli": "4.9.1",
86
+ "webpack-dev-middleware": "5.2.2",
87
87
  "webpack-hot-middleware": "2.25.1",
88
88
  "webpack-merge": "5.8.0"
89
89
  },
package/screenshot.png ADDED
Binary file
package/src/app.js CHANGED
@@ -1,187 +1,100 @@
1
- "use strict";
2
-
3
- var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
-
5
- var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
6
-
7
- var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
8
-
9
- var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
10
-
11
- var _auth = require("./auth");
12
-
13
- var _compression = _interopRequireDefault(require("compression"));
14
-
15
- var _config = require("./config");
16
-
17
- var _express = _interopRequireDefault(require("express"));
18
-
19
- var _runners = require("./gitlab/runners");
20
-
21
- var _http = _interopRequireDefault(require("http"));
22
-
23
- var _lessMiddleware = _interopRequireDefault(require("less-middleware"));
24
-
25
- var _os = _interopRequireDefault(require("os"));
26
-
27
- var _path = _interopRequireDefault(require("path"));
28
-
29
- var _socket = _interopRequireDefault(require("socket.io"));
30
-
31
- var _gitlab = require("./gitlab");
32
-
33
- function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
34
-
35
- function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
36
-
37
- var cacheDir = _path.default.join(_os.default.tmpdir(), 'gitlab-radiator-css-cache');
38
-
39
- var app = (0, _express.default)();
40
-
41
- var httpServer = _http.default.Server(app);
42
-
43
- var socketIoServer = (0, _socket.default)(httpServer);
1
+ import {basicAuth} from './auth'
2
+ import compression from 'compression'
3
+ import {config} from './config'
4
+ import express from 'express'
5
+ import {fetchOfflineRunners} from './gitlab/runners'
6
+ import http from 'http'
7
+ import lessMiddleware from 'less-middleware'
8
+ import os from 'os'
9
+ import path from 'path'
10
+ import socketIo from 'socket.io'
11
+ import {update} from './gitlab'
12
+
13
+ const cacheDir = path.join(os.tmpdir(), 'gitlab-radiator-css-cache')
14
+
15
+ const app = express()
16
+ const httpServer = http.Server(app)
17
+ const socketIoServer = socketIo(httpServer)
44
18
 
45
19
  if (process.env.NODE_ENV !== 'production') {
46
20
  // eslint-disable-next-line global-require
47
- var _require = require('./dev-assets'),
48
- bindDevAssets = _require.bindDevAssets;
49
-
50
- bindDevAssets(app);
21
+ const {bindDevAssets} = require('./dev-assets')
22
+ bindDevAssets(app)
51
23
  }
52
24
 
53
- app.disable('x-powered-by');
54
- app.use((0, _lessMiddleware.default)("".concat(__dirname, "/../public"), {
55
- dest: cacheDir,
56
- preprocess: {
57
- less: src => {
58
- var colorLess = '';
59
- Object.keys(_config.config.colors).forEach(stateName => {
60
- colorLess += "@".concat(stateName, "-color:").concat(_config.config.colors[stateName], ";");
61
- });
62
- return src + colorLess;
25
+ app.disable('x-powered-by')
26
+ app.use(lessMiddleware(`${__dirname}/../public`, {
27
+ dest: cacheDir,
28
+ preprocess: {
29
+ less: (src) => {
30
+ let colorLess = ''
31
+ Object.keys(config.colors).forEach((stateName) => {
32
+ colorLess += `@${stateName}-color:${config.colors[stateName]};`
33
+ })
34
+ return src + colorLess
35
+ }
63
36
  }
64
37
  }
65
- }));
66
- app.use(_express.default.static(cacheDir));
67
- app.use(_express.default.static("".concat(__dirname, "/../public")));
68
- app.use((0, _compression.default)());
69
- app.use((0, _auth.basicAuth)(_config.config.auth));
70
- httpServer.listen(_config.config.port, () => {
38
+ ))
39
+ app.use(express.static(cacheDir))
40
+ app.use(express.static(`${__dirname}/../public`))
41
+ app.use(compression())
42
+ app.use(basicAuth(config.auth))
43
+
44
+ httpServer.listen(config.port, () => {
71
45
  // eslint-disable-next-line no-console
72
- console.log("Listening on port *:".concat(_config.config.port));
73
- });
74
- var globalState = {
46
+ console.log(`Listening on port *:${config.port}`)
47
+ })
48
+
49
+ const globalState = {
75
50
  projects: null,
76
51
  error: null,
77
- zoom: _config.config.zoom,
78
- projectsOrder: _config.config.projectsOrder,
79
- columns: _config.config.columns,
80
- groupSuccessfulProjects: _config.config.groupSuccessfulProjects
81
- };
82
- socketIoServer.on('connection', socket => {
83
- socket.emit('state', withDate(globalState));
84
- });
85
-
86
- function runUpdate() {
87
- return _runUpdate.apply(this, arguments);
88
- }
89
-
90
- function _runUpdate() {
91
- _runUpdate = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() {
92
- return _regenerator.default.wrap(function _callee$(_context) {
93
- while (1) {
94
- switch (_context.prev = _context.next) {
95
- case 0:
96
- _context.prev = 0;
97
- _context.next = 3;
98
- return (0, _gitlab.update)(_config.config);
99
-
100
- case 3:
101
- globalState.projects = _context.sent;
102
- _context.next = 6;
103
- return errorIfRunnerOffline();
104
-
105
- case 6:
106
- globalState.error = _context.sent;
107
- socketIoServer.emit('state', withDate(globalState));
108
- _context.next = 15;
109
- break;
110
-
111
- case 10:
112
- _context.prev = 10;
113
- _context.t0 = _context["catch"](0);
114
- // eslint-disable-next-line no-console
115
- console.error(_context.t0.message);
116
- globalState.error = "Failed to communicate with GitLab API: ".concat(_context.t0.message);
117
- socketIoServer.emit('state', withDate(globalState));
118
-
119
- case 15:
120
- setTimeout(runUpdate, _config.config.interval);
121
-
122
- case 16:
123
- case "end":
124
- return _context.stop();
125
- }
126
- }
127
- }, _callee, null, [[0, 10]]);
128
- }));
129
- return _runUpdate.apply(this, arguments);
52
+ zoom: config.zoom,
53
+ projectsOrder: config.projectsOrder,
54
+ columns: config.columns,
55
+ groupSuccessfulProjects: config.groupSuccessfulProjects
130
56
  }
131
57
 
132
- function errorIfRunnerOffline() {
133
- return _errorIfRunnerOffline.apply(this, arguments);
58
+ socketIoServer.on('connection', (socket) => {
59
+ socket.emit('state', withDate(globalState))
60
+ })
61
+
62
+ async function runUpdate() {
63
+ try {
64
+ globalState.projects = await update(config)
65
+ globalState.error = await errorIfRunnerOffline()
66
+ socketIoServer.emit('state', withDate(globalState))
67
+ } catch (error) {
68
+ // eslint-disable-next-line no-console
69
+ console.error(error.message)
70
+ globalState.error = `Failed to communicate with GitLab API: ${error.message}`
71
+ socketIoServer.emit('state', withDate(globalState))
72
+ }
73
+ setTimeout(runUpdate, config.interval)
134
74
  }
135
75
 
136
- function _errorIfRunnerOffline() {
137
- _errorIfRunnerOffline = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2() {
138
- var offlineRunnersPerGitlab, _offlineRunnersPerGit, offline, totalCount, names, counts;
139
-
140
- return _regenerator.default.wrap(function _callee2$(_context2) {
141
- while (1) {
142
- switch (_context2.prev = _context2.next) {
143
- case 0:
144
- _context2.next = 2;
145
- return Promise.all(_config.config.gitlabs.map(_runners.fetchOfflineRunners));
146
-
147
- case 2:
148
- offlineRunnersPerGitlab = _context2.sent;
149
- _offlineRunnersPerGit = offlineRunnersPerGitlab.reduce((acc, runner) => {
150
- return {
151
- offline: acc.offline.concat(runner.offline),
152
- totalCount: acc.totalCount + runner.totalCount
153
- };
154
- }, {
155
- offline: [],
156
- totalCount: 0
157
- }), offline = _offlineRunnersPerGit.offline, totalCount = _offlineRunnersPerGit.totalCount;
158
-
159
- if (!(offline.length > 0)) {
160
- _context2.next = 8;
161
- break;
162
- }
163
-
164
- names = offline.map(r => r.name).sort().join(', ');
165
- counts = offline.length === totalCount ? 'All' : "".concat(offline.length, "/").concat(totalCount);
166
- return _context2.abrupt("return", "".concat(counts, " runners offline: ").concat(names));
167
-
168
- case 8:
169
- return _context2.abrupt("return", null);
76
+ async function errorIfRunnerOffline() {
77
+ const offlineRunnersPerGitlab = await Promise.all(config.gitlabs.map(fetchOfflineRunners))
78
+ const {offline, totalCount} = offlineRunnersPerGitlab.reduce((acc, runner) => {
79
+ return {
80
+ offline: acc.offline.concat(runner.offline),
81
+ totalCount: acc.totalCount + runner.totalCount
82
+ }
83
+ }, {offline: [], totalCount: 0})
170
84
 
171
- case 9:
172
- case "end":
173
- return _context2.stop();
174
- }
175
- }
176
- }, _callee2);
177
- }));
178
- return _errorIfRunnerOffline.apply(this, arguments);
85
+ if (offline.length > 0) {
86
+ const names = offline.map(r => r.name).sort().join(', ')
87
+ const counts = offline.length === totalCount ? 'All' : `${offline.length}/${totalCount}`
88
+ return `${counts} runners offline: ${names}`
89
+ }
90
+ return null
179
91
  }
180
92
 
181
- runUpdate();
93
+ runUpdate()
182
94
 
183
95
  function withDate(state) {
184
- return _objectSpread(_objectSpread({}, state), {}, {
96
+ return {
97
+ ...state,
185
98
  now: Date.now()
186
- });
187
- }
99
+ }
100
+ }
package/src/auth.js CHANGED
@@ -1,33 +1,21 @@
1
- "use strict";
1
+ import authenticate from 'basic-auth'
2
2
 
3
- var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
-
5
- Object.defineProperty(exports, "__esModule", {
6
- value: true
7
- });
8
- exports.basicAuth = basicAuth;
9
-
10
- var _basicAuth = _interopRequireDefault(require("basic-auth"));
11
-
12
- function basicAuth(auth) {
3
+ export function basicAuth(auth) {
13
4
  if (!auth || !auth.username || !auth.password) {
14
5
  // eslint-disable-next-line no-console
15
- console.log('No authentication configured');
16
- return (req, res, next) => next();
17
- } // eslint-disable-next-line no-console
6
+ console.log('No authentication configured')
7
+ return (req, res, next) => next()
8
+ }
18
9
 
19
-
20
- console.log('HTTP basic auth enabled');
10
+ // eslint-disable-next-line no-console
11
+ console.log('HTTP basic auth enabled')
21
12
  return (req, res, next) => {
22
- var _ref = (0, _basicAuth.default)(req) || {},
23
- name = _ref.name,
24
- pass = _ref.pass;
25
-
13
+ const {name, pass} = authenticate(req) || {}
26
14
  if (auth.username === name && auth.password === pass) {
27
- next();
15
+ next()
28
16
  } else {
29
- res.setHeader('WWW-Authenticate', 'Basic realm="gitlab-radiator"');
30
- res.status(401).end();
17
+ res.setHeader('WWW-Authenticate', 'Basic realm="gitlab-radiator"')
18
+ res.status(401).end()
31
19
  }
32
- };
33
- }
20
+ }
21
+ }
@@ -0,0 +1,21 @@
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
+ }
@@ -0,0 +1,75 @@
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
+ }
@@ -0,0 +1,54 @@
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'
@@ -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
+ }