...
 
Commits (9)
......@@ -2,3 +2,5 @@
node_modules/
dist/
npm-debug.log
config/dev.env.js
config/prod.env.js
The MIT License (MIT)
Copyright (c) 2016 Pierre-Alexandre Clorichel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# gitlab-gantt
# GitLab Gantt
> A Gantt chart for GitLab
An easy to use, painless and fully functional Gantt chart for GitLab.
## Build Setup
![GitLab Gantt](preview.png)
## Run it now!
Whether you have Docker installed on your local computer ([download and install it](https://www.docker.com/products/docker)), you can try _GitLab Gantt_ right now in four easy steps:
1. `git clone https://gitlab.com/clorichel/gitlab-gantt` will clone this repository
1. `cd gitlab-gantt && ./bashInDevEnv.sh` to connect to the development environment
1. `./bootstrapIt.sh` within the dev environment. You will be prompted for your GitLab instance URL and your Private Token (Wanna try with your GitLab.com account? Your account Private Token [is here](https://gitlab.com/profile/account)!)
1. `npm run dev`: run _GitLab Gantt_ right now, from the dev environment
Enjoy it on http://localhost:8080/! You may also read about the long version through [configuring](#configuring) and [installing](#installing).
## How it works
_GitLab Gantt_ is a **frontend only** application. It leverages [GitLab awesome API](https://gitlab.com/help/api/README.md) to read your issues, before simply displaying a gantt chart with it.
The automatically generated [gantt chart](https://en.wikipedia.org/wiki/Gantt_chart) will display each of your issues within a "date area": from a **start date**, to the **due date**. For each issue, the **default start date** is read from the issue creation date. As with GitLab you are not forced to fill in a due date for your issues, the _GitLab Gantt_ **default due date** will be set to the day after the issue creation date, faking all your issues having to be done in one day. For sure, if you insert a due date in your issues, it will be read automatically.
To give you maximum control over your issues and tasks management practices in the gantt chart, you can override this default values **right from your issue description** with two simple [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Calendar_dates) `YYYY-MM-DD` calendar dates:
```
GanttStart: 2016-09-26
GanttDue: 2016-10-25
```
The values of `GanttStart` and `GanttDue`, each on one dedicated line of your issue description (whether on top or bottom of the description), are considered as the _single source of truth_ if present to generate the gantt chart.
Only your opened issues are displayed. If an issue due date is past, the issue is marked late and **colored red**. If not, the issue is **shown green** as being on time.
## Configuring
The `./bootstrapIt.sh` script may already have configured the values prompted for you. See the [config](config) folder `*.example.js` files to persist your configuration for:
| Key | Description |
|------------------------|------------------------------------------------------------------------------------------------------------|
| `GITLAB_URL` | your GitLab instance URL (defaults to https://gitlab.com) |
| `GITLAB_PRIVATE_TOKEN` | your GitLab private token use to connect to the API |
| `MOMENTJS_LOCALE` | Moment.js [locale configuration](http://momentjs.com/docs/#/i18n/) to display dates in your usual language |
| `GANTT_START_STRING` | defaults to `GanttStart: `, the string to search on you issue description as the gantt start date |
| `GANTT_DUE_STRING` | defaults to `GanttDue: `, the string to search on you issue description as the gantt due date |
## Installing
Even if _GitLab Gantt_ is a throw-away application **storing no data** on your hard drive, you can install it on a server to persist the gantt graph display service for your users.
Initial scaffolding was done with [vue-cli](https://github.com/vuejs/vue-cli) webpack template. For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). Simply said, this makes _GitLab Gantt_ insanely powerful and ready to be run on production servers, or launched from your local computer within minutes:
``` bash
# install dependencies
......@@ -15,4 +62,28 @@ npm run dev
npm run build
```
For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
## What's next?
Without any obligation nor due date, one could expect this non-exhaustive list of improvements:
- refining of the overall application presentation and initial behavior
- specific visible axis indicating "today"
- unlimited issues on the gantt graph
- exhaustive select pickers with search for groups and projects
- links to issues from the gantt chart
- filtering on issues tags and assigned users
## Support
Your are free to [open an issue](https://gitlab.com/clorichel/gitlab-gantt/issues/new) right in this GitLab repository whether you should be facing a problem or a bug. Please advise this is not a commercial product, so one could experience random response time. Positive, friendly and productive conversations are expected on the issues. Screenshots and steps to reproduce are highly appreciated. Chances are you may get your issue solved if you follow these simple guidelines.
## Credits
- Florian Roscheck for his awesome work on https://github.com/flrs/visavail
- D3.js https://d3js.org/
- Moment.js http://momentjs.com/
## License
The _GitLab Gantt_ application is distributed under the [MIT License (MIT)](LICENSE). Please have a look at the dependencies licenses if you plan on using, building, or distributing this application.
\ No newline at end of file
#!/usr/bin/env bash
docker run -it -p 8080:8080 --rm -w /webapp -v $(pwd):/webapp node:6 /bin/bash
\ No newline at end of file
#!/usr/bin/env bash
echo -n "Please type in your GitLab instance URL (default: https://gitlab.com): "
read GITLAB_URL
if [ -z "${GITLAB_URL}" ]; then
GITLAB_URL=https://gitlab.com
fi
echo -n "Now provide your GitLab Private Token: "
read GITLAB_TOKEN
if [ -z "${GITLAB_TOKEN}" ]; then
echo "You need to provide a private token to connect to your GitLab instance API"
exit 1
fi
# copying default configuration files without overwritting
cp -n config/dev.env.example.js config/dev.env.js
cp -n config/prod.env.example.js config/prod.env.js
# filling in ./config/dev.env.js with typed values
sed -i "s/^\(.*GITLAB_URL:\s\).*$/\1'\"${GITLAB_URL//\//\\/}\"',/" ./config/dev.env.js
sed -i "s/^\(.*GITLAB_PRIVATE_TOKEN:\s\).*$/\1'\"${GITLAB_TOKEN//\//\\/}\"',/" ./config/dev.env.js
# installing node dependencies
npm install
\ No newline at end of file
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
// Place your GitLab instance URL here
GITLAB_URL: '"https://gitlab.com"',
// Your GitLab private token
GITLAB_PRIVATE_TOKEN: '"place your GitLab private token here"',
// Moment.js locale configuration
MOMENTJS_LOCALE: '"en"',
// You are free to configure any string for gantt start/due dates,
// which are read in your issues descriptions
GANTT_START_STRING: '"GanttStart: "',
GANTT_DUE_STRING: '"GanttDue: "',
})
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})
module.exports = {
NODE_ENV: '"production"',
// Place your GitLab instance URL here
GITLAB_URL: '"https://gitlab.com"',
// Your GitLab private token
GITLAB_PRIVATE_TOKEN: '"place your GitLab private token here"',
// Moment.js locale configuration
MOMENTJS_LOCALE: '"en"',
// You are free to configure any string for gantt start/due dates,
// which are read in your issues descriptions
GANTT_START_STRING: '"GanttStart: "',
GANTT_DUE_STRING: '"GanttDue: "',
}
\ No newline at end of file
module.exports = {
NODE_ENV: '"production"'
}
......@@ -2,7 +2,8 @@
<html>
<head>
<meta charset="utf-8">
<title>gitlab-gantt</title>
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<title>GitLab Gantt</title>
</head>
<body>
<div id="app"></div>
......
......@@ -10,7 +10,11 @@
"lint": "eslint --ext .js,.vue src"
},
"dependencies": {
"vue": "^2.0.1"
"d3": "^3.5.17",
"font-awesome": "^4.6.3",
"moment": "^2.15.1",
"vue": "^2.0.1",
"vue-resource": "^1.0.3"
},
"devDependencies": {
"autoprefixer": "^6.4.0",
......
preview.png

73.5 KB

<template>
<div id="app">
<img src="./assets/logo.png">
<hello></hello>
<p v-if="connected == -1"><i class="fa fa-circle-o-notch fa-spin" aria-hidden="true"></i> Connecting to GitLab...</p>
<p v-if="connected == false">Unable to connect to GitLab!</p>
<p v-if="currentUser">Connected to GitLab as <code>{{ currentUser.username }}</code>.</p>
<selectorWrapper v-bind:currentUser="currentUser" v-bind:GitLab="GitLab"></selectorWrapper>
</div>
</template>
<script>
import Hello from './components/Hello'
import SelectorWrapper from './components/SelectorWrapper'
import 'font-awesome/css/font-awesome.css'
export default {
name: 'app',
data () {
return {
GitLab: null,
currentUser: null,
connected: -1
}
},
components: {
Hello
SelectorWrapper
},
methods: {
getCurrentUser: function (event) {
this.$http.get(
this.GitLab + '/user',
{
headers: { 'PRIVATE-TOKEN': process.env.GITLAB_PRIVATE_TOKEN }
}
).then((response) => {
// Assigning response body directly to this.groups
this.currentUser = response.body
this.connected = true
}, (response) => {
this.connected = false
})
}
},
mounted: function () {
this.GitLab = process.env.GITLAB_URL + '/api/v3'
this.getCurrentUser()
}
}
</script>
......@@ -21,8 +51,6 @@ export default {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
This diff is collapsed.
<template>
<div v-if="this.currentUser">
<!-- Main filter selector -->
<p>List issues
<input type="radio" id="inputListByGroup" value="group" v-model="listBy" v-on:change="listByGroup"><label for="inputListByGroup"> by groups and projects</label>,
or <input type="radio" id="inputListByProject" value="project" v-model="listBy" v-on:change="listByProject"><label for="inputListByProject"> by projects</label>,
or <input type="radio" id="inputListByMe" value="me" v-model="listBy" v-on:change="listByMe"><label for="inputListByMe"> all issues created by me</label></p>
<!-- User wants to list by groups -->
<p v-if="this.listBy === 'group'">Groups you have access:
<select title="group" v-model="group" v-on:change="selectedGroup">
<option v-for="aGroup in groups" v-bind:value="aGroup">
{{ aGroup.path }}
</option>
</select>
/
<select title="group project" v-model="gProject" v-on:change="selectedGroupProject">
<option v-for="aGroupProject in groupProjects" v-bind:value="aGroupProject">
{{ aGroupProject.path }}
</option>
</select>
</p>
<!-- User wants to list by projects -->
<p v-if="this.listBy === 'project'">
Select a project: <select title="project" v-model="project" v-on:change="listProjectIssues">
<option v-for="aProject in projects" v-bind:value="aProject">
{{ aProject.path_with_namespace }}
</option>
</select>
</p>
<!-- Currently downloading? -->
<p v-if="this.downloading === true"><i class="fa fa-circle-o-notch fa-spin" aria-hidden="true"></i> Downloading from GitLab...</p>
<!-- The gantt component -->
<gantt v-bind:issues="issues" v-if="issues != null"></gantt>
</div>
</template>
<script>
import Gantt from './Gantt'
export default {
name: 'selectorWrapper',
props: [
'GitLab',
'currentUser'
],
data () {
return {
// main filter for listing issues:
// - by 'group' (group/project),
// - by 'project' (single project),
// - or by 'me' (all issue connected user created)
listBy: null,
defaultUnexistingPath: { path: 'loading...', path_with_namespace: 'loading...' },
defaultAllPath: { path: 'all' },
// FILTERING BY 'group'
// list of groups
groups: [ this.defaultUnexistingPath ],
// selected group
group: this.defaultUnexistingPath,
// list of projects in this group
groupProjects: [ this.defaultAllPath ],
// selected project in this group
gProject: this.defaultAllPath,
// FILTERING BY 'project'
// list of projects
projects: [ this.defaultUnexistingPath ],
// selected project
project: this.defaultUnexistingPath,
// currently downloading from GitLab
dowloading: false,
// issues are sent to <gant> as a `props`
issues: null
}
},
components: {
Gantt
},
methods: {
listByGroup: function (event) {
// clearing the issues
this.issues = null
// clearing the list of groups to default value
this.groups = [ this.defaultUnexistingPath ]
// selecting the default one
this.group = this.defaultUnexistingPath
// clearing the list of group projects to default value
this.groupProjects = [ this.defaultAllPath ]
// selecting the default one
this.gProject = this.defaultAllPath
// refreshing the list of groups
this.refreshGroups()
},
listByProject: function (event) {
// clearing the issues
this.issues = null
// clearing the list of projects to default value
this.projects = [ this.defaultUnexistingPath ]
// selecting the default one
this.project = this.defaultUnexistingPath
// refreshing the list of projects
this.refreshProjects()
},
listByMe: function (event) {
// clearing the issues
this.issues = null
// we are downloading
this.downloading = true
// user wants issues for all projects created by himself
this.$http.get(
this.GitLab + '/issues',
{
headers: { 'PRIVATE-TOKEN': process.env.GITLAB_PRIVATE_TOKEN },
params: {
'per_page': '100',
'state': 'opened'
}
}
).then((response) => {
// we are no more downloading
this.downloading = false
// Assigning response body directly to this.issues
this.issues = response.body
}, (response) => {
// error callback
})
},
selectedGroup: function (event) {
// clearing the list of groups to default value
this.groupProjects = [ this.defaultAllPath ]
// selecting the default one
this.gProject = this.defaultAllPath
// refresh the list of projects in this group
this.refreshGroupProjects()
// default to immediately listing all issues for all projects in this group
this.listGroupIssues()
},
selectedGroupProject: function (event) {
if (this.gProject.path === 'all') {
// listing all issues for all projects in this group
this.listGroupIssues()
} else {
// listing all issues for the selected project in this group
this.listGroupProjectIssues()
}
},
refreshGroups: function (event) {
// we are downloading
this.downloading = true
// user wants the list of groups
this.$http.get(
this.GitLab + '/groups',
{
headers: { 'PRIVATE-TOKEN': process.env.GITLAB_PRIVATE_TOKEN },
params: {
'per_page': '100',
'all_available': 1,
'search': 'gitlab-org' // this.currentUser.username // TODO remove this while implementing an efficient select with search
}
}
).then((response) => {
// we are no more downloading
this.downloading = false
// Assigning response body directly to this.groups
this.groups = response.body
}, (response) => {
// error callback
})
},
refreshGroupProjects: function (event) {
// we are downloading
this.downloading = true
// user wants the list of projects in this.group
this.$http.get(
this.GitLab + '/groups/' + this.group.id + '/projects',
{
headers: { 'PRIVATE-TOKEN': process.env.GITLAB_PRIVATE_TOKEN },
params: {
'per_page': '100'
}
}
).then((response) => {
// we are no more downloading
this.downloading = false
// Assigning response body directly to this.groupProjects
this.groupProjects = response.body
// Inserting first default unexisting "All" project
this.groupProjects.unshift(this.defaultAllPath)
}, (response) => {
// error callback
})
},
refreshProjects: function (event) {
// we are downloading
this.downloading = true
// user wants the list of projects
this.$http.get(
this.GitLab + '/projects',
{
headers: { 'PRIVATE-TOKEN': process.env.GITLAB_PRIVATE_TOKEN },
params: {
'per_page': '100'
}
}
).then((response) => {
// we are no more downloading
this.downloading = false
// Assigning response body directly to this.projects
this.projects = response.body
}, (response) => {
// error callback
})
},
listGroupIssues: function (event) {
// clearing the issues
this.issues = null
// we are downloading
this.downloading = true
// user wants issues for all projects in the selected group
this.$http.get(
this.GitLab + '/groups/' + this.group.id + '/issues',
{
headers: { 'PRIVATE-TOKEN': process.env.GITLAB_PRIVATE_TOKEN },
params: {
'per_page': '100',
'state': 'opened'
}
}
).then((response) => {
// we are no more downloading
this.downloading = false
// Assigning response body directly to this.issues
this.issues = response.body
}, (response) => {
// error callback
})
},
listGroupProjectIssues: function (event) {
// clearing the issues
this.issues = null
// we are downloading
this.downloading = true
// user wants issues for a selected project
this.$http.get(
this.GitLab + '/projects/' + this.gProject.id + '/issues',
{
headers: { 'PRIVATE-TOKEN': process.env.GITLAB_PRIVATE_TOKEN },
params: {
'per_page': '100',
'state': 'opened'
}
}
).then((response) => {
// we are no more downloading
this.downloading = false
this.issues = response.body
}, (response) => {
// error callback
})
},
listProjectIssues: function (event) {
// clearing the issues
this.issues = null
// we are downloading
this.downloading = true
// user wants issues for a selected project
this.$http.get(
this.GitLab + '/projects/' + this.project.id + '/issues',
{
headers: { 'PRIVATE-TOKEN': process.env.GITLAB_PRIVATE_TOKEN },
params: {
'per_page': '100',
'state': 'opened'
}
}
).then((response) => {
// we are no more downloading
this.downloading = false
this.issues = response.body
}, (response) => {
// error callback
})
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
import Vue from 'vue'
import VueResource from 'vue-resource'
Vue.use(VueResource)
import App from './App'
/* eslint-disable no-new */
......