@kubex/zinc 1.1.185 → 2.0.1
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/.browserslistrc +2 -0
- package/.github/workflows/js_build_and_deploy.yaml +54 -1
- package/.github/workflows/test.yaml +27 -0
- package/dist/chunks/zn.UWJLIXDT.js +1 -0
- package/dist/custom-elements.json +10319 -2666
- package/dist/vscode.html-custom-data.json +1012 -57
- package/dist/web-types.json +2097 -145
- package/dist/zn.d.ts +2294 -416
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +2310 -1637
- package/docs/_includes/default.njk +4 -1
- package/docs/assets/scripts/docs.js +22 -6
- package/docs/assets/styles/docs.css +9 -2
- package/docs/data/report-table-day-compare.json +380 -0
- package/docs/data/report-table.json +314 -0
- package/docs/pages/components/avatar-group.md +57 -0
- package/docs/pages/components/badge.md +67 -0
- package/docs/pages/components/button.md +41 -1
- package/docs/pages/components/chat-message.md +16 -0
- package/docs/pages/components/checkbox.md +1 -1
- package/docs/pages/components/chip.md +25 -13
- package/docs/pages/components/color-picker.md +37 -0
- package/docs/pages/components/copy-button.md +2 -2
- package/docs/pages/components/data-table-filter.md +33 -50
- package/docs/pages/components/data-table-search.md +4 -4
- package/docs/pages/components/data-table.md +27 -3
- package/docs/pages/components/date-range.md +116 -0
- package/docs/pages/components/dialog.md +29 -37
- package/docs/pages/components/diff-row.md +28 -0
- package/docs/pages/components/diff.md +46 -0
- package/docs/pages/components/editor.md +55 -0
- package/docs/pages/components/icon.md +46 -1
- package/docs/pages/components/input.md +30 -11
- package/docs/pages/components/item.md +35 -0
- package/docs/pages/components/key-value.md +25 -0
- package/docs/pages/components/menu.md +74 -7
- package/docs/pages/components/nav-item.md +15 -0
- package/docs/pages/components/nav-list.md +132 -0
- package/docs/pages/components/navbar.md +100 -1
- package/docs/pages/components/page.md +242 -74
- package/docs/pages/components/priority-list.md +43 -5
- package/docs/pages/components/query-builder.md +23 -0
- package/docs/pages/components/radio-group.md +33 -1
- package/docs/pages/components/radio.md +1 -1
- package/docs/pages/components/report-table.md +202 -0
- package/docs/pages/components/stat.md +30 -0
- package/docs/pages/components/tile.md +11 -0
- package/docs/pages/components/tooltip.md +1 -1
- package/docs/pages/components/translation-grid.md +86 -0
- package/docs/pages/components/vertical-stepper.md +108 -64
- package/package.json +2 -1
- package/scss/_global-spacing.scss +2 -2
- package/scss/_root.scss +11 -171
- package/scss/boot.scss +14 -0
- package/scss/mixins/_helper-mixins.scss +24 -2
- package/scss/mixins/_motion-mixins.scss +126 -0
- package/scss/mixins/_spacing-mixins.scss +4 -3
- package/scss/mixins/_tab-indicator-mixins.scss +12 -0
- package/scss/mixins/_typography-mixins.scss +1 -1
- package/scss/mixins/index.scss +14 -0
- package/scss/shared/_base.scss +32 -1
- package/scss/shared/_button-mixin.scss +7 -5
- package/scss/shared/_table.scss +35 -4
- package/scss/shared/nav.scss +4 -0
- package/scss/shared/shimmer.scss +1 -1
- package/scss/table-controls.scss +206 -0
- package/scss/themes/_dark.scss +1 -3
- package/scss/themes/_index.scss +1 -1
- package/scss/themes/_light.scss +46 -6
- package/scss/themes/_modes.scss +11 -11
- package/scss/themes/_openbyte.scss +944 -0
- package/scss/themes/_typography.scss +4 -2
- package/scss/variables/index.scss +0 -9
- package/src/components/alert/alert.component.ts +14 -4
- package/src/components/alert/alert.scss +55 -69
- package/src/components/alert/alert.test.ts +8 -0
- package/src/components/animated-button/animated-button.component.ts +3 -2
- package/src/components/animated-button/animated-button.scss +6 -23
- package/src/components/animated-button/animated-button.test.ts +4 -3
- package/src/components/avatar-group/avatar-group.component.ts +84 -0
- package/src/components/avatar-group/avatar-group.scss +54 -0
- package/src/components/avatar-group/avatar-group.test.ts +59 -0
- package/src/components/avatar-group/index.ts +12 -0
- package/src/components/badge/badge.component.ts +49 -0
- package/src/components/badge/badge.scss +80 -0
- package/src/components/badge/badge.test.ts +34 -0
- package/src/components/badge/index.ts +12 -0
- package/src/components/bulk-actions/bulk-actions.component.ts +14 -2
- package/src/components/button/button.component.ts +228 -8
- package/src/components/button/button.scss +115 -32
- package/src/components/button/button.test.ts +122 -0
- package/src/components/button-menu/button-menu.component.ts +32 -11
- package/src/components/button-menu/button-menu.scss +5 -1
- package/src/components/channel-tile/channel-tile.component.ts +34 -27
- package/src/components/channel-tile/channel-tile.scss +76 -43
- package/src/components/chart/builders.test.ts +46 -0
- package/src/components/chart/builders.ts +27 -3
- package/src/components/chart/chart-live.test.ts +4 -6
- package/src/components/chart/chart.component.ts +125 -5
- package/src/components/chart/chart.scss +121 -0
- package/src/components/chart/chart.test.ts +81 -0
- package/src/components/chat-message/chat-message.component.ts +21 -1
- package/src/components/chat-message/chat-message.scss +13 -2
- package/src/components/chat-message/chat-message.test.ts +9 -0
- package/src/components/checkbox/checkbox.component.ts +61 -27
- package/src/components/checkbox/checkbox.scss +58 -25
- package/src/components/checkbox-group/checkbox-group.component.ts +7 -1
- package/src/components/chip/chip.component.ts +11 -1
- package/src/components/chip/chip.scss +91 -26
- package/src/components/chip/chip.test.ts +17 -0
- package/src/components/collapsible/collapsible.component.ts +17 -3
- package/src/components/collapsible/collapsible.scss +19 -10
- package/src/components/color-picker/color-picker.component.ts +277 -0
- package/src/components/color-picker/color-picker.scss +141 -0
- package/src/components/color-picker/color-picker.test.ts +57 -0
- package/src/components/color-picker/index.ts +12 -0
- package/src/components/cols/cols.component.ts +10 -0
- package/src/components/cols/cols.test.ts +17 -0
- package/src/components/confirm/confirm.component.ts +20 -6
- package/src/components/confirm/confirm.test.ts +52 -0
- package/src/components/content-block/content-block.component.ts +38 -9
- package/src/components/content-block/content-block.scss +11 -5
- package/src/components/content-block/content-block.test.ts +26 -0
- package/src/components/copy-button/copy-button.component.ts +2 -2
- package/src/components/copy-button/copy-button.scss +17 -4
- package/src/components/data-select/data-select.component.ts +3 -1
- package/src/components/data-table/data-table.component.ts +362 -140
- package/src/components/data-table/data-table.scss +136 -60
- package/src/components/data-table/data-table.test.ts +356 -4
- package/src/components/data-table-filter/data-table-filter.component.ts +426 -406
- package/src/components/data-table-filter/data-table-filter.scss +176 -148
- package/src/components/data-table-filter/data-table-filter.test.ts +271 -273
- package/src/components/data-table-search/data-table-search.component.ts +4 -2
- package/src/components/data-table-search/data-table-search.scss +8 -1
- package/src/components/data-table-search/data-table-search.test.ts +29 -0
- package/src/components/date-range/date-range-math.test.ts +78 -0
- package/src/components/date-range/date-range-math.ts +105 -0
- package/src/components/date-range/date-range.component.ts +375 -0
- package/src/components/date-range/date-range.scss +272 -0
- package/src/components/date-range/date-range.test.ts +247 -0
- package/src/components/date-range/index.ts +12 -0
- package/src/components/datepicker/datepicker.component.ts +44 -7
- package/src/components/datepicker/datepicker.scss +9 -0
- package/src/components/datepicker/datepicker.test.ts +2 -2
- package/src/components/dialog/dialog.component.ts +38 -19
- package/src/components/dialog/dialog.scss +65 -36
- package/src/components/dialog/dialog.test.ts +14 -0
- package/src/components/diff/diff.component.ts +55 -0
- package/src/components/diff/diff.scss +46 -0
- package/src/components/diff/diff.test.ts +15 -0
- package/src/components/diff/index.ts +12 -0
- package/src/components/diff-row/diff-row.component.ts +75 -0
- package/src/components/diff-row/diff-row.scss +70 -0
- package/src/components/diff-row/diff-row.test.ts +37 -0
- package/src/components/diff-row/index.ts +12 -0
- package/src/components/dropdown/dropdown.component.ts +12 -10
- package/src/components/dropdown/dropdown.scss +2 -0
- package/src/components/editor/editor.component.ts +159 -11
- package/src/components/editor/editor.scss +250 -1
- package/src/components/editor/editor.test.ts +31 -3
- package/src/components/editor/modules/ai/panel/ai-panel.scss +0 -1
- package/src/components/editor/modules/attachment/attachment.ts +53 -31
- package/src/components/editor/modules/context-menu/context-menu.ts +17 -0
- package/src/components/editor/modules/dialog/dialog.component.ts +23 -1
- package/src/components/editor/modules/dialog/dialog.test.ts +28 -0
- package/src/components/editor/modules/emoji/emoji.ts +4 -5
- package/src/components/editor/modules/merge-field/merge-field.ts +83 -0
- package/src/components/editor/modules/toolbar/toolbar.component.ts +121 -68
- package/src/components/editor/modules/toolbar/toolbar.scss +38 -0
- package/src/components/editor/modules/toolbar/toolbar.ts +25 -1
- package/src/components/empty-state/empty-state.component.ts +6 -1
- package/src/components/empty-state/empty-state.scss +21 -4
- package/src/components/expanding-action/expanding-action.component.ts +22 -34
- package/src/components/expanding-action/expanding-action.scss +3 -45
- package/src/components/expanding-action/expanding-action.test.ts +2 -1
- package/src/components/file/file.component.ts +30 -2
- package/src/components/file/file.scss +6 -0
- package/src/components/flow-builder/flow-builder.component.ts +36 -3
- package/src/components/flow-builder/flow-builder.scss +0 -6
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +8 -0
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.scss +10 -5
- package/src/components/flow-builder/modules/flow-node/flow-node.component.ts +17 -0
- package/src/components/flow-builder/modules/flow-node/flow-node.scss +10 -0
- package/src/components/form-actions/form-actions.component.ts +48 -2
- package/src/components/form-actions/form-actions.scss +1 -1
- package/src/components/form-group/form-group.component.ts +10 -3
- package/src/components/form-group/form-group.scss +37 -4
- package/src/components/form-group/form-group.test.ts +10 -0
- package/src/components/header/header.component.ts +11 -4
- package/src/components/header/header.scss +47 -5
- package/src/components/hover-container/hover-container.component.ts +4 -5
- package/src/components/icon/icon.component.ts +95 -9
- package/src/components/icon/icon.scss +70 -6
- package/src/components/icon/icon.test.ts +55 -0
- package/src/components/icon-picker/icon-picker.component.ts +35 -1
- package/src/components/inline-edit/inline-edit.component.ts +38 -8
- package/src/components/inline-edit/inline-edit.scss +128 -19
- package/src/components/input/input.component.ts +173 -297
- package/src/components/input/input.scss +65 -43
- package/src/components/input/input.test.ts +86 -4
- package/src/components/item/item.component.ts +12 -0
- package/src/components/item/item.scss +102 -3
- package/src/components/item/item.test.ts +23 -0
- package/src/components/key/key.component.ts +2 -2
- package/src/components/key/key.scss +11 -0
- package/src/components/key-value/index.ts +12 -0
- package/src/components/key-value/key-value.component.ts +160 -0
- package/src/components/key-value/key-value.scss +20 -0
- package/src/components/key-value/key-value.test.ts +22 -0
- package/src/components/markdown-editor/markdown-editor.component.ts +8 -1
- package/src/components/menu/menu.component.ts +21 -2
- package/src/components/menu/menu.scss +43 -23
- package/src/components/menu/menu.test.ts +33 -0
- package/src/components/menu-item/menu-item.component.ts +13 -2
- package/src/components/menu-item/menu-item.scss +56 -23
- package/src/components/menu-item/menu-item.test.ts +31 -0
- package/src/components/menu-item/submenu-controller.ts +25 -15
- package/src/components/menu-label/index.ts +12 -0
- package/src/components/menu-label/menu-label.component.ts +30 -0
- package/src/components/menu-label/menu-label.scss +18 -0
- package/src/components/nav-item/index.ts +12 -0
- package/src/components/nav-item/nav-item.component.ts +83 -0
- package/src/components/nav-item/nav-item.scss +128 -0
- package/src/components/nav-list/index.ts +12 -0
- package/src/components/nav-list/nav-list.component.ts +313 -0
- package/src/components/nav-list/nav-list.scss +87 -0
- package/src/components/nav-list/nav-list.test.ts +250 -0
- package/src/components/navbar/navbar.component.ts +235 -171
- package/src/components/navbar/navbar.scss +333 -169
- package/src/components/navbar/navbar.test.ts +63 -143
- package/src/components/page/page.component.ts +114 -29
- package/src/components/page/page.scss +223 -95
- package/src/components/page/page.test.ts +214 -3
- package/src/components/page-builder/page-builder.component.ts +51 -5
- package/src/components/page-builder/page-builder.scss +6 -1
- package/src/components/page-nav/page-nav.component.ts +25 -3
- package/src/components/page-nav/page-nav.scss +10 -2
- package/src/components/pagination/pagination.scss +1 -1
- package/src/components/panel/panel.component.ts +14 -13
- package/src/components/panel/panel.scss +14 -10
- package/src/components/panel/panel.test.ts +42 -0
- package/src/components/popup/popup.component.ts +20 -13
- package/src/components/popup/popup.test.ts +21 -1
- package/src/components/priority-list/priority-list.component.ts +157 -172
- package/src/components/priority-list/priority-list.scss +113 -17
- package/src/components/priority-list/priority-list.test.ts +301 -3
- package/src/components/progress-bar/progress-bar.component.ts +1 -1
- package/src/components/progress-bar/progress-bar.scss +16 -4
- package/src/components/progress-tile/progress-tile.scss +1 -1
- package/src/components/query-builder/query-builder.component.ts +85 -10
- package/src/components/query-builder/query-builder.scss +7 -1
- package/src/components/query-builder/query-builder.test.ts +40 -0
- package/src/components/radio/radio.component.ts +57 -18
- package/src/components/radio/radio.scss +101 -26
- package/src/components/radio-group/radio-group.component.ts +45 -7
- package/src/components/radio-group/radio-group.scss +17 -0
- package/src/components/radio-group/radio-group.test.ts +63 -2
- package/src/components/rating/rating.component.ts +23 -0
- package/src/components/rating/rating.scss +1 -1
- package/src/components/remarkd-editor/remarkd-editor.component.ts +60 -5
- package/src/components/remarkd-editor/remarkd-editor.scss +12 -4
- package/src/components/report-table/index.ts +12 -0
- package/src/components/report-table/report-format.test.ts +86 -0
- package/src/components/report-table/report-format.ts +72 -0
- package/src/components/report-table/report-math.test.ts +74 -0
- package/src/components/report-table/report-math.ts +252 -0
- package/src/components/report-table/report-table.component.ts +681 -0
- package/src/components/report-table/report-table.scss +328 -0
- package/src/components/report-table/report-table.test.ts +702 -0
- package/src/components/reveal/reveal.component.ts +15 -1
- package/src/components/reveal/reveal.scss +5 -0
- package/src/components/schedule-builder/schedule-builder.component.ts +23 -8
- package/src/components/scroll-container/scroll-container.component.ts +19 -1
- package/src/components/scroll-container/scroll-container.scss +6 -1
- package/src/components/select/select.component.ts +52 -36
- package/src/components/select/select.scss +42 -25
- package/src/components/select/select.test.ts +14 -2
- package/src/components/settings-container/settings-container.scss +18 -5
- package/src/components/sidebar/sidebar.component.ts +8 -1
- package/src/components/sidebar/sidebar.scss +6 -1
- package/src/components/skeleton/skeleton.component.ts +3 -3
- package/src/components/skeleton/skeleton.scss +6 -6
- package/src/components/skeleton/skeleton.test.ts +9 -0
- package/src/components/slash-menu/slash-menu.scss +1 -1
- package/src/components/slideout/slideout.component.ts +20 -3
- package/src/components/slideout/slideout.scss +13 -6
- package/src/components/slideout/slideout.test.ts +19 -0
- package/src/components/split-button/split-button.component.ts +1 -1
- package/src/components/split-pane/split-pane.component.ts +71 -8
- package/src/components/split-pane/split-pane.scss +145 -41
- package/src/components/split-pane/split-pane.test.ts +23 -3
- package/src/components/stat/stat.component.ts +209 -14
- package/src/components/stat/stat.scss +305 -5
- package/src/components/stat/stat.test.ts +143 -2
- package/src/components/status-indicator/status-indicator.scss +22 -21
- package/src/components/stepper/stepper.scss +3 -3
- package/src/components/tab/tab.component.ts +1 -0
- package/src/components/table/table.component.ts +12 -1
- package/src/components/table/table.scss +21 -13
- package/src/components/table/table.test.ts +17 -0
- package/src/components/tabs/tabs.component.ts +150 -37
- package/src/components/tabs/tabs.scss +48 -7
- package/src/components/tabs/tabs.test.ts +169 -0
- package/src/components/textarea/textarea.component.ts +25 -3
- package/src/components/textarea/textarea.scss +10 -0
- package/src/components/textarea/textarea.test.ts +8 -4
- package/src/components/theme-editor/theme-editor.scss +0 -6
- package/src/components/tile/tile.component.ts +32 -2
- package/src/components/tile/tile.scss +40 -7
- package/src/components/tile/tile.test.ts +10 -0
- package/src/components/toggle/toggle.component.ts +23 -7
- package/src/components/toggle/toggle.scss +36 -29
- package/src/components/toggle/toggle.test.ts +17 -0
- package/src/components/tooltip/tooltip.component.ts +3 -0
- package/src/components/tooltip/tooltip.scss +11 -6
- package/src/components/tooltip/tooltip.test.ts +12 -0
- package/src/components/translation-field/index.ts +12 -0
- package/src/components/translation-field/translation-field.component.ts +55 -0
- package/src/components/translation-field/translation-field.scss +101 -0
- package/src/components/translation-fields/index.ts +12 -0
- package/src/components/translation-fields/translation-fields.component.ts +80 -0
- package/src/components/translation-fields/translation-fields.scss +60 -0
- package/src/components/translation-grid/index.ts +12 -0
- package/src/components/translation-grid/translation-grid.component.ts +157 -0
- package/src/components/translation-grid/translation-grid.scss +138 -0
- package/src/components/translation-grid/translation-grid.test.ts +28 -0
- package/src/components/vertical-stepper/vertical-stepper.component.ts +35 -9
- package/src/components/vertical-stepper/vertical-stepper.scss +110 -39
- package/src/components/vertical-stepper/vertical-stepper.test.ts +35 -0
- package/src/components/well/well.scss +2 -2
- package/src/events/events.ts +2 -0
- package/src/events/zn-chart-click.ts +13 -0
- package/src/events/zn-report-change.ts +7 -0
- package/src/form-control.scss +36 -3
- package/src/internal/anchored-popover.ts +175 -0
- package/src/internal/animate.ts +19 -5
- package/src/internal/cosmic-border.ts +15 -0
- package/src/internal/form.ts +1 -1
- package/src/internal/hover-intent.ts +15 -0
- package/src/internal/input-modality.ts +15 -0
- package/src/internal/pointer-intent.ts +37 -0
- package/src/internal/reorder.ts +632 -0
- package/src/internal/scroll.ts +51 -3
- package/src/internal/selection-card.ts +1 -1
- package/src/internal/tab-indicator.ts +92 -0
- package/src/internal/table-sort.ts +25 -0
- package/src/internal/table-tabs.ts +108 -0
- package/src/internal/test-drag.ts +35 -0
- package/src/internal/theme-openbyte.test.ts +126 -0
- package/src/internal/theme.ts +4 -4
- package/src/internal/zinc-element.ts +2 -2
- package/src/selection-card.scss +25 -0
- package/src/types/web-test-runner-commands.d.ts +1 -0
- package/src/utilities/color.test.ts +43 -0
- package/src/utilities/color.ts +189 -0
- package/src/zinc.ts +14 -0
- package/.github/workflows/eleventy_build.yml +0 -42
- package/scss/themes/_aura.scss +0 -114
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import {AnchoredPopoverController} from '../../internal/anchored-popover';
|
|
2
|
+
import {classMap} from 'lit/directives/class-map.js';
|
|
3
|
+
import {
|
|
4
|
+
columnValue,
|
|
5
|
+
computeTotals,
|
|
6
|
+
type ReportColumn,
|
|
7
|
+
type ReportFilterField,
|
|
8
|
+
type ReportGroup,
|
|
9
|
+
type ReportLink,
|
|
10
|
+
type ReportOption,
|
|
11
|
+
type ReportResponse,
|
|
12
|
+
type ReportRow,
|
|
13
|
+
type ReportValues
|
|
14
|
+
} from './report-math';
|
|
15
|
+
import {type CSSResultGroup, html, nothing, type PropertyValues, type TemplateResult, unsafeCSS} from 'lit';
|
|
16
|
+
import {type DatePreset, todayIso} from '../date-range/date-range-math';
|
|
17
|
+
import {formatValue, toneClass, unitLabel} from './report-format';
|
|
18
|
+
import {HasSlotController} from '../../internal/slot';
|
|
19
|
+
import {ifDefined} from 'lit/directives/if-defined.js';
|
|
20
|
+
import {property, state} from 'lit/decorators.js';
|
|
21
|
+
import {ref} from 'lit/directives/ref.js';
|
|
22
|
+
import {renderSortIcon, sortKeyHandler, type SortState} from '../../internal/table-sort';
|
|
23
|
+
import {repeat} from 'lit/directives/repeat.js';
|
|
24
|
+
import {styleMap} from 'lit/directives/style-map.js';
|
|
25
|
+
import {TabRowController} from '../../internal/table-tabs';
|
|
26
|
+
import {Task} from '@lit/task';
|
|
27
|
+
import ZincElement from '../../internal/zinc-element';
|
|
28
|
+
import ZnAlert from '../alert';
|
|
29
|
+
import ZnButton from '../button';
|
|
30
|
+
import ZnCheckbox from '../checkbox';
|
|
31
|
+
import ZnDataTableFilter from '../data-table-filter';
|
|
32
|
+
import ZnDataTableSearch from '../data-table-search';
|
|
33
|
+
import ZnDateRange from '../date-range';
|
|
34
|
+
import ZnEmptyState from '../empty-state';
|
|
35
|
+
import ZnIcon from '../icon';
|
|
36
|
+
import ZnPanel from '../panel';
|
|
37
|
+
import ZnSkeleton from '../skeleton';
|
|
38
|
+
|
|
39
|
+
import styles from './report-table.scss';
|
|
40
|
+
import tableControlsStyles from '../../../scss/table-controls.scss';
|
|
41
|
+
|
|
42
|
+
const DIMENSION = '__dim';
|
|
43
|
+
const SHARABLE_KEYS = ['type', 'groupBy', 'from', 'to', 'filter', 'search'] as const;
|
|
44
|
+
type SharableKey = typeof SHARABLE_KEYS[number];
|
|
45
|
+
type ReportArgs = readonly [string, string, string, string, string, string];
|
|
46
|
+
|
|
47
|
+
const INPUT_TAGS = ['zn-input', 'zn-select', 'zn-query-builder', 'zn-multiselect', 'zn-params-select', 'zn-datepicker', 'input', 'select', 'textarea'];
|
|
48
|
+
|
|
49
|
+
const asArray = <T>(value: unknown): T[] => (Array.isArray(value) ? value as T[] : []);
|
|
50
|
+
const asRecord = <T>(value: unknown): T | undefined =>
|
|
51
|
+
value !== null && typeof value === 'object' && !Array.isArray(value) ? value as T : undefined;
|
|
52
|
+
|
|
53
|
+
function normalize(raw: unknown): ReportResponse {
|
|
54
|
+
const data = asRecord<ReportResponse>(raw);
|
|
55
|
+
const response: ReportResponse = {
|
|
56
|
+
...data,
|
|
57
|
+
groups: asArray<ReportGroup>(data?.groups).map(group => ({...group, columns: asArray<ReportColumn>(group?.columns)})),
|
|
58
|
+
rows: asArray<ReportRow>(data?.rows).map(row => ({...row, values: asRecord<ReportValues>(row?.values) ?? {}})),
|
|
59
|
+
totals: asRecord<Record<string, number>>(data?.totals),
|
|
60
|
+
};
|
|
61
|
+
return response;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function rawValue(values: ReportValues, path: string): unknown {
|
|
65
|
+
let current: unknown = values;
|
|
66
|
+
for (const part of path.split('.')) {
|
|
67
|
+
const record = asRecord<ReportValues>(current);
|
|
68
|
+
current = record && Object.prototype.hasOwnProperty.call(record, part) ? record[part] : undefined;
|
|
69
|
+
}
|
|
70
|
+
return current;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @summary A grouped report: one row per value of a dimension, read column by column, with a total row.
|
|
75
|
+
* @documentation https://zinc.style/components/report-table
|
|
76
|
+
* @status experimental
|
|
77
|
+
* @since 1.0
|
|
78
|
+
*
|
|
79
|
+
* @dependency zn-alert
|
|
80
|
+
* @dependency zn-button
|
|
81
|
+
* @dependency zn-checkbox
|
|
82
|
+
* @dependency zn-data-table-filter
|
|
83
|
+
* @dependency zn-data-table-search
|
|
84
|
+
* @dependency zn-date-range
|
|
85
|
+
* @dependency zn-empty-state
|
|
86
|
+
* @dependency zn-icon
|
|
87
|
+
* @dependency zn-panel
|
|
88
|
+
* @dependency zn-skeleton
|
|
89
|
+
*
|
|
90
|
+
* @event zn-report-change - Emitted when the type, group by, date range or filter is changed, before the report is fetched again.
|
|
91
|
+
*
|
|
92
|
+
* @slot inputs - Inputs whose name and value are sent with every request (csrf tokens and similar).
|
|
93
|
+
* @slot empty-state - Replaces the default empty state.
|
|
94
|
+
*/
|
|
95
|
+
export default class ZnReportTable extends ZincElement {
|
|
96
|
+
static styles: CSSResultGroup = [unsafeCSS(styles), unsafeCSS(tableControlsStyles)];
|
|
97
|
+
static dependencies = {
|
|
98
|
+
'zn-alert': ZnAlert,
|
|
99
|
+
'zn-button': ZnButton,
|
|
100
|
+
'zn-checkbox': ZnCheckbox,
|
|
101
|
+
'zn-date-range': ZnDateRange,
|
|
102
|
+
'zn-empty-state': ZnEmptyState,
|
|
103
|
+
'zn-icon': ZnIcon,
|
|
104
|
+
'zn-data-table-filter': ZnDataTableFilter,
|
|
105
|
+
'zn-data-table-search': ZnDataTableSearch,
|
|
106
|
+
'zn-panel': ZnPanel,
|
|
107
|
+
'zn-skeleton': ZnSkeleton,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
@property({attribute: 'data-uri'}) dataUri: string;
|
|
111
|
+
@property() method: 'GET' | 'POST' = 'POST';
|
|
112
|
+
@property() caption = '';
|
|
113
|
+
@property({reflect: true}) type = '';
|
|
114
|
+
@property({attribute: 'group-by', reflect: true}) groupBy = '';
|
|
115
|
+
@property({reflect: true}) from = '';
|
|
116
|
+
@property({reflect: true}) to = '';
|
|
117
|
+
@property({reflect: true}) filter = '';
|
|
118
|
+
@property() currency = 'USD';
|
|
119
|
+
@property() locale: string;
|
|
120
|
+
|
|
121
|
+
/** Report types, shown as tabs when there are two or more. `type` defaults to the first. */
|
|
122
|
+
@property({type: Array}) types: ReportOption[] = [];
|
|
123
|
+
|
|
124
|
+
/** Group by options; those with a `category` are listed under its heading. */
|
|
125
|
+
@property({attribute: 'group-bys', type: Array}) groupBys: ReportOption[] = [];
|
|
126
|
+
|
|
127
|
+
/** Fields for the funnel's filter builder. The funnel is hidden when there are none. */
|
|
128
|
+
@property({attribute: 'filter-fields', type: Array}) filterFields: ReportFilterField[] = [];
|
|
129
|
+
|
|
130
|
+
/** Quick ranges for the date range, replacing the default ones. */
|
|
131
|
+
@property({type: Array}) presets: DatePreset[] | undefined;
|
|
132
|
+
|
|
133
|
+
/** The latest day that can be picked. Defaults to today. */
|
|
134
|
+
@property({attribute: 'max-date'}) maxDate = '';
|
|
135
|
+
|
|
136
|
+
/** A short line shown under the date range's calendar. */
|
|
137
|
+
@property({attribute: 'date-note'}) dateNote = '';
|
|
138
|
+
|
|
139
|
+
@property({attribute: 'search-placeholder'}) searchPlaceholder = '';
|
|
140
|
+
|
|
141
|
+
/** Mirrors the type, group by, dates, filter and search to the URL query string, and reads them back on load. */
|
|
142
|
+
@property({type: Boolean}) sharable = false;
|
|
143
|
+
|
|
144
|
+
/** Leaves out columns that are zero or empty in every row, and groups left without columns. */
|
|
145
|
+
@property({attribute: 'hide-empty-columns', type: Boolean}) hideEmptyColumns = false;
|
|
146
|
+
|
|
147
|
+
/** Gives each row a tick box that highlights it, with one in the heading for every row shown. */
|
|
148
|
+
@property({type: Boolean}) selectable = false;
|
|
149
|
+
|
|
150
|
+
/** Shades number cells by their size against the largest in their column. */
|
|
151
|
+
@property({type: Boolean}) heatmap = false;
|
|
152
|
+
|
|
153
|
+
@state() protected response: ReportResponse | null = null;
|
|
154
|
+
@state() protected search = '';
|
|
155
|
+
@state() private sortKey = '';
|
|
156
|
+
@state() private sortDirection: 'asc' | 'desc' = 'desc';
|
|
157
|
+
@state() private selected = new Set<string>();
|
|
158
|
+
|
|
159
|
+
private sharableDefaults: Record<SharableKey, string> | null = null;
|
|
160
|
+
private adoption: {from: string; to: string} | null = null;
|
|
161
|
+
private dimObserver: ResizeObserver | null = null;
|
|
162
|
+
|
|
163
|
+
private readonly groupByPopover = new AnchoredPopoverController(this, {
|
|
164
|
+
trigger: '.report__groupby',
|
|
165
|
+
popover: '.report__gpop',
|
|
166
|
+
width: () => Math.min(this.hasCategories() ? 560 : 506, window.innerWidth * 0.94),
|
|
167
|
+
align: 'end',
|
|
168
|
+
focus: ['.report__gopt[aria-current="true"]', '.report__gopt'],
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
private readonly filterEl = document.createElement('zn-data-table-filter');
|
|
172
|
+
|
|
173
|
+
private readonly tabRow = new TabRowController(this, {onSelect: (value: string) => {
|
|
174
|
+
if (value !== this.type) this.change({type: value});
|
|
175
|
+
}});
|
|
176
|
+
|
|
177
|
+
private readonly hasSlotController = new HasSlotController(this, 'inputs', 'empty-state');
|
|
178
|
+
|
|
179
|
+
private readonly _task = new Task(this, {
|
|
180
|
+
task: async ([dataUri], {signal}) => {
|
|
181
|
+
if (!dataUri) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const body = {type: this.type, groupBy: this.groupBy, from: this.from, to: this.to, filter: this.filter, ...this.inputParams()};
|
|
185
|
+
const response = await fetch(dataUri, {
|
|
186
|
+
method: this.method,
|
|
187
|
+
headers: {'x-kx-fetch-style': 'zn-report-table'},
|
|
188
|
+
signal,
|
|
189
|
+
credentials: 'same-origin',
|
|
190
|
+
body: this.method === 'POST' ? JSON.stringify(body) : undefined,
|
|
191
|
+
});
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
throw new Error(response.statusText);
|
|
194
|
+
}
|
|
195
|
+
return normalize(await response.json());
|
|
196
|
+
},
|
|
197
|
+
args: (): ReportArgs => [this.dataUri, this.type, this.groupBy, this.from, this.to, this.filter],
|
|
198
|
+
// A group by adopted from the response is what was just fetched, so it doesn't fetch again
|
|
199
|
+
argsEqual: (previous: ReportArgs, next: ReportArgs) => {
|
|
200
|
+
const adoption = this.adoption;
|
|
201
|
+
this.adoption = null;
|
|
202
|
+
return previous.every((value, index) => value === next[index]
|
|
203
|
+
|| (index === 2 && adoption?.from === value && adoption.to === next[index]));
|
|
204
|
+
},
|
|
205
|
+
onComplete: (result) => {
|
|
206
|
+
this.response = result;
|
|
207
|
+
if (result?.groupBy && result.groupBy !== this.groupBy) {
|
|
208
|
+
this.adoption = {from: this.groupBy, to: result.groupBy};
|
|
209
|
+
this.groupBy = result.groupBy;
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
connectedCallback() {
|
|
215
|
+
super.connectedCallback();
|
|
216
|
+
this.filterEl.addEventListener('zn-filter-change', this.handleFilter);
|
|
217
|
+
this.readSharableState();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
protected willUpdate() {
|
|
221
|
+
if (!this.type && this.types.length > 0) {
|
|
222
|
+
this.type = this.types[0].value;
|
|
223
|
+
}
|
|
224
|
+
this.filterEl.filters = this.activeFilterFields();
|
|
225
|
+
this.filterEl.value = this.filter;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
protected updated(changed: PropertyValues) {
|
|
229
|
+
if (this.sharable && SHARABLE_KEYS.some(key => changed.has(key))) {
|
|
230
|
+
this.writeSharableState();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
refresh(): Promise<void> {
|
|
235
|
+
return this._task.run();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
protected visibleRows(): ReportRow[] {
|
|
239
|
+
const query = this.search.trim().toLowerCase();
|
|
240
|
+
const rows = (this.response?.rows ?? []).filter(row => !query || String(row.label ?? '').toLowerCase().includes(query));
|
|
241
|
+
return this.sortRows(rows);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
protected totalsFor(rows: ReportRow[]): Record<string, number | null> {
|
|
245
|
+
const columns = this.columns();
|
|
246
|
+
const server = this.response?.totals;
|
|
247
|
+
if (server && !this.search.trim()) {
|
|
248
|
+
return Object.fromEntries(columns.map(column => [column.key, typeof server[column.key] === 'number' ? server[column.key] : null]));
|
|
249
|
+
}
|
|
250
|
+
return computeTotals(columns, rows);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
protected columns(): ReportColumn[] {
|
|
254
|
+
return (this.response?.groups ?? []).flatMap(group => group.columns);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private sortRows(rows: ReportRow[]): ReportRow[] {
|
|
258
|
+
if (!this.sortKey) return rows;
|
|
259
|
+
const direction = this.sortDirection === 'asc' ? 1 : -1;
|
|
260
|
+
const column = this.columns().find(c => c.key === this.sortKey);
|
|
261
|
+
if (this.sortKey !== DIMENSION && !column) return rows;
|
|
262
|
+
|
|
263
|
+
const textOf = (row: ReportRow) => this.sortKey === DIMENSION ? String(row.label ?? '') : String(this.cellValue(column!, row) ?? '');
|
|
264
|
+
const compare = this.sortKey === DIMENSION || column!.format === 'text'
|
|
265
|
+
? (a: ReportRow, b: ReportRow) => textOf(a).localeCompare(textOf(b), undefined, {numeric: true})
|
|
266
|
+
: (a: ReportRow, b: ReportRow) => columnValue(column!, a.values) - columnValue(column!, b.values);
|
|
267
|
+
return [...rows].sort((a, b) => compare(a, b) * direction);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private sortState(key: string): SortState {
|
|
271
|
+
if (this.sortKey !== key) return 'none';
|
|
272
|
+
return this.sortDirection === 'asc' ? 'ascending' : 'descending';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private cycleSort(key: string) {
|
|
276
|
+
if (this.sortKey !== key) {
|
|
277
|
+
this.sortKey = key;
|
|
278
|
+
this.sortDirection = 'desc';
|
|
279
|
+
} else if (this.sortDirection === 'desc') {
|
|
280
|
+
this.sortDirection = 'asc';
|
|
281
|
+
} else {
|
|
282
|
+
this.sortKey = '';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private change(patch: Partial<Pick<ZnReportTable, 'type' | 'groupBy' | 'from' | 'to' | 'filter'>>) {
|
|
287
|
+
if (patch.groupBy !== undefined && patch.groupBy !== this.groupBy) {
|
|
288
|
+
this.selected = new Set();
|
|
289
|
+
}
|
|
290
|
+
Object.assign(this, patch);
|
|
291
|
+
this.emit('zn-report-change', {
|
|
292
|
+
detail: {type: this.type, groupBy: this.groupBy, from: this.from, to: this.to, filter: this.filter},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private activeGroupBys(): ReportOption[] {
|
|
297
|
+
return this.response?.groupBys ?? this.groupBys;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private activeFilterFields(): ReportFilterField[] {
|
|
301
|
+
return this.response?.filterFields ?? this.filterFields;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private hasCategories(): boolean {
|
|
305
|
+
return this.activeGroupBys().some(option => !!option.category);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private dimensionLabel(): string {
|
|
309
|
+
return this.response?.dimension?.label ?? '';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private readSharableState() {
|
|
313
|
+
if (!this.sharable || this.sharableDefaults) return;
|
|
314
|
+
this.sharableDefaults = {type: this.type, groupBy: this.groupBy, from: this.from, to: this.to, filter: this.filter, search: this.search};
|
|
315
|
+
const params = new URLSearchParams(window.location.search);
|
|
316
|
+
SHARABLE_KEYS.forEach(key => {
|
|
317
|
+
const value = params.get(key);
|
|
318
|
+
if (value !== null) this[key] = value;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private writeSharableState() {
|
|
323
|
+
const defaults = this.sharableDefaults;
|
|
324
|
+
if (!defaults) return;
|
|
325
|
+
const params = new URLSearchParams(window.location.search);
|
|
326
|
+
SHARABLE_KEYS.forEach(key => {
|
|
327
|
+
const fallback = key === 'type' ? defaults.type || this.types[0]?.value || '' : defaults[key];
|
|
328
|
+
const value = this[key];
|
|
329
|
+
if (value && value !== fallback) {
|
|
330
|
+
params.set(key, value);
|
|
331
|
+
} else {
|
|
332
|
+
params.delete(key);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
const query = params.toString();
|
|
336
|
+
window.history.replaceState(window.history.state, '', `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private inputParams(): Record<string, unknown> {
|
|
340
|
+
const params: Record<string, unknown> = {};
|
|
341
|
+
this.hasSlotController.getSlots('inputs').forEach(input => {
|
|
342
|
+
if (!INPUT_TAGS.includes(input.tagName.toLowerCase())) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const field = input as HTMLInputElement;
|
|
346
|
+
const raw = field.value || input.getAttribute('value');
|
|
347
|
+
const name = field.name || input.getAttribute('name');
|
|
348
|
+
if (name) {
|
|
349
|
+
params[name] = typeof raw === 'string' ? raw.trim() : raw;
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
return params;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private format(column: ReportColumn, value: number | string | null): string {
|
|
356
|
+
return formatValue(column, value, {currency: this.response?.currency ?? this.currency ?? 'USD', locale: this.locale});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private cellValue(column: ReportColumn, row: ReportRow): number | string | null {
|
|
360
|
+
if (column.format === 'text') {
|
|
361
|
+
const raw = rawValue(row.values, column.value ?? column.key);
|
|
362
|
+
return raw === undefined || raw === null || typeof raw === 'object' ? '' : String(raw);
|
|
363
|
+
}
|
|
364
|
+
return columnValue(column, row.values);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// The console only routes links without a native target, so the target is passed as data-target
|
|
368
|
+
private renderLink(link: ReportLink, content: string): TemplateResult {
|
|
369
|
+
return html`<a href="${link.uri}" data-target="${ifDefined(link.target || undefined)}">${content}</a>`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private cellClasses(column: ReportColumn, value: number | string | null, first: boolean) {
|
|
373
|
+
const tone = typeof value === 'number' ? toneClass(column, value) : '';
|
|
374
|
+
return {
|
|
375
|
+
report__num: column.format !== 'text',
|
|
376
|
+
report__sep: first,
|
|
377
|
+
report__bold: column.style === 'bold',
|
|
378
|
+
report__pos: tone === 'report__pos',
|
|
379
|
+
report__neg: tone === 'report__neg',
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private displayGroups(response: ReportResponse): ReportGroup[] {
|
|
384
|
+
if (!this.hideEmptyColumns) return response.groups;
|
|
385
|
+
const filled = (column: ReportColumn) => response.rows.some(row => {
|
|
386
|
+
const value = this.cellValue(column, row);
|
|
387
|
+
return typeof value === 'number' ? value !== 0 : value !== null && value !== '';
|
|
388
|
+
});
|
|
389
|
+
return response.groups
|
|
390
|
+
.map(group => ({...group, columns: group.columns.filter(filled)}))
|
|
391
|
+
.filter(group => group.columns.length > 0);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private heatScales(groups: ReportGroup[], rows: ReportRow[]): Map<string, number> {
|
|
395
|
+
const scales = new Map<string, number>();
|
|
396
|
+
if (!this.heatmap) return scales;
|
|
397
|
+
groups.flatMap(group => group.columns).filter(column => column.format !== 'text').forEach(column => {
|
|
398
|
+
const max = Math.max(0, ...rows.map(row => Math.abs(Number(this.cellValue(column, row)) || 0)));
|
|
399
|
+
if (max > 0) scales.set(column.key, max);
|
|
400
|
+
});
|
|
401
|
+
return scales;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private renderTable(response: ReportResponse, rows: ReportRow[], busy: boolean): TemplateResult {
|
|
405
|
+
const groups = this.displayGroups(response);
|
|
406
|
+
const scales = this.heatScales(groups, rows);
|
|
407
|
+
const totals = this.totalsFor(rows);
|
|
408
|
+
const dimension = response.dimension?.label ?? '';
|
|
409
|
+
|
|
410
|
+
return html`
|
|
411
|
+
<div class="table__frame">
|
|
412
|
+
${busy ? html`<div class="table__loader" aria-hidden="true"></div>` : nothing}
|
|
413
|
+
<div class="report__scroll table__enter ${busy ? 'report__scroll--busy' : ''}" aria-busy="${busy ? 'true' : 'false'}">
|
|
414
|
+
<table class="report">
|
|
415
|
+
<thead>
|
|
416
|
+
<tr class="report__groups">
|
|
417
|
+
<th class="report__dim" ${ref(this.observeDim)}></th>
|
|
418
|
+
${groups.map(group => html`
|
|
419
|
+
<th class="report__sep" colspan="${group.columns.length}"><span class="report__glabel">${group.label}</span></th>`)}
|
|
420
|
+
</tr>
|
|
421
|
+
<tr>
|
|
422
|
+
${this.renderHeading(DIMENSION, dimension, {report__dim: true}, this.selectable ? this.renderSelectAll(rows) : nothing)}
|
|
423
|
+
${groups.map(group => group.columns.map((column, index) => this.renderHeading(
|
|
424
|
+
column.key,
|
|
425
|
+
group.columns.length === 1 && column.label === group.label ? '' : column.label,
|
|
426
|
+
{report__num: column.format !== 'text', report__sep: index === 0},
|
|
427
|
+
nothing,
|
|
428
|
+
unitLabel(column, this.response?.currency ?? this.currency ?? 'USD'),
|
|
429
|
+
)))}
|
|
430
|
+
</tr>
|
|
431
|
+
</thead>
|
|
432
|
+
<tbody>
|
|
433
|
+
${repeat(rows, row => row.id, row => html`
|
|
434
|
+
<tr aria-selected="${ifDefined(this.selectable ? String(this.selected.has(row.id)) : undefined)}">
|
|
435
|
+
<td class="report__dim">
|
|
436
|
+
<span class="report__head">
|
|
437
|
+
${this.selectable ? html`
|
|
438
|
+
<zn-checkbox class="report__check" size="small" aria-label="Select ${row.label}"
|
|
439
|
+
.checked="${this.selected.has(row.id)}"
|
|
440
|
+
@zn-change="${() => this.toggleRow(row.id)}"></zn-checkbox>` : nothing}
|
|
441
|
+
${row.uri ? this.renderLink({uri: row.uri, target: row.target}, row.label) : row.label}
|
|
442
|
+
</span>
|
|
443
|
+
</td>
|
|
444
|
+
${groups.map(group => group.columns.map((column, index) => {
|
|
445
|
+
const value = this.cellValue(column, row);
|
|
446
|
+
const link = row.links?.[column.key];
|
|
447
|
+
const text = this.format(column, value);
|
|
448
|
+
const scale = scales.get(column.key);
|
|
449
|
+
const heat = scale && typeof value === 'number' && value !== 0 ? Math.abs(value) / scale : 0;
|
|
450
|
+
return html`
|
|
451
|
+
<td class=${classMap({...this.cellClasses(column, value, index === 0), report__heat: heat > 0, 'report__heat--neg': heat > 0 && (value as number) < 0})}
|
|
452
|
+
style=${styleMap({'--_heat': heat > 0 ? heat.toFixed(3) : undefined})}>${link ? this.renderLink(link, text) : text}</td>`;
|
|
453
|
+
}))}
|
|
454
|
+
</tr>`)}
|
|
455
|
+
</tbody>
|
|
456
|
+
<tfoot>
|
|
457
|
+
<tr class="report__total">
|
|
458
|
+
<td class="report__dim">Total</td>
|
|
459
|
+
${groups.map(group => group.columns.map((column, index) => {
|
|
460
|
+
const value = totals[column.key] ?? null;
|
|
461
|
+
return html`
|
|
462
|
+
<td class=${classMap(this.cellClasses(column, value, index === 0))}>${this.format(column, value)}</td>`;
|
|
463
|
+
}))}
|
|
464
|
+
</tr>
|
|
465
|
+
</tfoot>
|
|
466
|
+
</table>
|
|
467
|
+
</div>
|
|
468
|
+
</div>`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private renderHeading(key: string, label: string, classes: Record<string, boolean>, prefix: TemplateResult | typeof nothing = nothing, unit = ''): TemplateResult {
|
|
472
|
+
const sort = () => this.cycleSort(key);
|
|
473
|
+
return html`
|
|
474
|
+
<th class=${classMap(classes)} tabindex="0" aria-sort="${this.sortState(key)}"
|
|
475
|
+
@click="${sort}" @keydown="${sortKeyHandler(sort)}">
|
|
476
|
+
<span class="report__head">${prefix}${label}${unit ? html`<span class="report__unit">${unit}</span>` : nothing}${renderSortIcon(this.sortState(key))}</span>
|
|
477
|
+
</th>`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Inside a sortable heading, so its clicks and keys must not reach the heading's sort handlers
|
|
481
|
+
private renderSelectAll(rows: ReportRow[]): TemplateResult {
|
|
482
|
+
const count = rows.filter(row => this.selected.has(row.id)).length;
|
|
483
|
+
return html`
|
|
484
|
+
<zn-checkbox class="report__check" size="small" aria-label="Select all shown"
|
|
485
|
+
.checked="${count > 0 && count === rows.length}"
|
|
486
|
+
.indeterminate="${count > 0 && count < rows.length}"
|
|
487
|
+
@click="${(e: Event) => e.stopPropagation()}"
|
|
488
|
+
@keydown="${(e: Event) => e.stopPropagation()}"
|
|
489
|
+
@zn-change="${() => this.toggleAll(rows, count < rows.length)}"></zn-checkbox>`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private toggleRow(id: string) {
|
|
493
|
+
const selected = new Set(this.selected);
|
|
494
|
+
if (!selected.delete(id)) selected.add(id);
|
|
495
|
+
this.selected = selected;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private toggleAll(rows: ReportRow[], select: boolean) {
|
|
499
|
+
const selected = new Set(this.selected);
|
|
500
|
+
rows.forEach(row => select ? selected.add(row.id) : selected.delete(row.id));
|
|
501
|
+
this.selected = selected;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private renderBody(rows: ReportRow[], busy: boolean): TemplateResult {
|
|
505
|
+
const response = this.response;
|
|
506
|
+
if (!response) {
|
|
507
|
+
return busy ? this.renderSkeleton() : html``;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const alert = response.error
|
|
511
|
+
? html`
|
|
512
|
+
<zn-alert class="report__alert" level="${response.error.level ?? 'error'}" icon="${ifDefined(response.error.icon)}" size="small">
|
|
513
|
+
${response.error.text}
|
|
514
|
+
</zn-alert>`
|
|
515
|
+
: nothing;
|
|
516
|
+
|
|
517
|
+
if (rows.length === 0) {
|
|
518
|
+
if (busy) return this.renderSkeleton();
|
|
519
|
+
return html`${alert}
|
|
520
|
+
<div class="table__enter">
|
|
521
|
+
<slot name="empty-state">
|
|
522
|
+
<zn-empty-state caption="No results found" icon="search"
|
|
523
|
+
description="Nothing matched this report. Try another date range or filter."></zn-empty-state>
|
|
524
|
+
</slot>
|
|
525
|
+
</div>`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return html`${alert}${this.renderTable(response, rows, busy)}`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private renderSkeleton(): TemplateResult {
|
|
532
|
+
return html`
|
|
533
|
+
<div class="report__skeleton table__enter">
|
|
534
|
+
${[0, 1, 2, 3, 4].map(() => html`<zn-skeleton></zn-skeleton>`)}
|
|
535
|
+
</div>`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private renderRowCount(rows: ReportRow[]): string | undefined {
|
|
539
|
+
const total = this.response?.rows?.length;
|
|
540
|
+
if (total === undefined) return undefined;
|
|
541
|
+
const shown = rows.length;
|
|
542
|
+
const noun = total === 1 ? 'row' : 'rows';
|
|
543
|
+
return shown === total ? `${total} ${noun}` : `${shown} of ${total} ${noun}`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private renderGroupBy(): TemplateResult | typeof nothing {
|
|
547
|
+
const options = this.activeGroupBys();
|
|
548
|
+
if (options.length === 0) return nothing;
|
|
549
|
+
const current = options.find(option => option.value === this.groupBy)?.label || this.dimensionLabel() || this.groupBy;
|
|
550
|
+
|
|
551
|
+
return html`
|
|
552
|
+
<zn-button class="report__groupby" panel-bg size="small" aria-haspopup="dialog"
|
|
553
|
+
aria-expanded="${this.groupByPopover.open}" @click="${this.toggleGroupBy}">
|
|
554
|
+
Group by: <strong>${current}</strong>
|
|
555
|
+
<zn-icon src="chevron-down@lu" size="14"></zn-icon>
|
|
556
|
+
</zn-button>`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private renderGroupByPopover(): TemplateResult | typeof nothing {
|
|
560
|
+
if (!this.groupByPopover.open) return nothing;
|
|
561
|
+
const options = this.activeGroupBys();
|
|
562
|
+
const option = (item: ReportOption) => html`
|
|
563
|
+
<button type="button" class="report__gopt" aria-current="${item.value === this.groupBy ? 'true' : 'false'}"
|
|
564
|
+
@click="${() => this.selectGroupBy(item.value)}">${item.label}</button>`;
|
|
565
|
+
|
|
566
|
+
let body: TemplateResult;
|
|
567
|
+
if (this.hasCategories()) {
|
|
568
|
+
const categories = new Map<string, ReportOption[]>();
|
|
569
|
+
options.forEach(item => {
|
|
570
|
+
const name = item.category ?? '';
|
|
571
|
+
categories.set(name, [...(categories.get(name) ?? []), item]);
|
|
572
|
+
});
|
|
573
|
+
body = html`
|
|
574
|
+
<div class="report__ggrid">
|
|
575
|
+
${[...categories].map(([name, items]) => html`
|
|
576
|
+
<div class="report__gcat" role="group" aria-label="${name}">
|
|
577
|
+
${name ? html`<div class="report__gcat__head">${name}</div>` : nothing}
|
|
578
|
+
${items.map(option)}
|
|
579
|
+
</div>`)}
|
|
580
|
+
</div>`;
|
|
581
|
+
} else {
|
|
582
|
+
body = html`<div class="report__ggrid report__ggrid--flow">${options.map(option)}</div>`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return html`
|
|
586
|
+
<div class="${classMap({report__gpop: true, 'report__gpop--categories': this.hasCategories()})}" role="dialog"
|
|
587
|
+
aria-label="Group by" style="${this.groupByPopover.style}">
|
|
588
|
+
${body}
|
|
589
|
+
</div>`;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private toggleGroupBy = () => {
|
|
593
|
+
if (this.groupByPopover.open) {
|
|
594
|
+
this.groupByPopover.hide();
|
|
595
|
+
} else {
|
|
596
|
+
this.groupByPopover.show();
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
private selectGroupBy(value: string) {
|
|
601
|
+
this.groupByPopover.hide();
|
|
602
|
+
if (value !== this.groupBy) {
|
|
603
|
+
this.change({groupBy: value});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private handleSearch = (e: CustomEvent<{value: string}>) => {
|
|
608
|
+
e.stopPropagation();
|
|
609
|
+
this.search = e.detail.value;
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// Lets the sticky group labels clear the frozen first column, and the loader sit under the header
|
|
613
|
+
private observeDim = (el: Element | undefined) => {
|
|
614
|
+
this.dimObserver?.disconnect();
|
|
615
|
+
this.dimObserver = null;
|
|
616
|
+
if (!el) return;
|
|
617
|
+
const head = el.closest('thead')!;
|
|
618
|
+
this.dimObserver = new ResizeObserver(() => {
|
|
619
|
+
this.style.setProperty('--_dim-width', `${(el as HTMLElement).offsetWidth}px`);
|
|
620
|
+
this.style.setProperty('--_head-height', `${head.offsetHeight}px`);
|
|
621
|
+
});
|
|
622
|
+
this.dimObserver.observe(el);
|
|
623
|
+
this.dimObserver.observe(head);
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
disconnectedCallback() {
|
|
627
|
+
super.disconnectedCallback();
|
|
628
|
+
this.filterEl.removeEventListener('zn-filter-change', this.handleFilter);
|
|
629
|
+
this.dimObserver?.disconnect();
|
|
630
|
+
this.dimObserver = null;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
private handleFilter = (e: Event) => {
|
|
634
|
+
e.stopPropagation();
|
|
635
|
+
this.change({filter: this.filterEl.value});
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
private handleRange = (e: Event) => {
|
|
639
|
+
const range = e.target as ZnDateRange;
|
|
640
|
+
if (range.from === this.from && range.to === this.to) return;
|
|
641
|
+
this.change({from: range.from, to: range.to});
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
private renderActions(): TemplateResult {
|
|
645
|
+
const dimension = this.dimensionLabel();
|
|
646
|
+
const placeholder = this.searchPlaceholder || (dimension ? `Search ${dimension.toLowerCase()}...` : 'Search...');
|
|
647
|
+
|
|
648
|
+
return html`
|
|
649
|
+
<div slot="actions" class="report__actions">
|
|
650
|
+
<zn-data-table-search class="report__search" placeholder="${placeholder}" value="${this.search}"
|
|
651
|
+
@zn-search-change="${this.handleSearch}"></zn-data-table-search>
|
|
652
|
+
${this.activeFilterFields().length > 0 ? this.filterEl : nothing}
|
|
653
|
+
<zn-date-range from="${this.from}" to="${this.to}" max="${this.maxDate || todayIso()}"
|
|
654
|
+
.presets="${this.presets}" note="${this.dateNote}"
|
|
655
|
+
@zn-change="${this.handleRange}"></zn-date-range>
|
|
656
|
+
${this.renderGroupBy()}
|
|
657
|
+
</div>`;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
render() {
|
|
661
|
+
const rows = this.visibleRows();
|
|
662
|
+
const body = this._task.render({
|
|
663
|
+
initial: () => this.renderBody(rows, false),
|
|
664
|
+
pending: () => this.renderBody(rows, true),
|
|
665
|
+
complete: () => this.renderBody(rows, false),
|
|
666
|
+
error: () => html`
|
|
667
|
+
<zn-alert class="report__alert" level="error" size="small">Unable to load the report</zn-alert>`,
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
return html`
|
|
671
|
+
<zn-panel caption="${ifDefined(this.caption || undefined)}" description="${ifDefined(this.renderRowCount(rows))}"
|
|
672
|
+
inline-description flush>
|
|
673
|
+
${this.renderActions()}
|
|
674
|
+
${this.types.length > 1 ? this.tabRow.render(this.types, this.type, false) : nothing}
|
|
675
|
+
${this.activeFilterFields().length > 0 ? html`
|
|
676
|
+
<zn-data-table-filter class="table__chips" .source="${this.filterEl}"></zn-data-table-filter>` : nothing}
|
|
677
|
+
${body}
|
|
678
|
+
</zn-panel>
|
|
679
|
+
${this.renderGroupByPopover()}`;
|
|
680
|
+
}
|
|
681
|
+
}
|