mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
feat: project details page with branch statistics (resolve #242)
This commit is contained in:
parent
bb0d0569fd
commit
c2d3426bcd
@ -51,7 +51,7 @@ Check out our latest [blog post](https://muetsch.io/wakapi-s-year-2021.html), fe
|
|||||||
* ✅ Self-hosted
|
* ✅ Self-hosted
|
||||||
|
|
||||||
## 🚧 Roadmap
|
## 🚧 Roadmap
|
||||||
Plans for the near future mainly include, besides usual improvements and bug fixes, a UI redesign as well as additional types of charts and statistics (see [#101](https://github.com/muety/wakapi/issues/101), [#80](https://github.com/muety/wakapi/issues/80), [#76](https://github.com/muety/wakapi/issues/76), [#12](https://github.com/muety/wakapi/issues/12)). If you have feature requests or any kind of improvement proposals feel free to open an issue or share them in our [user survey](https://github.com/muety/wakapi/issues/82).
|
Plans for the near future mainly include, besides usual improvements and bug fixes, a UI redesign as well as additional types of charts and statistics (see [#101](https://github.com/muety/wakapi/issues/101), [#76](https://github.com/muety/wakapi/issues/76), [#12](https://github.com/muety/wakapi/issues/12)). If you have feature requests or any kind of improvement proposals feel free to open an issue or share them in our [user survey](https://github.com/muety/wakapi/issues/82).
|
||||||
|
|
||||||
## ⌨️ How to use?
|
## ⌨️ How to use?
|
||||||
There are different options for how to use Wakapi, ranging from our hosted cloud service to self-hosting it. Regardless of which option choose, you will always have to do the [client setup](#-client-setup) in addition.
|
There are different options for how to use Wakapi, ranging from our hosted cloud service to self-hosting it. Regardless of which option choose, you will always have to do the [client setup](#-client-setup) in addition.
|
||||||
@ -59,8 +59,6 @@ There are different options for how to use Wakapi, ranging from our hosted cloud
|
|||||||
### ☁️ Option 1: Use [wakapi.dev](https://wakapi.dev)
|
### ☁️ Option 1: Use [wakapi.dev](https://wakapi.dev)
|
||||||
If you want to try out a free, hosted cloud service, all you need to do is create an account and then set up your client-side tooling (see below).
|
If you want to try out a free, hosted cloud service, all you need to do is create an account and then set up your client-side tooling (see below).
|
||||||
|
|
||||||
However, we do not guarantee data persistence, so you might potentially lose your data if the service is taken down some day ❕
|
|
||||||
|
|
||||||
### 📦 Option 2: Quick-run a Release
|
### 📦 Option 2: Quick-run a Release
|
||||||
```bash
|
```bash
|
||||||
$ curl -L https://wakapi.dev/get | bash
|
$ curl -L https://wakapi.dev/get | bash
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -56,6 +56,7 @@ type SummaryParams struct {
|
|||||||
From time.Time
|
From time.Time
|
||||||
To time.Time
|
To time.Time
|
||||||
User *User
|
User *User
|
||||||
|
Filters *Filters
|
||||||
Recompute bool
|
Recompute bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,6 +305,26 @@ func (s *Summary) findFirstPresentType() (uint8, error) {
|
|||||||
return 127, errors.New("no type present")
|
return 127, errors.New("no type present")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SummaryParams) HasFilters() bool {
|
||||||
|
return s.Filters != nil && !s.Filters.IsEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SummaryParams) IsProjectDetails() bool {
|
||||||
|
if !s.HasFilters() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, entity, filters := s.Filters.One()
|
||||||
|
return entity == SummaryProject && len(filters) == 1 // exactly one
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SummaryParams) GetProjectFilter() string {
|
||||||
|
if !s.IsProjectDetails() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
_, _, filters := s.Filters.One()
|
||||||
|
return filters[0]
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SummaryItem) TotalFixed() time.Duration {
|
func (s *SummaryItem) TotalFixed() time.Duration {
|
||||||
// this is a workaround, since currently, the total time of a summary item is mistakenly represented in seconds
|
// this is a workaround, since currently, the total time of a summary item is mistakenly represented in seconds
|
||||||
// TODO: fix some day, while migrating persisted summary items
|
// TODO: fix some day, while migrating persisted summary items
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
routeutils "github.com/muety/wakapi/routes/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
conf "github.com/muety/wakapi/config"
|
conf "github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/middlewares"
|
"github.com/muety/wakapi/middlewares"
|
||||||
su "github.com/muety/wakapi/routes/utils"
|
|
||||||
"github.com/muety/wakapi/services"
|
"github.com/muety/wakapi/services"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
)
|
)
|
||||||
@ -51,7 +51,7 @@ func (h *SummaryApiHandler) RegisterRoutes(router *mux.Router) {
|
|||||||
// @Success 200 {object} models.Summary
|
// @Success 200 {object} models.Summary
|
||||||
// @Router /summary [get]
|
// @Router /summary [get]
|
||||||
func (h *SummaryApiHandler) Get(w http.ResponseWriter, r *http.Request) {
|
func (h *SummaryApiHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
summary, err, status := su.LoadUserSummary(h.summarySrvc, r)
|
summary, err, status := routeutils.LoadUserSummary(h.summarySrvc, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
|
@ -50,7 +50,7 @@ func (h *AllTimeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||||||
return // response was already sent by util function
|
return // response was already sent by util function
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err, status := h.loadUserSummary(user, routeutils.ParseFilters(r))
|
summary, err, status := h.loadUserSummary(user, utils.ParseSummaryFilters(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/muety/wakapi/middlewares"
|
"github.com/muety/wakapi/middlewares"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
||||||
routeutils "github.com/muety/wakapi/routes/utils"
|
|
||||||
"github.com/muety/wakapi/services"
|
"github.com/muety/wakapi/services"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
)
|
)
|
||||||
@ -95,7 +94,7 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err, status := h.loadUserSummary(requestedUser, rangeFrom, rangeTo, routeutils.ParseFilters(r))
|
summary, err, status := h.loadUserSummary(requestedUser, rangeFrom, rangeTo, utils.ParseSummaryFilters(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
|
@ -132,7 +132,7 @@ func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary
|
|||||||
summaries := make([]*models.Summary, len(intervals))
|
summaries := make([]*models.Summary, len(intervals))
|
||||||
|
|
||||||
// filtering
|
// filtering
|
||||||
filters := routeutils.ParseFilters(r)
|
filters := utils.ParseSummaryFilters(r)
|
||||||
|
|
||||||
for i, interval := range intervals {
|
for i, interval := range intervals {
|
||||||
summary, err := h.summarySrvc.Aliased(interval[0], interval[1], user, h.summarySrvc.Retrieve, filters, end.After(time.Now()))
|
summary, err := h.summarySrvc.Aliased(interval[0], interval[1], user, h.summarySrvc.Retrieve, filters, end.After(time.Now()))
|
||||||
|
@ -26,11 +26,13 @@ func NewSummaryHandler(summaryService services.ISummaryService, userService serv
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *SummaryHandler) RegisterRoutes(router *mux.Router) {
|
func (h *SummaryHandler) RegisterRoutes(router *mux.Router) {
|
||||||
r := router.PathPrefix("/summary").Subrouter()
|
r1 := router.PathPrefix("/summary").Subrouter()
|
||||||
r.Use(
|
r1.Use(middlewares.NewAuthenticateMiddleware(h.userSrvc).WithRedirectTarget(defaultErrorRedirectTarget()).Handler)
|
||||||
middlewares.NewAuthenticateMiddleware(h.userSrvc).WithRedirectTarget(defaultErrorRedirectTarget()).Handler,
|
r1.Methods(http.MethodGet).HandlerFunc(h.GetIndex)
|
||||||
)
|
|
||||||
r.Methods(http.MethodGet).HandlerFunc(h.GetIndex)
|
r2 := router.PathPrefix("/summary").Subrouter()
|
||||||
|
r2.Use(middlewares.NewAuthenticateMiddleware(h.userSrvc).WithRedirectTarget(defaultErrorRedirectTarget()).Handler)
|
||||||
|
r2.Methods(http.MethodGet).HandlerFunc(h.GetIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -20,7 +20,7 @@ func LoadUserSummary(ss services.ISummaryService, r *http.Request) (*models.Summ
|
|||||||
retrieveSummary = ss.Summarize
|
retrieveSummary = ss.Summarize
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err := ss.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, ParseFilters(r), summaryParams.Recompute)
|
summary, err := ss.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, summaryParams.Filters, summaryParams.Recompute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err, http.StatusInternalServerError
|
return nil, err, http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
@ -30,29 +30,3 @@ func LoadUserSummary(ss services.ISummaryService, r *http.Request) (*models.Summ
|
|||||||
|
|
||||||
return summary, nil, http.StatusOK
|
return summary, nil, http.StatusOK
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseFilters(r *http.Request) *models.Filters {
|
|
||||||
filters := &models.Filters{}
|
|
||||||
if q := r.URL.Query().Get("project"); q != "" {
|
|
||||||
filters.With(models.SummaryProject, q)
|
|
||||||
}
|
|
||||||
if q := r.URL.Query().Get("language"); q != "" {
|
|
||||||
filters.With(models.SummaryLanguage, q)
|
|
||||||
}
|
|
||||||
if q := r.URL.Query().Get("editor"); q != "" {
|
|
||||||
filters.With(models.SummaryEditor, q)
|
|
||||||
}
|
|
||||||
if q := r.URL.Query().Get("machine"); q != "" {
|
|
||||||
filters.With(models.SummaryMachine, q)
|
|
||||||
}
|
|
||||||
if q := r.URL.Query().Get("operating_system"); q != "" {
|
|
||||||
filters.With(models.SummaryOS, q)
|
|
||||||
}
|
|
||||||
if q := r.URL.Query().Get("label"); q != "" {
|
|
||||||
filters.With(models.SummaryLabel, q)
|
|
||||||
}
|
|
||||||
if q := r.URL.Query().Get("branch"); q != "" {
|
|
||||||
filters.With(models.SummaryBranch, q)
|
|
||||||
}
|
|
||||||
return filters
|
|
||||||
}
|
|
||||||
|
File diff suppressed because one or more lines are too long
@ -8,6 +8,11 @@ function TimePicker({ fromDate, toDate, timeSelection }) {
|
|||||||
fromDate: fromDate,
|
fromDate: fromDate,
|
||||||
toDate: toDate,
|
toDate: toDate,
|
||||||
timeSelection: timeSelection,
|
timeSelection: timeSelection,
|
||||||
|
intervalLink(interval) {
|
||||||
|
const queryParams = new URLSearchParams(window.location.search)
|
||||||
|
queryParams.set('interval', interval)
|
||||||
|
return `summary?${queryParams.toString()}`
|
||||||
|
},
|
||||||
onDateUpdated() {
|
onDateUpdated() {
|
||||||
document.getElementById('time-picker-form').submit()
|
document.getElementById('time-picker-form').submit()
|
||||||
},
|
},
|
||||||
|
@ -13,6 +13,7 @@ const editorsCanvas = document.getElementById('chart-editor')
|
|||||||
const languagesCanvas = document.getElementById('chart-language')
|
const languagesCanvas = document.getElementById('chart-language')
|
||||||
const machinesCanvas = document.getElementById('chart-machine')
|
const machinesCanvas = document.getElementById('chart-machine')
|
||||||
const labelsCanvas = document.getElementById('chart-label')
|
const labelsCanvas = document.getElementById('chart-label')
|
||||||
|
const branchesCanvas = document.getElementById('chart-branches')
|
||||||
|
|
||||||
const projectContainer = document.getElementById('project-container')
|
const projectContainer = document.getElementById('project-container')
|
||||||
const osContainer = document.getElementById('os-container')
|
const osContainer = document.getElementById('os-container')
|
||||||
@ -20,10 +21,11 @@ const editorContainer = document.getElementById('editor-container')
|
|||||||
const languageContainer = document.getElementById('language-container')
|
const languageContainer = document.getElementById('language-container')
|
||||||
const machineContainer = document.getElementById('machine-container')
|
const machineContainer = document.getElementById('machine-container')
|
||||||
const labelContainer = document.getElementById('label-container')
|
const labelContainer = document.getElementById('label-container')
|
||||||
|
const branchContainer = document.getElementById('branch-container')
|
||||||
|
|
||||||
const containers = [projectContainer, osContainer, editorContainer, languageContainer, machineContainer, labelContainer]
|
const containers = [projectContainer, osContainer, editorContainer, languageContainer, machineContainer, labelContainer, branchContainer]
|
||||||
const canvases = [projectsCanvas, osCanvas, editorsCanvas, languagesCanvas, machinesCanvas, labelsCanvas]
|
const canvases = [projectsCanvas, osCanvas, editorsCanvas, languagesCanvas, machinesCanvas, labelsCanvas, branchesCanvas]
|
||||||
const data = [wakapiData.projects, wakapiData.operatingSystems, wakapiData.editors, wakapiData.languages, wakapiData.machines, wakapiData.labels]
|
const data = [wakapiData.projects, wakapiData.operatingSystems, wakapiData.editors, wakapiData.languages, wakapiData.machines, wakapiData.labels, wakapiData.branches]
|
||||||
|
|
||||||
let topNPickers = [...document.getElementsByClassName('top-picker')]
|
let topNPickers = [...document.getElementsByClassName('top-picker')]
|
||||||
topNPickers.sort(((a, b) => parseInt(a.attributes['data-entity'].value) - parseInt(b.attributes['data-entity'].value)))
|
topNPickers.sort(((a, b) => parseInt(a.attributes['data-entity'].value) - parseInt(b.attributes['data-entity'].value)))
|
||||||
@ -64,9 +66,10 @@ function draw(subselection) {
|
|||||||
callbacks: {
|
callbacks: {
|
||||||
label: (item) => {
|
label: (item) => {
|
||||||
const d = wakapiData[key][item.dataIndex]
|
const d = wakapiData[key][item.dataIndex]
|
||||||
return `${d.key}: ${d.total.toString().toHHMMSS()}`
|
return ` ${d.key}: ${d.total.toString().toHHMMSS()}`
|
||||||
},
|
},
|
||||||
title: () => 'Total Time'
|
title: () => 'Total Time',
|
||||||
|
footer: () => key === 'projects' ? 'Click for details' : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,6 +130,16 @@ function draw(subselection) {
|
|||||||
tooltip: getTooltipOptions('projects'),
|
tooltip: getTooltipOptions('projects'),
|
||||||
},
|
},
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
|
onClick: (event, data) => {
|
||||||
|
const idx = data[0].index
|
||||||
|
const name = wakapiData.projects[idx].key
|
||||||
|
const query = new URLSearchParams(window.location.search)
|
||||||
|
query.set('project', name)
|
||||||
|
window.location.replace(`${window.location.pathname.slice(1)}?${query.toString()}`)
|
||||||
|
},
|
||||||
|
onHover: (event, elem) => {
|
||||||
|
event.native.target.style.cursor = elem[0] ? 'pointer' : 'default'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
@ -319,12 +332,58 @@ function draw(subselection) {
|
|||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
let branchChart = branchesCanvas && !branchesCanvas.classList.contains('hidden') && shouldUpdate(0)
|
||||||
|
? new Chart(branchesCanvas.getContext('2d'), {
|
||||||
|
type: "bar",
|
||||||
|
data: {
|
||||||
|
datasets: [{
|
||||||
|
data: wakapiData.branches
|
||||||
|
.slice(0, Math.min(showTopN[0], wakapiData.branches.length))
|
||||||
|
.map(p => parseInt(p.total)),
|
||||||
|
backgroundColor: wakapiData.branches.map((p, i) => {
|
||||||
|
const c = hexToRgb(getColor(p.key, i % baseColors.length))
|
||||||
|
return `rgba(${c.r}, ${c.g}, ${c.b}, 1)`
|
||||||
|
}),
|
||||||
|
hoverBackgroundColor: wakapiData.branches.map((p, i) => {
|
||||||
|
const c = hexToRgb(getColor(p.key, i % baseColors.length))
|
||||||
|
return `rgba(${c.r}, ${c.g}, ${c.b}, 0.8)`
|
||||||
|
}),
|
||||||
|
}],
|
||||||
|
labels: wakapiData.branches
|
||||||
|
.slice(0, Math.min(showTopN[0], wakapiData.branches.length))
|
||||||
|
.map(p => p.key)
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'y',
|
||||||
|
scales: {
|
||||||
|
xAxes: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Duration (hh:mm:ss)',
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
callback: (label) => label.toString().toHHMMSS(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: getTooltipOptions('branches'),
|
||||||
|
},
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
|
||||||
charts[0] = projectChart ? projectChart : charts[0]
|
charts[0] = projectChart ? projectChart : charts[0]
|
||||||
charts[1] = osChart ? osChart : charts[1]
|
charts[1] = osChart ? osChart : charts[1]
|
||||||
charts[2] = editorChart ? editorChart : charts[2]
|
charts[2] = editorChart ? editorChart : charts[2]
|
||||||
charts[3] = languageChart ? languageChart : charts[3]
|
charts[3] = languageChart ? languageChart : charts[3]
|
||||||
charts[4] = machineChart ? machineChart : charts[4]
|
charts[4] = machineChart ? machineChart : charts[4]
|
||||||
charts[5] = labelChart ? labelChart : charts[5]
|
charts[5] = labelChart ? labelChart : charts[5]
|
||||||
|
charts[6] = branchChart ? branchChart : charts[6]
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTopN() {
|
function parseTopN() {
|
||||||
|
@ -96,14 +96,43 @@ func ParseSummaryParams(r *http.Request) (*models.SummaryParams, error) {
|
|||||||
|
|
||||||
recompute := params.Get("recompute") != "" && params.Get("recompute") != "false"
|
recompute := params.Get("recompute") != "" && params.Get("recompute") != "false"
|
||||||
|
|
||||||
|
filters := ParseSummaryFilters(r)
|
||||||
|
|
||||||
return &models.SummaryParams{
|
return &models.SummaryParams{
|
||||||
From: from,
|
From: from,
|
||||||
To: to,
|
To: to,
|
||||||
User: user,
|
User: user,
|
||||||
Recompute: recompute,
|
Recompute: recompute,
|
||||||
|
Filters: filters,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseSummaryFilters(r *http.Request) *models.Filters {
|
||||||
|
filters := &models.Filters{}
|
||||||
|
if q := r.URL.Query().Get("project"); q != "" {
|
||||||
|
filters.With(models.SummaryProject, q)
|
||||||
|
}
|
||||||
|
if q := r.URL.Query().Get("language"); q != "" {
|
||||||
|
filters.With(models.SummaryLanguage, q)
|
||||||
|
}
|
||||||
|
if q := r.URL.Query().Get("editor"); q != "" {
|
||||||
|
filters.With(models.SummaryEditor, q)
|
||||||
|
}
|
||||||
|
if q := r.URL.Query().Get("machine"); q != "" {
|
||||||
|
filters.With(models.SummaryMachine, q)
|
||||||
|
}
|
||||||
|
if q := r.URL.Query().Get("operating_system"); q != "" {
|
||||||
|
filters.With(models.SummaryOS, q)
|
||||||
|
}
|
||||||
|
if q := r.URL.Query().Get("label"); q != "" {
|
||||||
|
filters.With(models.SummaryLabel, q)
|
||||||
|
}
|
||||||
|
if q := r.URL.Query().Get("branch"); q != "" {
|
||||||
|
filters.With(models.SummaryBranch, q)
|
||||||
|
}
|
||||||
|
return filters
|
||||||
|
}
|
||||||
|
|
||||||
func extractUser(r *http.Request) *models.User {
|
func extractUser(r *http.Request) *models.User {
|
||||||
type principalGetter interface {
|
type principalGetter interface {
|
||||||
GetPrincipal() *models.User
|
GetPrincipal() *models.User
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
|
|
||||||
{{ if .User.HasData }}
|
{{ if .User.HasData }}
|
||||||
|
|
||||||
|
{{ if not .IsProjectDetails }}
|
||||||
<!-- KPIs -->
|
<!-- KPIs -->
|
||||||
<div class="flex gap-x-6 gap-y-6 w-full mb-4 flex-wrap">
|
<div class="flex gap-x-6 gap-y-6 w-full mb-4 flex-wrap">
|
||||||
<div class="flex flex-col space-y-2 w-40 p-4 rounded-md p-4 text-gray-300 bg-gray-850 leading-none border-2 border-green-700">
|
<div class="flex flex-col space-y-2 w-40 p-4 rounded-md p-4 text-gray-300 bg-gray-850 leading-none border-2 border-green-700">
|
||||||
@ -57,56 +58,68 @@
|
|||||||
<span class="font-semibold text-xl truncate" title="{{ .MaxByToString 2 }}">{{ .MaxByToString 2 }}</span>
|
<span class="font-semibold text-xl truncate" title="{{ .MaxByToString 2 }}">{{ .MaxByToString 2 }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{ else }}
|
||||||
|
<div class="mb-8 w-full">
|
||||||
|
<h1 class="font-semibold text-3xl text-white">Project "{{ .GetProjectFilter }}"</h1>
|
||||||
|
<h4 class="font-semibold text-lg text-gray-500">{{ .TotalTime | duration }}</h4>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
<div class="flex flex-wrap w-full justify-center mt-4">
|
<div class="grid gap-2 grid-cols-1 md:grid-cols-2 w-full mt-4">
|
||||||
<div class="w-full lg:w-1/2 p-1" style="max-width: 100vw;">
|
<div class="row-span-2 p-4 px-6 pb-10 bg-gray-850 text-gray-300 rounded-md shadow flex flex-col {{ if .IsProjectDetails }} hidden {{ end }}" id="project-container" style="max-height: 608px; max-width: 100vw">
|
||||||
<div class="p-4 px-6 pb-10 bg-gray-850 text-gray-300 rounded-md shadow flex flex-col" id="project-container" style="height: 608px">
|
<div class="flex justify-between">
|
||||||
<div class="flex justify-between">
|
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Projects</span>
|
||||||
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Projects</span>
|
<div class="flex justify-end flex-1 text-xs items-center">
|
||||||
<div class="flex justify-end flex-1 text-xs items-center">
|
<input type="number" min="1" id="project-top-picker" data-entity="0" class="top-picker bg-gray-800 rounded-md text-center w-12" value="10">
|
||||||
<input type="number" min="1" id="project-top-picker" data-entity="0" class="top-picker bg-gray-800 rounded-md text-center w-12" value="10">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<canvas id="chart-projects" class="mt-2"></canvas>
|
|
||||||
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
|
|
||||||
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<canvas id="chart-projects" class="mt-2"></canvas>
|
||||||
|
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
|
||||||
|
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full lg:w-1/2 p-1" style="max-width: 100vw;">
|
<div class="row-span-2 p-4 px-6 pb-10 bg-gray-850 text-gray-300 rounded-md shadow flex flex-col {{ if not .IsProjectDetails }} hidden {{ end }}" id="branch-container" style="max-height: 608px; max-width: 100vw">
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="flex justify-between">
|
||||||
<div class="p-4 px-6 pb-10 bg-gray-850 text-gray-300 rounded-md shadow flex flex-col" id="language-container" style="height: 300px">
|
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Branches</span>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-end flex-1 text-xs items-center">
|
||||||
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Languages</span>
|
<input type="number" min="1" id="branch-top-picker" data-entity="6" class="top-picker bg-gray-800 rounded-md text-center w-12" value="10">
|
||||||
<div class="flex justify-end flex-1 text-xs items-center">
|
|
||||||
<input type="number" min="1" id="language-top-picker" data-entity="3" class="top-picker bg-gray-800 rounded-md text-center w-12" value="10">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<canvas id="chart-language" class="mt-4"></canvas>
|
|
||||||
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
|
|
||||||
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="p-4 px-6 pb-10 bg-gray-850 text-gray-300 rounded-md shadow flex flex-col" id="editor-container" style="height: 300px">
|
<canvas id="chart-branches" class="mt-2"></canvas>
|
||||||
<div class="flex justify-between">
|
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
|
||||||
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Editors</span>
|
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
|
||||||
<div class="flex justify-end flex-1 text-xs items-center">
|
|
||||||
<input type="number" min="1" id="editor-top-picker" data-entity="2" class="top-picker bg-gray-800 rounded-md text-center w-12" value="10">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<canvas id="chart-editor" class="mt-4"></canvas>
|
|
||||||
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
|
|
||||||
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full lg:w-1/2 p-1" style="max-width: 100vw;">
|
<div class="p-4 px-6 pb-10 bg-gray-850 text-gray-300 rounded-md shadow flex flex-col" id="language-container" style="max-height: 300px">
|
||||||
<div class="p-4 px-6 pb-10 bg-gray-850 text-gray-300 rounded-md shadow flex flex-col" id="os-container" style="height: 300px">
|
<div class="flex justify-between">
|
||||||
|
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Languages</span>
|
||||||
|
<div class="flex justify-end flex-1 text-xs items-center">
|
||||||
|
<input type="number" min="1" id="language-top-picker" data-entity="3" class="top-picker bg-gray-800 rounded-md text-center w-12" value="10">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<canvas id="chart-language" class="mt-4"></canvas>
|
||||||
|
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
|
||||||
|
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 px-6 pb-10 bg-gray-850 text-gray-300 rounded-md shadow flex flex-col" id="editor-container" style="max-height: 300px">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Editors</span>
|
||||||
|
<div class="flex justify-end flex-1 text-xs items-center">
|
||||||
|
<input type="number" min="1" id="editor-top-picker" data-entity="2" class="top-picker bg-gray-800 rounded-md text-center w-12" value="10">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<canvas id="chart-editor" class="mt-4"></canvas>
|
||||||
|
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
|
||||||
|
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="{{ if .IsProjectDetails }} hidden {{ end }}" style="max-width: 100vw;">
|
||||||
|
<div class="p-4 px-6 pb-10 bg-gray-850 text-gray-300 rounded-md shadow flex flex-col" id="os-container" style="max-height: 300px">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap mr-1 cursor-pointer">Operating Systems</span>
|
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap mr-1 cursor-pointer">Operating Systems</span>
|
||||||
@ -122,8 +135,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden w-full lg:w-1/2 p-1" style="max-width: 100vw;">
|
|
||||||
<div class="p-4 px-6 pb-10 bg-gray-850 text-gray-300 rounded-md shadow flex flex-col" id="machine-container" style="height: 300px">
|
<div class="hidden" style="max-width: 100vw;">
|
||||||
|
<div class="p-4 px-6 pb-10 bg-gray-850 text-gray-300 rounded-md shadow flex flex-col" id="machine-container" style="max-height: 300px">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap mr-1 cursor-pointer text-gray-600" onclick="swapCharts('os', 'machine')">Operating Systems</span>
|
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap mr-1 cursor-pointer text-gray-600" onclick="swapCharts('os', 'machine')">Operating Systems</span>
|
||||||
@ -139,8 +153,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full lg:w-1/2 p-1" style="max-width: 100vw;">
|
|
||||||
<div class="p-4 px-6 pb-10 bg-gray-850 text-gray-300 rounded-md shadow flex flex-col" id="label-container" style="height: 300px">
|
<div style="max-width: 100vw;">
|
||||||
|
<div class="p-4 px-6 pb-10 bg-gray-850 text-gray-300 rounded-md shadow flex flex-col {{ if .IsProjectDetails }} hidden {{ end }}" id="label-container" style="max-height: 300px">
|
||||||
<div class="flex justify-between text-lg" style="margin-bottom: -10px">
|
<div class="flex justify-between text-lg" style="margin-bottom: -10px">
|
||||||
<span class="font-semibold whitespace-nowrap">Labels</span>
|
<span class="font-semibold whitespace-nowrap">Labels</span>
|
||||||
<a href="settings#data" class="ml-4 inline p-2 hover:bg-gray-800 rounded" style="margin-top: -5px">
|
<a href="settings#data" class="ml-4 inline p-2 hover:bg-gray-800 rounded" style="margin-top: -5px">
|
||||||
@ -204,6 +219,11 @@
|
|||||||
wakapiData.languages = {{ .Languages | json }}
|
wakapiData.languages = {{ .Languages | json }}
|
||||||
wakapiData.machines = {{ .Machines | json }}
|
wakapiData.machines = {{ .Machines | json }}
|
||||||
wakapiData.labels = {{ .Labels | json }}
|
wakapiData.labels = {{ .Labels | json }}
|
||||||
|
{{ if .IsProjectDetails }}
|
||||||
|
wakapiData.branches = {{ .Branches | json }}
|
||||||
|
{{ else }}
|
||||||
|
wakapiData.branches = []
|
||||||
|
{{ end }}
|
||||||
</script>
|
</script>
|
||||||
<script src="assets/js/summary.js"></script>
|
<script src="assets/js/summary.js"></script>
|
||||||
|
|
||||||
|
@ -7,15 +7,15 @@
|
|||||||
|
|
||||||
<div v-cloak v-show="state.showDropdownTimepicker" class="z-10 absolute top-0 right-0 popup mt-12 w-40" id="time-picker-dropdown">
|
<div v-cloak v-show="state.showDropdownTimepicker" class="z-10 absolute top-0 right-0 popup mt-12 w-40" id="time-picker-dropdown">
|
||||||
<div class="flex-grow flex flex-col flex bg-gray-850 shadow-md rounded w-40 p-1 ">
|
<div class="flex-grow flex flex-col flex bg-gray-850 shadow-md rounded w-40 p-1 ">
|
||||||
<a id="time-option-today" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" href="summary?interval=today" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">Today</a>
|
<a id="time-option-today" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" :href="intervalLink('today')" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">Today</a>
|
||||||
<a id="time-option-yesterday" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" href="summary?interval=yesterday" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">Yesterday</a>
|
<a id="time-option-yesterday" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" :href="intervalLink('yesterday')" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">Yesterday</a>
|
||||||
<a id="time-option-week" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" href="summary?interval=week" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">This Week</a>
|
<a id="time-option-week" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" :href="intervalLink('week')" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">This Week</a>
|
||||||
<a id="time-option-month" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" href="summary?interval=month" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">This Month</a>
|
<a id="time-option-month" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" :href="intervalLink('month')" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">This Month</a>
|
||||||
<a id="time-option-year" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" href="summary?interval=year" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">This Year</a>
|
<a id="time-option-year" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" :href="intervalLink('year')" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">This Year</a>
|
||||||
<a id="time-option-last_7_days" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" href="summary?interval=last_7_days" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">Past 7 Days</a>
|
<a id="time-option-last_7_days" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" :href="intervalLink('last_7_days')" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">Past 7 Days</a>
|
||||||
<a id="time-option-last_30_days" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" href="summary?interval=last_30_days" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">Past 30 Days</a>
|
<a id="time-option-last_30_days" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" :href="intervalLink('last_30_days')" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">Past 30 Days</a>
|
||||||
<a id="time-option-last_12_months" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" href="summary?interval=last_12_months" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">Past 12 Months</a>
|
<a id="time-option-last_12_months" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" :href="intervalLink('last_12_months')" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">Past 12 Months</a>
|
||||||
<a id="time-option-any" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" href="summary?interval=any" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">All Time</a>
|
<a id="time-option-any" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" :href="intervalLink('any')" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">All Time</a>
|
||||||
<hr class="my-2">
|
<hr class="my-2">
|
||||||
<form id="time-picker-form" class="flex flex-col space-y-1">
|
<form id="time-picker-form" class="flex flex-col space-y-1">
|
||||||
<div class="flex flex-col space-x-1 bg-gray-900 rounded p-1 border-2 border-gray-800">
|
<div class="flex flex-col space-x-1 bg-gray-900 rounded p-1 border-2 border-gray-800">
|
||||||
|
Loading…
Reference in New Issue
Block a user