diff --git a/.todo b/.todo index 4ddf24a..fb5499f 100644 --- a/.todo +++ b/.todo @@ -7,35 +7,35 @@ team: email: vladislav.kurmaz@gmail.com name: Vladyslav Kurmaz timeline: - "v0.2.0": - date: "2024-09-04" - "v0.3.0": - date: "2024-09-12" - "v0.4.0": - date: "2024-09-15" - "v0.5.0": - date: "2024-09-23" - "v0.6.0": - date: "2024-10-22" - "v0.7.0": - date: "2024-10" + - name: v0.7.0 + date: 2024-10-28 23:55:00 GMT+0200 + - name: v0.6.0 + date: 2024-10-22 17:30:00 GMT+0200 + - name: v0.5.0 + date: 2024-09-23 17:30:00 GMT+0200 + - name: v0.4.0 + date: 2024-09-15 17:30:00 GMT+0200 + - name: v0.3.0 + date: 2024-09-12 17:30:00 GMT+0200 + - name: v0.2.0 + date: 2024-09-04 17:30:00 GMT+0200 tasks: | - [>:022:v0.7.0] Describe command @vlad.k + [+:022:v0.7.0] Describe command - project @vlad.k [+:021:v0.6.0] Add project description section @vlad.k [+:020:v0.5.0] Add option to display tasks with different statuses: --backlog, --indev, --done @vlad.k [+:019:v0.5.0] Add component as optional parameter for ls command @vlad.k - [-:018:v0.7.0] Generate snapshot @vlad.k + [-:018:v0.8.0] Generate snapshot @vlad.k [+:017:v0.5.0] Add SRS section @vlad.k - [-:016:v0.7.0] Add describe command with resolved links @vlad.k + [-:016:v0.8.0] Add describe command with resolved links @vlad.k [x:015:v0.5.0] Integrate .tpm folder at repository root level to store snapshots @vlad.k - [-:014:v0.7.0] Update README.md with Hello World section using repository @vlad.k + [-:014:v0.8.0] Update README.md with Hello World section using repository @vlad.k [+:013:v0.6.0] Add search @vlad.k [+:012:v0.6.0] Add CI/CD @vlad.k [+:011:v0.6.0] Add unit test framework @vlad.k [+:010:v0.6.0] Extract tags, timeline from task description @vlad.k [+:009:v0.5.0] Add command to generate .todo template --team, --timeline, --tasks, --force @vlad.k [+:008:v0.6.0] Merge multiple descriptions from one file @vlad.k - [-:007:v0.7.0] Server command: express server + fs watch @vlad.k + [-:007:v0.8.0] Server command: express server + fs watch @vlad.k [-] Create API using express @vlad.k [-] Implement web part: Dashboard @vlad.k [-] Implement web part: Timeline @vlad.k diff --git a/src/app.js b/src/app.js index 838ab8a..12cd2f3 100644 --- a/src/app.js +++ b/src/app.js @@ -95,6 +95,7 @@ class App { if (c) { if (what.project) { result.projects = await c.describeProject(); + // console.log(result.projects[0].summary.timeline); } } return result; diff --git a/src/component.js b/src/component.js index e27ad3a..b4d9bd8 100644 --- a/src/component.js +++ b/src/component.js @@ -9,7 +9,7 @@ const assign = require('assign-deep'); const sourceFactory = require('./source'); const projectFactory = require('./project'); const memberFactory = require('./member'); -const deadlineFactory = require('./deadline'); +const timelineFactory = require('./timeline'); const taskFactory = require('./task'); const srsFactory = require('./srs'); @@ -29,7 +29,7 @@ class Component { this.sources = []; this.project = []; this.team = null; - this.timeline = null; + this.timeline = []; this.tasks = []; this.srs = null; this.components = []; @@ -117,8 +117,12 @@ class Component { result |= true; } if (data.timeline) { - this.timeline = assign({}, data.timeline); - result |= true; + if (data.timeline.length) { + const timeline = timelineFactory.create(this.logger, source); + await timeline.load(data.timeline); + this.timeline.push(timeline); + result |= true; + } } if (data.tasks) { const task = taskFactory.create(this.logger, source); @@ -187,6 +191,8 @@ class Component { } // timeline if (what.timeline && this.timeline) { + summary.timeline = summary.timeline.concat(await timeline.getSummary(0)); + if (Object.keys(this.timeline).length) { this.logger.con((require('yaml')).stringify({ timeline: this.timeline })); } @@ -253,7 +259,8 @@ class Component { project = assign(project, p); }); let summary = { - tasks: { todo: 0, indev: 0, tbd: 0, blocked: 0, done: 0, dropped: 0 } + tasks: { todo: 0, indev: 0, tbd: 0, blocked: 0, done: 0, dropped: 0 }, + timeline: [] }; project.summary = await this.getSummary(summary); project.summary.team = this.getTeam({}, false, true); @@ -266,6 +273,15 @@ class Component { } async getSummary(summary) { + for (const timeline of this.timeline) { + summary.timeline = summary.timeline.concat(await timeline.getSummary({features: 0})); + } + for (const deadline of summary.timeline) { + const tsks = await Promise.all(this.tasks.map(async t => t.getCountByDeadlime(deadline.name))); + const cnt = tsks.reduce((acc, c) => acc + c, 0); + deadline.features += cnt; + } + // for (const task of this.tasks) { summary.tasks = await task.getSummary(summary.tasks); }; diff --git a/src/deadline.js b/src/deadline.js deleted file mode 100644 index dde9f45..0000000 --- a/src/deadline.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -class Timeline { - - /* - * - * params: - */ - constructor(logger) { - this.logger = logger; - } - -} - -module.exports.create = (logger) => { - return new Timeline(logger); -} diff --git a/src/server.js b/src/server.js index 858316d..c1e888e 100644 --- a/src/server.js +++ b/src/server.js @@ -10,6 +10,7 @@ const yaml = require('js-yaml'); const utils = require('./utils'); +const {version} = require('../package.json'); class Server { @@ -43,6 +44,10 @@ class Server { ea.get('/main.js', (req, res) => { res.send(getLocalContent('main.js')); }) + // API + ea.get('/info', (req, res) => { + res.send(this.makeResponce({version})); + }) ea.get('/teams', (req, res) => { res.send(this.makeResponce(root.getTeam({}, true, true))); }) @@ -62,7 +67,7 @@ class Server { }) ea.listen(port, () => { - this.logger.con(`start server on http://localhost:${port} in ${readOnly?'read-only':'read-write'} mode`); + this.logger.con(`start server version ${version} on http://localhost:${port} in ${readOnly?'read-only':'read-write'} mode`); }) } diff --git a/src/task.js b/src/task.js index 056848b..5436e11 100644 --- a/src/task.js +++ b/src/task.js @@ -101,6 +101,11 @@ class Task { return tasksSummary; } + async getCountByDeadlime(deadline) { + const st = await Promise.all(this.tasks.map(async t => t.getCountByDeadlime(deadline))); + return this.deadline === deadline ? 1 : 0 + st.reduce((acc, c) => acc + c, 0); + } + } module.exports.create = (logger, source) => { diff --git a/src/timeline.js b/src/timeline.js new file mode 100644 index 0000000..c8908d5 --- /dev/null +++ b/src/timeline.js @@ -0,0 +1,25 @@ +'use strict'; + +const assign = require('assign-deep'); + +class Timeline { + + constructor(logger, source) { + this.logger = logger; + this.source = source; + this.deadlines = []; + } + + async load(data) { + this.deadlines = data.map(r => assign({}, r)); + } + + async getSummary(options) { + return this.deadlines.map(r => { return { ...assign({}, r), ...options } }); + } + +} + +module.exports.create = (logger, source) => { + return new Timeline(logger, source); +} diff --git a/src/deadline.spec.js b/src/timeline.spec.js similarity index 64% rename from src/deadline.spec.js rename to src/timeline.spec.js index 5d522c9..5c8b0c0 100644 --- a/src/deadline.spec.js +++ b/src/timeline.spec.js @@ -1,6 +1,6 @@ const chai = require('chai'); -const deadlineFactory = require('./deadline'); +const timelineFactory = require('./timeline'); const { expect } = chai; @@ -9,7 +9,7 @@ const logger = require('./logger').create(0); describe('Timeline entity', function () { it('can be created', function () { - expect(deadlineFactory.create(logger)).not.to.be.null; + expect(timelineFactory.create(logger)).not.to.be.null; }); }); \ No newline at end of file diff --git a/web/index.html b/web/index.html index 75e2c4b..499f1da 100644 --- a/web/index.html +++ b/web/index.html @@ -24,7 +24,7 @@ -
- - +
+

+ +

@@ -97,8 +78,8 @@
-
-
+
+
diff --git a/web/main.js b/web/main.js index f95dc78..4bcd892 100644 --- a/web/main.js +++ b/web/main.js @@ -4,17 +4,32 @@ google.charts.setOnLoadCallback(drawChart); let projects = []; let timeline = {}; -let team = {}; +let teams = {}; + +const colorsRAG = { + red: '#c45850', //'#dc3545', + amber: '#e8c3b9', //'#ffc107', + green: '#3cba9f' //'#28a745' +}; //----------------------------------------------------------------------------- // Initialisation $(document).ready(function(){ - updateTeam(); - //updateTimeline(); - initDashboard(); - updateDashboard(); + // get general information + $.getJSON("info", function(res, status){ + if (res.success) { + $('#nav_version').text(res.data.version); + } + }); + // + initTeam(); updateTeam(); + //initTimeline(); updateTimeline(); + initDashboard(); updateDashboard(); // }); +//----------------------------------------------------------------------------- +// Utils + //----------------------------------------------------------------------------- // Dashboard @@ -22,9 +37,9 @@ $("#dashboard-tab").click(function(){ updateDashboard(); }); -function getProject(id, name, lastCommit) { +function getProject(id, name, summary) { const r = { ids: { tasks: `project-${id}-tasks`, workload: `project-${id}-workload` } }; - const lut = dateFns.fp.intervalToDuration({start: new Date(lastCommit), end: new Date() }); + const lut = dateFns.fp.intervalToDuration({start: new Date(summary.lastCommit), end: new Date() }); let diff = '?'; if (lut.years) { diff = `${lut.years}y`; @@ -39,7 +54,11 @@ function getProject(id, name, lastCommit) { } else if (lut.seconds) { diff = `${lut.seconds}s`; } - let lastUpdateTime = `Updated ${diff} ago`; + const lastUpdateTime = `Updated ${diff} ago`; + + // const rd = dateFns.fp.intervalToDuration({start: new Date(summary.release.date), end: new Date() }); + // console.log(rd); + r.html = '' + '
' + @@ -50,11 +69,11 @@ function getProject(id, name, lastCommit) { '
' + '
' + `
` + - `
` + - '
' + + `
` + + '
' + ` ` + '
' + - '
' + + '
' + ` ` + '
' + '
' + @@ -66,22 +85,32 @@ function getProject(id, name, lastCommit) { return r; } - - function getProjectDetails(description, summary) { const r = {}; + // release + let releaseName = 'n/a'; + let releaseDate = 'n/a'; + let releaseFeatures = 'n/a'; + if (summary.timeline.length) { + releaseName = summary.timeline[0].name; + releaseDate = summary.timeline[0].date; + releaseFeatures = summary.timeline[0].features; + } r.html = '' + '
' + - '
' + + '
' + `
${description}
` + '
' + '
    ' + '
  • Release
  • ' + '
  • ' + - ' name24.10.0' + + ` name${releaseName}` + + '
  • ' + + '
  • ' + + ` date${releaseDate}` + '
  • ' + '
  • ' + - ' date24.10.27' + + ` features${releaseFeatures}` + '
  • ' + '
  • Workload & Bandwidth
  • ' + '
  • ' + @@ -119,7 +148,7 @@ function initDashboard() { projects = res.data.projects; if (projects) { projects.forEach(function(p) { - const proj = getProject(p.id, p.name, p.summary.lastCommit); + const proj = getProject(p.id, p.name, p.summary); list.append(proj.html); const details = getProjectDetails(p.description, p.summary); list.append(details.html); @@ -157,7 +186,8 @@ function initDashboard() { data: { datasets: [ { - backgroundColor: ["#3cba9f", "#3e95cd", "#8e5ea2"], + // backgroundColor: ["#3cba9f", "#3e95cd", "#8e5ea2"], + backgroundColor: [colorsRAG.green, colorsRAG.amber, colorsRAG.red], data: [p.summary.tasks.indev,p.summary.tasks.todo,p.summary.tasks.tbd+p.summary.tasks.blocked] } ] @@ -233,6 +263,10 @@ function updateDashboard() { //----------------------------------------------------------------------------- // Timeline +$("#timeline-tab").click(function(){ + updateTimeline(); +}); +var ganttChart = null; function daysToMilliseconds(days) { return days * 24 * 60 * 60 * 1000; @@ -271,9 +305,10 @@ function drawChart() { } }; - var chart = new google.visualization.Gantt(document.getElementById('chart_div')); - - chart.draw(data, options); + if (!ganttChart) { + ganttChart = new google.visualization.Gantt(document.getElementById('timeline_gantt_chart')); + } + ganttChart.draw(data, options); } function updateTimeline() { @@ -301,14 +336,18 @@ function getMember(id, name, email) { ''; } -function updateTeam() { +function initTeam() { $.getJSON("teams", function(res, status){ if (res.success) { + teams = res.data; var list = $('#dashboard_team_list'); list.empty(); - Object.keys(res.data).forEach(function(v){ - list.append(getMember(v, res.data[v].name, res.data[v].email)); + Object.keys(teams).forEach(function(v){ + list.append(getMember(v, teams[v].name, teams[v].email)); }); } }); } + +function updateTeam() { +}