From a87b588a24553594ce30b055de93090b35738ec8 Mon Sep 17 00:00:00 2001 From: hulmgulm Date: Sun, 3 Nov 2024 14:27:27 +0100 Subject: [PATCH] feat: total in dashboard entries (#89) --- dashboard/convert/entry.go | 1 + dashboard/dbrange/ranges_test.go | 2 +- dashboard/entry/add.go | 3 +- dashboard/entry/entries_test.go | 39 ++++++++++++------- dashboard/entry/update.go | 7 +++- model/dashboard.go | 1 + schema.graphql | 5 ++- ui/src/dashboard/DashboardPage.tsx | 1 + ui/src/dashboard/Entry/AddPopup.tsx | 1 + ui/src/dashboard/Entry/DashboardBarChart.tsx | 5 ++- ui/src/dashboard/Entry/DashboardEntry.tsx | 10 ++--- ui/src/dashboard/Entry/DashboardEntryForm.tsx | 33 ++++++++++++++++ ui/src/dashboard/Entry/DashboardLineChart.tsx | 5 ++- ui/src/dashboard/Entry/DashboardTable.tsx | 9 ++++- ui/src/dashboard/Entry/EditPopup.tsx | 1 + ui/src/dashboard/Entry/TagTooltip.tsx | 28 ++++++++----- ui/src/gql/dashboard.ts | 21 ++++++++-- 17 files changed, 129 insertions(+), 43 deletions(-) diff --git a/dashboard/convert/entry.go b/dashboard/convert/entry.go index 12d11e1..c606341 100644 --- a/dashboard/convert/entry.go +++ b/dashboard/convert/entry.go @@ -49,6 +49,7 @@ func ToExternalEntry(entry model.DashboardEntry) (*gqlmodel.DashboardEntry, erro return &gqlmodel.DashboardEntry{ ID: entry.ID, Title: entry.Title, + Total: entry.Total, Pos: &pos, StatsSelection: stats, EntryType: ExternalEntryType(entry.Type), diff --git a/dashboard/dbrange/ranges_test.go b/dashboard/dbrange/ranges_test.go index 8da3804..2fd1aa0 100644 --- a/dashboard/dbrange/ranges_test.go +++ b/dashboard/dbrange/ranges_test.go @@ -150,7 +150,7 @@ func TestRanges(t *testing.T) { }) require.EqualError(t, err, "dashboard range does not exist") - entry, err := resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "other", gqlmodel.InputStatsSelection{ + entry, err := resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "other", true, gqlmodel.InputStatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"abc"}, RangeID: &xrange.ID, diff --git a/dashboard/entry/add.go b/dashboard/entry/add.go index 57ca4c1..f9fe8ee 100644 --- a/dashboard/entry/add.go +++ b/dashboard/entry/add.go @@ -17,7 +17,7 @@ import ( ) // AddDashboardEntry adds a dashboard entry. -func (r *ResolverForEntry) AddDashboardEntry(ctx context.Context, dashboardID int, entryType gqlmodel.EntryType, title string, stats gqlmodel.InputStatsSelection, pos *gqlmodel.InputResponsiveDashboardEntryPos) (*gqlmodel.DashboardEntry, error) { +func (r *ResolverForEntry) AddDashboardEntry(ctx context.Context, dashboardID int, entryType gqlmodel.EntryType, title string, total bool, stats gqlmodel.InputStatsSelection, pos *gqlmodel.InputResponsiveDashboardEntryPos) (*gqlmodel.DashboardEntry, error) { userID := auth.GetUser(ctx).ID if _, err := util.FindDashboard(r.DB, userID, dashboardID); err != nil { @@ -28,6 +28,7 @@ func (r *ResolverForEntry) AddDashboardEntry(ctx context.Context, dashboardID in Keys: strings.Join(stats.Tags, ","), Type: convert.InternalEntryType(entryType), Title: title, + Total: total, DashboardID: dashboardID, Interval: convert.InternalInterval(stats.Interval), MobilePosition: convert.EmptyPos(), diff --git a/dashboard/entry/entries_test.go b/dashboard/entry/entries_test.go index 52cb741..b1255a7 100644 --- a/dashboard/entry/entries_test.go +++ b/dashboard/entry/entries_test.go @@ -13,6 +13,7 @@ import ( func TestEntries(t *testing.T) { db := test.InMemoryDB(t) defer db.Close() + var bVal bool resolver := dashboard.NewResolverForDashboard(db.DB) @@ -35,7 +36,7 @@ func TestEntries(t *testing.T) { } require.Equal(t, expectAdded, dashboard) - _, err = resolver.AddDashboardEntry(user1, 5, gqlmodel.EntryTypeBarChart, "test", gqlmodel.InputStatsSelection{ + _, err = resolver.AddDashboardEntry(user1, 5, gqlmodel.EntryTypeBarChart, "test", false, gqlmodel.InputStatsSelection{ Interval: "", Tags: []string{"hhol"}, ExcludeTags: nil, @@ -47,7 +48,7 @@ func TestEntries(t *testing.T) { }, }, nil) require.EqualError(t, err, "dashboard does not exist") - _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", gqlmodel.InputStatsSelection{ + _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", false, gqlmodel.InputStatsSelection{ Interval: gqlmodel.StatsIntervalHourly, Tags: []string{"hhol"}, ExcludeTags: nil, @@ -59,7 +60,7 @@ func TestEntries(t *testing.T) { }, }, nil) require.EqualError(t, err, "dashboard range does not exist") - _, err = resolver.AddDashboardEntry(user2, 1, gqlmodel.EntryTypeBarChart, "test", gqlmodel.InputStatsSelection{ + _, err = resolver.AddDashboardEntry(user2, 1, gqlmodel.EntryTypeBarChart, "test", false, gqlmodel.InputStatsSelection{ Interval: "doubly", Tags: []string{"hhol"}, ExcludeTags: nil, @@ -71,7 +72,7 @@ func TestEntries(t *testing.T) { }, }, nil) require.EqualError(t, err, "dashboard does not exist") - _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", gqlmodel.InputStatsSelection{ + _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", false, gqlmodel.InputStatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"hhol"}, ExcludeTags: nil, @@ -83,7 +84,7 @@ func TestEntries(t *testing.T) { }, }, nil) require.EqualError(t, err, "range to (now-2) invalid: expected unit at the end but got nothing") - _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", gqlmodel.InputStatsSelection{ + _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", false, gqlmodel.InputStatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"hhol"}, ExcludeTags: nil, @@ -95,7 +96,7 @@ func TestEntries(t *testing.T) { }, }, nil) require.EqualError(t, err, "range from (now-2) invalid: expected unit at the end but got nothing") - _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", gqlmodel.InputStatsSelection{ + _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", false, gqlmodel.InputStatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{}, ExcludeTags: nil, @@ -108,7 +109,7 @@ func TestEntries(t *testing.T) { }, nil) require.EqualError(t, err, "at least one tag is required") - entry, err := resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", gqlmodel.InputStatsSelection{ + entry, err := resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", false, gqlmodel.InputStatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"abc"}, ExcludeTags: nil, @@ -128,6 +129,7 @@ func TestEntries(t *testing.T) { expectedEntry := &gqlmodel.DashboardEntry{ ID: 1, Title: "test", + Total: false, StatsSelection: &gqlmodel.StatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"abc"}, @@ -173,7 +175,7 @@ func TestEntries(t *testing.T) { }, }) require.NoError(t, err) - _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "other", gqlmodel.InputStatsSelection{ + _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "other", false, gqlmodel.InputStatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"abc"}, RangeID: p(xrange.ID), @@ -188,11 +190,11 @@ func TestEntries(t *testing.T) { require.EqualError(t, err, "range is used in entries: other") chart := gqlmodel.EntryTypePieChart - _, err = resolver.UpdateDashboardEntry(user2, 1, &chart, nil, nil, nil) + _, err = resolver.UpdateDashboardEntry(user2, 1, &chart, nil, nil, nil, nil) require.EqualError(t, err, "dashboard does not exist") - _, err = resolver.UpdateDashboardEntry(user1, 3, &chart, nil, nil, nil) + _, err = resolver.UpdateDashboardEntry(user1, 3, &chart, nil, nil, nil, nil) require.EqualError(t, err, "entry does not exist") - _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &gqlmodel.InputStatsSelection{ + _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &bVal, &gqlmodel.InputStatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"kek"}, ExcludeTags: nil, @@ -204,7 +206,7 @@ func TestEntries(t *testing.T) { }, }, nil) require.EqualError(t, err, "range from (now-2) invalid: expected unit at the end but got nothing") - _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &gqlmodel.InputStatsSelection{ + _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &bVal, &gqlmodel.InputStatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"kek"}, ExcludeTags: nil, @@ -222,6 +224,7 @@ func TestEntries(t *testing.T) { { ID: 1, Title: "test", + Total: false, StatsSelection: &gqlmodel.StatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"abc"}, @@ -256,6 +259,7 @@ func TestEntries(t *testing.T) { { ID: 2, Title: "other", + Total: false, StatsSelection: &gqlmodel.StatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"abc"}, @@ -284,7 +288,7 @@ func TestEntries(t *testing.T) { EntryType: gqlmodel.EntryTypeBarChart, }, }, dashboards[0].Items) - _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &gqlmodel.InputStatsSelection{ + _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &bVal, &gqlmodel.InputStatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"kek"}, ExcludeTags: nil, @@ -308,6 +312,7 @@ func TestEntries(t *testing.T) { { ID: 1, Title: "cool title", + Total: false, StatsSelection: &gqlmodel.StatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"kek"}, @@ -342,6 +347,7 @@ func TestEntries(t *testing.T) { { ID: 2, Title: "other", + Total: false, StatsSelection: &gqlmodel.StatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"abc"}, @@ -371,7 +377,7 @@ func TestEntries(t *testing.T) { }, }, dashboards[0].Items) - _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &gqlmodel.InputStatsSelection{ + _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &bVal, &gqlmodel.InputStatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"kek"}, ExcludeTags: nil, @@ -385,7 +391,7 @@ func TestEntries(t *testing.T) { }}) require.EqualError(t, err, "dashboard range does not exist") - _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &gqlmodel.InputStatsSelection{ + _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &bVal, &gqlmodel.InputStatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"kek"}, ExcludeTags: nil, @@ -405,6 +411,7 @@ func TestEntries(t *testing.T) { { ID: 1, Title: "cool title", + Total: false, StatsSelection: &gqlmodel.StatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"kek"}, @@ -435,6 +442,7 @@ func TestEntries(t *testing.T) { { ID: 2, Title: "other", + Total: false, StatsSelection: &gqlmodel.StatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"abc"}, @@ -479,6 +487,7 @@ func TestEntries(t *testing.T) { { ID: 2, Title: "other", + Total: false, StatsSelection: &gqlmodel.StatsSelection{ Interval: gqlmodel.StatsIntervalDaily, Tags: []string{"abc"}, diff --git a/dashboard/entry/update.go b/dashboard/entry/update.go index b2566e2..d5d406b 100644 --- a/dashboard/entry/update.go +++ b/dashboard/entry/update.go @@ -16,7 +16,7 @@ import ( ) // UpdateDashboardEntry updates a dashboard entry. -func (r *ResolverForEntry) UpdateDashboardEntry(ctx context.Context, id int, entryType *gqlmodel.EntryType, title *string, stats *gqlmodel.InputStatsSelection, pos *gqlmodel.InputResponsiveDashboardEntryPos) (*gqlmodel.DashboardEntry, error) { +func (r *ResolverForEntry) UpdateDashboardEntry(ctx context.Context, id int, entryType *gqlmodel.EntryType, title *string, total *bool, stats *gqlmodel.InputStatsSelection, pos *gqlmodel.InputResponsiveDashboardEntryPos) (*gqlmodel.DashboardEntry, error) { userID := auth.GetUser(ctx).ID entry, err := util.FindDashboardEntry(r.DB, id) @@ -31,6 +31,11 @@ func (r *ResolverForEntry) UpdateDashboardEntry(ctx context.Context, id int, ent if title != nil { entry.Title = *title } + + if total != nil { + entry.Total = *total + } + if stats != nil { if stats.RangeID != nil { if _, err := util.FindDashboardRange(r.DB, *stats.RangeID); err != nil { diff --git a/model/dashboard.go b/model/dashboard.go index 063367a..759de85 100644 --- a/model/dashboard.go +++ b/model/dashboard.go @@ -29,6 +29,7 @@ type DashboardEntry struct { ID int `gorm:"primary_key;unique_index;AUTO_INCREMENT"` DashboardID int `gorm:"type:int REFERENCES dashboards(id) ON DELETE CASCADE"` Title string + Total bool `gorm:"default:false"` Type DashboardType Keys string Interval Interval diff --git a/schema.graphql b/schema.graphql index ee92e6f..e6200e1 100644 --- a/schema.graphql +++ b/schema.graphql @@ -40,8 +40,8 @@ type RootMutation { updateDashboardRange(rangeId: Int!, range: InputNamedDateRange!): NamedDateRange removeDashboardRange(rangeId: Int!): NamedDateRange - addDashboardEntry(dashboardId: Int!, entryType: EntryType!, title: String!, stats: InputStatsSelection!, pos: InputResponsiveDashboardEntryPos): DashboardEntry @hasRole(role: USER) - updateDashboardEntry(entryId: Int!, entryType: EntryType, title: String, stats: InputStatsSelection, pos: InputResponsiveDashboardEntryPos): DashboardEntry @hasRole(role: USER) + addDashboardEntry(dashboardId: Int!, entryType: EntryType!, title: String!, total: Boolean!, stats: InputStatsSelection!, pos: InputResponsiveDashboardEntryPos): DashboardEntry @hasRole(role: USER) + updateDashboardEntry(entryId: Int!, entryType: EntryType, title: String, total: Boolean, stats: InputStatsSelection, pos: InputResponsiveDashboardEntryPos): DashboardEntry @hasRole(role: USER) removeDashboardEntry(id: Int!): DashboardEntry! @hasRole(role: USER) setUserSettings(settings: InputUserSettings!): UserSettings! @hasRole(role: USER) @@ -251,6 +251,7 @@ input InputRelativeOrStaticRange { type DashboardEntry { id: Int! title: String! + total: Boolean! statsSelection: StatsSelection! pos: ResponsiveDashboardEntryPos! entryType: EntryType! diff --git a/ui/src/dashboard/DashboardPage.tsx b/ui/src/dashboard/DashboardPage.tsx index dbf7e28..cc623dd 100644 --- a/ui/src/dashboard/DashboardPage.tsx +++ b/ui/src/dashboard/DashboardPage.tsx @@ -74,6 +74,7 @@ const newEntry = (): Dashboards_dashboards_items => { __typename: 'DashboardEntryPos', }, }, + total: false, }; }; diff --git a/ui/src/dashboard/Entry/AddPopup.tsx b/ui/src/dashboard/Entry/AddPopup.tsx index d95c66e..22aa090 100644 --- a/ui/src/dashboard/Entry/AddPopup.tsx +++ b/ui/src/dashboard/Entry/AddPopup.tsx @@ -78,6 +78,7 @@ export const AddPopup: React.FC = ({ dashboardId, entryType: entry.entryType, title: entry.title, + total: entry.total, stats: { tags: entry.statsSelection.tags, interval: entry.statsSelection.interval, diff --git a/ui/src/dashboard/Entry/DashboardBarChart.tsx b/ui/src/dashboard/Entry/DashboardBarChart.tsx index 01b9678..58a45c3 100644 --- a/ui/src/dashboard/Entry/DashboardBarChart.tsx +++ b/ui/src/dashboard/Entry/DashboardBarChart.tsx @@ -12,6 +12,7 @@ interface DashboardPieChartProps { entries: Stats_stats[]; interval: StatsInterval; type: 'stacked' | 'normal'; + total: boolean; } interface Indexed { @@ -20,7 +21,7 @@ interface Indexed { data: Record; } -export const DashboardBarChart: React.FC = ({entries, interval, type}) => { +export const DashboardBarChart: React.FC = ({entries, interval, type, total}) => { const indexedEntries: Indexed[] = entries .map((entry) => { return { @@ -46,7 +47,7 @@ export const DashboardBarChart: React.FC = ({entries, in - } /> + } /> dateFormat(moment(entry.start))} interval={'preserveStartEnd'} /> diff --git a/ui/src/dashboard/Entry/DashboardEntry.tsx b/ui/src/dashboard/Entry/DashboardEntry.tsx index faded58..de67341 100644 --- a/ui/src/dashboard/Entry/DashboardEntry.tsx +++ b/ui/src/dashboard/Entry/DashboardEntry.tsx @@ -88,7 +88,7 @@ const SpecificDashboardEntry: React.FC<{entry: Dashboards_dashboards_items; rang ); } - return ; + return ; case EntryType.StackedBarChart: if (entries.length === 0) { return ( @@ -97,7 +97,7 @@ const SpecificDashboardEntry: React.FC<{entry: Dashboards_dashboards_items; rang ); } - return ; + return ; case EntryType.LineChart: if (entries.length === 0) { return ( @@ -106,7 +106,7 @@ const SpecificDashboardEntry: React.FC<{entry: Dashboards_dashboards_items; rang ); } - return ; + return ; case EntryType.VerticalTable: if (entries.length === 0) { return ( @@ -115,7 +115,7 @@ const SpecificDashboardEntry: React.FC<{entry: Dashboards_dashboards_items; rang ); } - return ; + return ; case EntryType.HorizontalTable: if (entries.length === 0) { return ( @@ -124,7 +124,7 @@ const SpecificDashboardEntry: React.FC<{entry: Dashboards_dashboards_items; rang ); } - return ; + return ; default: return expectNever(entry.entryType); } diff --git a/ui/src/dashboard/Entry/DashboardEntryForm.tsx b/ui/src/dashboard/Entry/DashboardEntryForm.tsx index a586159..ae5d02b 100644 --- a/ui/src/dashboard/Entry/DashboardEntryForm.tsx +++ b/ui/src/dashboard/Entry/DashboardEntryForm.tsx @@ -163,6 +163,24 @@ export const DashboardEntryForm: React.FC = ({entry, onChange: s ))} )} + {entry.entryType !== EntryType.PieChart ? ( + + + {getTotalTitle(entry.entryType)} + + { + entry.total = e.target.checked; + setEntry(entry); + }} + /> + + + + ) : ( + undefined + )} = ({entry, onChange: s ); }; + +const getTotalTitle = (entryType: EntryType) => { + switch (entryType) { + case EntryType.HorizontalTable: + return 'Show "Total" row:'; + case EntryType.VerticalTable: + return 'Show "Total" column:'; + case EntryType.BarChart: + case EntryType.StackedBarChart: + case EntryType.LineChart: + return 'Show "Total" in tooltip:'; + default: + return ''; + } +}; diff --git a/ui/src/dashboard/Entry/DashboardLineChart.tsx b/ui/src/dashboard/Entry/DashboardLineChart.tsx index 3cc1936..90ec14c 100644 --- a/ui/src/dashboard/Entry/DashboardLineChart.tsx +++ b/ui/src/dashboard/Entry/DashboardLineChart.tsx @@ -11,6 +11,7 @@ import {TagTooltip} from './TagTooltip'; interface DashboardPieChartProps { entries: Stats_stats[]; interval: StatsInterval; + total: boolean; } interface Indexed { @@ -19,7 +20,7 @@ interface Indexed { data: Record; } -export const DashboardLineChart: React.FC = ({entries, interval}) => { +export const DashboardLineChart: React.FC = ({entries, interval, total}) => { const indexedEntries: Indexed[] = entries .map((entry) => { return { @@ -45,7 +46,7 @@ export const DashboardLineChart: React.FC = ({entries, i - } /> + } /> dateFormat(moment(entry.start))} interval={'preserveStartEnd'} /> diff --git a/ui/src/dashboard/Entry/DashboardTable.tsx b/ui/src/dashboard/Entry/DashboardTable.tsx index 368b391..d28cb7c 100644 --- a/ui/src/dashboard/Entry/DashboardTable.tsx +++ b/ui/src/dashboard/Entry/DashboardTable.tsx @@ -11,6 +11,7 @@ interface DashboardTableProps { entries: Stats_stats[]; interval: StatsInterval; mode: 'vertical' | 'horizontal'; + total: boolean; } interface Indexed { @@ -19,16 +20,20 @@ interface Indexed { data: Record; } -export const DashboardTable: React.FC = ({entries, interval, mode}) => { +export const DashboardTable: React.FC = ({entries, interval, mode, total}) => { const indexedEntries: Indexed[] = entries .map((entry) => { - return { + const result = { start: entry.start, end: entry.end, data: entry.entries!.reduce((all: Record, current) => { return {...all, [current.key + ':' + current.value]: current.timeSpendInSeconds}; }, {}), }; + if (total) { + result.data['Total'] = Object.values(result.data).reduce((acc, value) => acc + value); + } + return result; }) .sort((left, right) => moment(left.start).diff(right.start)); const dateFormat = ofInterval(interval); diff --git a/ui/src/dashboard/Entry/EditPopup.tsx b/ui/src/dashboard/Entry/EditPopup.tsx index 0920405..5a5f66a 100644 --- a/ui/src/dashboard/Entry/EditPopup.tsx +++ b/ui/src/dashboard/Entry/EditPopup.tsx @@ -64,6 +64,7 @@ export const EditPopup: React.FC = ({entry, anchorEl, onChange: entryId: entry.id, entryType: entry.entryType, title: entry.title, + total: entry.total, stats: { tags: entry.statsSelection.tags, interval: entry.statsSelection.interval, diff --git a/ui/src/dashboard/Entry/TagTooltip.tsx b/ui/src/dashboard/Entry/TagTooltip.tsx index aa539d0..4100ee3 100644 --- a/ui/src/dashboard/Entry/TagTooltip.tsx +++ b/ui/src/dashboard/Entry/TagTooltip.tsx @@ -1,4 +1,4 @@ -import {TooltipProps} from 'recharts'; +import {TooltipProps, TooltipPayload} from 'recharts'; import {FInterval} from './dateformat'; import Paper from '@material-ui/core/Paper'; import {Typography} from '@material-ui/core'; @@ -6,24 +6,34 @@ import moment from 'moment-timezone'; import prettyMs from 'pretty-ms'; import * as React from 'react'; -export const TagTooltip = ({active, payload, dateFormat}: TooltipProps & {dateFormat: FInterval}) => { +export const TagTooltip = ({active, payload, dateFormat, total}: TooltipProps & {dateFormat: FInterval; total: boolean}) => { if (active && payload) { const first = payload[0]; const start = dateFormat(moment(first.payload.start)); const end = dateFormat(moment(first.payload.end)); + return ( {first && {start === end ? `${start}` : `${start} - ${end}`}} - {payload.map((entry) => { - return ( - - {entry.name}: {prettyMs((entry.payload.data[entry.name] as number) * 1000)} - - ); - })} + {payload.map((entry) => ( + + {entry.name}: {prettyMs((entry.payload.data[entry.name] as number) * 1000)} + + ))} + {total ? ( + + Total: + {prettyMs(sum(payload) * 1000)} + + ) : ( + undefined + )} ); } return null; }; + +const sum = (payload: readonly TooltipPayload[]): number => + payload.reduce((acc, entry) => (acc + entry.payload.data[entry.name]) as number, 0); diff --git a/ui/src/gql/dashboard.ts b/ui/src/gql/dashboard.ts index 6ae792d..a50f0ee 100644 --- a/ui/src/gql/dashboard.ts +++ b/ui/src/gql/dashboard.ts @@ -17,6 +17,7 @@ export const Dashboards = gql` items { id title + total entryType statsSelection { range { @@ -90,8 +91,14 @@ export const UpdatePos = gql` `; export const UpdateDashboardEntry = gql` - mutation UpdateDashboardEntry($entryId: Int!, $entryType: EntryType!, $title: String!, $stats: InputStatsSelection!) { - updateDashboardEntry(entryId: $entryId, entryType: $entryType, title: $title, stats: $stats) { + mutation UpdateDashboardEntry( + $entryId: Int! + $entryType: EntryType! + $title: String! + $total: Boolean! + $stats: InputStatsSelection! + ) { + updateDashboardEntry(entryId: $entryId, entryType: $entryType, title: $title, total: $total, stats: $stats) { id } } @@ -101,10 +108,18 @@ export const AddDashboardEntry = gql` $dashboardId: Int! $entryType: EntryType! $title: String! + $total: Boolean! $stats: InputStatsSelection! $pos: InputResponsiveDashboardEntryPos ) { - addDashboardEntry(dashboardId: $dashboardId, entryType: $entryType, title: $title, stats: $stats, pos: $pos) { + addDashboardEntry( + dashboardId: $dashboardId + entryType: $entryType + title: $title + total: $total + stats: $stats + pos: $pos + ) { id } }