From e68f8746f3828c3b255e66d4fb7ca39a7aa464d4 Mon Sep 17 00:00:00 2001 From: Mihai <103061463+mihai-peteu@users.noreply.github.com> Date: Fri, 15 Dec 2023 08:21:56 -0800 Subject: [PATCH] feat(csv-export-modal): migrate from Konnect [KHCP-8746] (#940) --- .gitignore | 1 + packages/analytics/analytics-chart/README.md | 5 + .../analytics-chart/fixtures/mockData.ts | 568 ++++++++++++++++++ .../analytics/analytics-chart/package.json | 12 +- .../sandbox/pages/BarChartDemo.vue | 1 + .../sandbox/pages/DonutChartDemo.vue | 1 + .../sandbox/pages/TimeSeriesChartDemo.vue | 1 + .../src/components/AnalyticsChart.cy.ts | 542 ++--------------- .../src/components/AnalyticsChart.vue | 57 +- .../src/components/CsvExportModal.cy.ts | 83 +++ .../src/components/CsvExportModal.vue | 253 ++++++++ .../src/components/DownloadCsv.vue | 26 + .../src/components/SimpleChart.cy.ts | 20 +- .../src/components/vue-json-csv/LICENSE | 21 + .../components/vue-json-csv/VueJsonCsv.cy.ts | 71 +++ .../components/vue-json-csv/VueJsonCsv.vue | 175 ++++++ .../analytics-chart/src/composables/index.ts | 2 + .../src/composables/useChartSelectedRange.ts | 19 + .../analytics/analytics-chart/src/index.ts | 3 +- .../analytics-chart/src/locales/en.json | 13 + .../analytics-chart/src/types/chart-export.ts | 23 + .../analytics-chart/src/types/index.ts | 1 + pnpm-lock.yaml | 158 ++++- 23 files changed, 1516 insertions(+), 540 deletions(-) create mode 100644 packages/analytics/analytics-chart/fixtures/mockData.ts create mode 100644 packages/analytics/analytics-chart/src/components/CsvExportModal.cy.ts create mode 100644 packages/analytics/analytics-chart/src/components/CsvExportModal.vue create mode 100644 packages/analytics/analytics-chart/src/components/DownloadCsv.vue create mode 100644 packages/analytics/analytics-chart/src/components/vue-json-csv/LICENSE create mode 100644 packages/analytics/analytics-chart/src/components/vue-json-csv/VueJsonCsv.cy.ts create mode 100644 packages/analytics/analytics-chart/src/components/vue-json-csv/VueJsonCsv.vue create mode 100644 packages/analytics/analytics-chart/src/composables/useChartSelectedRange.ts create mode 100644 packages/analytics/analytics-chart/src/types/chart-export.ts diff --git a/.gitignore b/.gitignore index 11b25b1cfa..4527c33510 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ bundle-analyzer cypress/videos/ cypress/screenshots/ +cypress/downloads/ # Editor directories and files .vscode/* diff --git a/packages/analytics/analytics-chart/README.md b/packages/analytics/analytics-chart/README.md index 7cc5dddb94..9ff69456b4 100644 --- a/packages/analytics/analytics-chart/README.md +++ b/packages/analytics/analytics-chart/README.md @@ -30,6 +30,11 @@ yarn add @kong-ui-public/analytics-chart ### Props - AnalyticsChart +#### `allowCsvExport` + +- type: boolean +- required: `false` + #### `chartData` - type: [AnalyticsExploreResult](https://github.com/Kong/public-ui-components/blob/main/packages/analytics/analytics-utilities/src/types/analytics-data.ts#L77) diff --git a/packages/analytics/analytics-chart/fixtures/mockData.ts b/packages/analytics/analytics-chart/fixtures/mockData.ts new file mode 100644 index 0000000000..eb36de25f4 --- /dev/null +++ b/packages/analytics/analytics-chart/fixtures/mockData.ts @@ -0,0 +1,568 @@ +export const emptyExploreResult = { + records: [], + meta: { + start: 1685559600, + end: 1685599200, + queryId: '12345', + dimensions: { + StatusCode: ['200', '300', '400', '500'], + }, + metricNames: ['TotalRequests'], + metricUnits: { + TotalRequests: 'requests', + }, + granularity: 3600000, + truncated: false, + limit: 10, + }, +} + +export const exploreResult = { + records: [ + { + version: '1.0', + timestamp: '2023-05-30T13:09:00.987Z', + event: { + StatusCode: '200', + TotalRequests: 255.49999999999997, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T13:09:00.987Z', + event: { + StatusCode: '300', + TotalRequests: 182.5, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T13:09:00.987Z', + event: { + StatusCode: '400', + TotalRequests: 182.5, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T13:09:00.987Z', + event: { + StatusCode: '500', + TotalRequests: 73, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T14:09:00.987Z', + event: { + StatusCode: '200', + TotalRequests: 412.8, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T14:09:00.987Z', + event: { + StatusCode: '300', + TotalRequests: 309.59999999999997, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T14:09:00.987Z', + event: { + StatusCode: '400', + TotalRequests: 206.4, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T14:09:00.987Z', + event: { + StatusCode: '500', + TotalRequests: 51.6, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T15:09:00.987Z', + event: { + StatusCode: '200', + TotalRequests: 867.6, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T15:09:00.987Z', + event: { + StatusCode: '300', + TotalRequests: 385.6, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T15:09:00.987Z', + event: { + StatusCode: '400', + TotalRequests: 385.6, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T15:09:00.987Z', + event: { + StatusCode: '500', + TotalRequests: 96.4, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T16:09:00.987Z', + event: { + StatusCode: '200', + TotalRequests: 982.4000000000001, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T16:09:00.987Z', + event: { + StatusCode: '300', + TotalRequests: 614, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T16:09:00.987Z', + event: { + StatusCode: '400', + TotalRequests: 614, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T16:09:00.987Z', + event: { + StatusCode: '500', + TotalRequests: 245.60000000000002, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T17:09:00.987Z', + event: { + StatusCode: '200', + TotalRequests: 1043, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T17:09:00.987Z', + event: { + StatusCode: '300', + TotalRequests: 745, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T17:09:00.987Z', + event: { + StatusCode: '400', + TotalRequests: 596, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T17:09:00.987Z', + event: { + StatusCode: '500', + TotalRequests: 298, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T18:09:00.987Z', + event: { + StatusCode: '200', + TotalRequests: 1260.8000000000002, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T18:09:00.987Z', + event: { + StatusCode: '300', + TotalRequests: 788, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T18:09:00.987Z', + event: { + StatusCode: '400', + TotalRequests: 472.79999999999995, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T18:09:00.987Z', + event: { + StatusCode: '500', + TotalRequests: 157.60000000000002, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T19:09:00.987Z', + event: { + StatusCode: '200', + TotalRequests: 1652, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T19:09:00.987Z', + event: { + StatusCode: '300', + TotalRequests: 1239, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T19:09:00.987Z', + event: { + StatusCode: '400', + TotalRequests: 619.5, + }, + }, + { + version: '1.0', + timestamp: '2023-05-30T19:09:00.987Z', + event: { + StatusCode: '500', + TotalRequests: 413, + }, + }, + ], + meta: { + start: 1685452140.987, + end: 1685473740.987, + queryId: '12345', + dimensions: { + StatusCode: ['200', '300', '400', '500'], + }, + metricNames: ['TotalRequests'], + metricUnits: { + TotalRequests: 'requests', + }, + granularity: 3600000, + truncated: false, + limit: 10, + }, +} + +export const exploreV2Result = { + meta: { + startMs: 1701158400000, + endMs: 1702368000000, + granularity: 604800000, + queryId: '00179418-9e55-4351-ae43-c8da0dcbed02', + metricNames: [ + 'REQUEST_COUNT', + ], + truncated: false, + limit: 50, + metricUnits: { + REQUEST_COUNT: 'count', + }, + dimensions: { + STATUS_CODE_GROUPED: [ + '2XX', + '3XX', + '4XX', + '5XX', + ], + }, + }, + records: [ + { + event: { + STATUS_CODE_GROUPED: '2XX', + REQUEST_COUNT: 22995565, + }, + timestamp: '2023-11-28T08:00:00.000Z', + }, + { + event: { + STATUS_CODE_GROUPED: '3XX', + REQUEST_COUNT: 7307048, + }, + timestamp: '2023-11-28T08:00:00.000Z', + }, + { + event: { + STATUS_CODE_GROUPED: '4XX', + REQUEST_COUNT: 9612003, + }, + timestamp: '2023-11-28T08:00:00.000Z', + }, + { + event: { + STATUS_CODE_GROUPED: '5XX', + REQUEST_COUNT: 6227845, + }, + timestamp: '2023-11-28T08:00:00.000Z', + }, + { + event: { + STATUS_CODE_GROUPED: '2XX', + REQUEST_COUNT: 22148124, + }, + timestamp: '2023-12-05T08:00:00.000Z', + }, + { + event: { + STATUS_CODE_GROUPED: '3XX', + REQUEST_COUNT: 7096101, + }, + timestamp: '2023-12-05T08:00:00.000Z', + }, + { + event: { + STATUS_CODE_GROUPED: '4XX', + REQUEST_COUNT: 9448612, + }, + timestamp: '2023-12-05T08:00:00.000Z', + }, + { + event: { + STATUS_CODE_GROUPED: '5XX', + REQUEST_COUNT: 6016577, + }, + timestamp: '2023-12-05T08:00:00.000Z', + }, + ], +} + +export const dailyExploreResult = { + records: [ + { + version: 'v1', + timestamp: '2023-05-25T06:00:00.000Z', + event: { + TotalRequests: 425722, + }, + }, + { + version: 'v1', + timestamp: '2023-05-26T06:00:00.000Z', + event: { + TotalRequests: 430278, + }, + }, + { + version: 'v1', + timestamp: '2023-05-27T06:00:00.000Z', + event: { + TotalRequests: 429998, + }, + }, + { + version: 'v1', + timestamp: '2023-05-28T06:00:00.000Z', + event: { + TotalRequests: 430544, + }, + }, + { + version: 'v1', + timestamp: '2023-05-29T06:00:00.000Z', + event: { + TotalRequests: 426258, + }, + }, + { + version: 'v1', + timestamp: '2023-05-30T06:00:00.000Z', + event: { + TotalRequests: 430446, + }, + }, + { + version: 'v1', + timestamp: '2023-05-31T06:00:00.000Z', + event: { + TotalRequests: 225018, + }, + }, + ], + meta: { + start: 1684994400, + end: 1685599200, + granularity: 86400000, + queryId: '23976f', + metricNames: ['TotalRequests'], + truncated: true, + limit: 50, + metricUnits: { + TotalRequests: 'count', + }, + dimensions: {}, + }, +} + +export const multiDimensionExploreResult = { + records: [ + { + version: 'v1', + timestamp: '2023-05-31T19:00:00.000Z', + event: { + StatusCode: '200', + Service: 'service A', + TotalRequests: 17945, + }, + }, + { + version: 'v1', + timestamp: '2023-05-31T20:00:00.000Z', + event: { + StatusCode: '200', + Service: 'service A', + TotalRequests: 17940, + }, + }, + { + version: 'v1', + timestamp: '2023-05-31T21:00:00.000Z', + event: { + StatusCode: '300', + Service: 'service A', + TotalRequests: 17955, + }, + }, + { + version: 'v1', + timestamp: '2023-05-31T22:00:00.000Z', + event: { + StatusCode: '300', + Service: 'service A', + TotalRequests: 17944, + }, + }, + { + version: 'v1', + timestamp: '2023-05-31T23:00:00.000Z', + event: { + StatusCode: '500', + Service: 'service A', + TotalRequests: 17940, + }, + }, + { + version: 'v1', + timestamp: '2023-06-01T00:00:00.000Z', + event: { + StatusCode: '500', + Service: 'service A', + TotalRequests: 17925, + }, + }, + { + version: 'v1', + timestamp: '2023-06-01T01:00:00.000Z', + event: { + StatusCode: '200', + Service: 'service A', + TotalRequests: 17930, + }, + }, + { + version: 'v1', + timestamp: '2023-06-01T02:00:00.000Z', + event: { + StatusCode: '300', + Service: 'service B (default)', + TotalRequests: 17945, + }, + }, + { + version: 'v1', + timestamp: '2023-06-01T03:00:00.000Z', + event: { + StatusCode: '400', + Service: 'service B (default)', + TotalRequests: 17940, + }, + }, + { + version: 'v1', + timestamp: '2023-06-01T04:00:00.000Z', + event: { + StatusCode: '400', + Service: 'service B', + TotalRequests: 17939, + }, + }, + { + version: 'v1', + timestamp: '2023-06-01T05:00:00.000Z', + event: { + StatusCode: '400', + Service: 'service B', + TotalRequests: 17930, + }, + }, + { + version: 'v1', + timestamp: '2023-06-01T06:00:00.000Z', + event: { + StatusCode: '500', + Service: 'service B', + TotalRequests: 17950, + }, + }, + { + version: 'v1', + timestamp: '2023-06-01T07:00:00.000Z', + event: { + StatusCode: '200', + Service: 'service B', + TotalRequests: 17935, + }, + }, + { + version: 'v1', + timestamp: '2023-06-01T08:00:00.000Z', + event: { + StatusCode: '300', + Service: 'service B', + TotalRequests: 17930, + }, + }, + { + version: 'v1', + timestamp: '2023-06-01T09:00:00.000Z', + event: { + StatusCode: '400', + Service: 'service B', + TotalRequests: 17935, + }, + }, + ], + meta: { + start: 1685559600, + end: 1685646000, + granularity: 3600000, + queryId: 'ab6ee70c-ca00-47ef-8151-d52ca940837a', + metricNames: ['TotalRequests'], + truncated: false, + limit: 50, + metricUnits: { + TotalRequests: 'count', + }, + dimensions: { + Service: ['service A', 'service B'], + StatusCode: ['200', '300', '400', '500'], + }, + }, +} diff --git a/packages/analytics/analytics-chart/package.json b/packages/analytics/analytics-chart/package.json index 577d8bc8b5..98be33f4bc 100644 --- a/packages/analytics/analytics-chart/package.json +++ b/packages/analytics/analytics-chart/package.json @@ -30,6 +30,11 @@ "@kong/design-tokens": "^1.12.1", "@kong/kongponents": "9.0.0-alpha.73", "@types/uuid": "^9.0.7", + "file-saver": "^2.0.5", + "lodash.mapkeys": "^4.6.0", + "lodash.pick": "^4.4.0", + "lodash.pickby": "^4.6.0", + "papaparse": "^5.4.1", "vue": "^3.3.9" }, "scripts": { @@ -64,6 +69,11 @@ "dependencies": { "@kong-ui-public/analytics-utilities": "workspace:^0.11.0", "@kong/icons": "^1.8.3", + "@types/file-saver": "^2.0.7", + "@types/lodash.mapkeys": "^4.6.9", + "@types/lodash.pick": "^4.4.9", + "@types/lodash.pickby": "^4.6.9", + "@types/papaparse": "^5.3.14", "approximate-number": "^2.1.1", "chart.js": "^4.4.0", "chartjs-adapter-date-fns": "^3.0.0", @@ -76,6 +86,6 @@ }, "distSizeChecker": { "warningLimit": "1.35MB", - "errorLimit": "1.4MB" + "errorLimit": "1.5MB" } } diff --git a/packages/analytics/analytics-chart/sandbox/pages/BarChartDemo.vue b/packages/analytics/analytics-chart/sandbox/pages/BarChartDemo.vue index f89f7040c5..c77697a83a 100644 --- a/packages/analytics/analytics-chart/sandbox/pages/BarChartDemo.vue +++ b/packages/analytics/analytics-chart/sandbox/pages/BarChartDemo.vue @@ -179,6 +179,7 @@
', () => { - it('Renders a line chart for total requests count with satus code dimension', () => { + beforeEach(() => { + cy.viewport(1280, 800) + }) + + it('Renders a line chart for total requests count with status code dimension', () => { cy.mount(AnalyticsChart, { props: { chartData: exploreResult, @@ -723,6 +242,55 @@ describe('', () => { cy.get('[data-testid="no-data-in-report"] .k-empty-state-message').should('contain.text', emptyStateDescription) }) + it('doest not render an "Export button" if the dataset is empty', () => { + cy.mount(AnalyticsChart, { + props: { + allowCsvExport: true, + chartData: emptyExploreResult, + chartOptions: { + type: ChartTypes.TIMESERIES_LINE, + }, + chartTitle: 'Requests', + }, + }) + cy.getTestId('csv-export-button').should('not.exist') + }) + + it('does not render an "Export" button if chart data is present but prop is set to `false`', () => { + cy.mount(AnalyticsChart, { + props: { + allowCsvExport: false, + chartData: exploreResult, + chartOptions: { + type: ChartTypes.TIMESERIES_LINE, + }, + chartTitle: 'Requests', + }, + }) + cy.getTestId('csv-export-button').should('not.exist') + }) + + it('Renders an "Export" button, and tabulated data in the modal preview', () => { + cy.mount(AnalyticsChart, { + props: { + allowCsvExport: true, + chartData: exploreResult, + chartOptions: { + type: ChartTypes.TIMESERIES_LINE, + }, + chartTitle: 'Requests', + }, + }) + + cy.getTestId('csv-export-button').should('exist') + + // eslint-disable-next-line cypress/unsafe-to-chain-command + cy.getTestId('csv-export-button').click().then(() => { + cy.getTestId('csv-export-modal').should('exist') + cy.get('.modal-body .vitals-table').should('exist') + }) + }) + it('multi dimension bar charts have "tooltipContext"', () => { cy.mount(AnalyticsChart, { props: { diff --git a/packages/analytics/analytics-chart/src/components/AnalyticsChart.vue b/packages/analytics/analytics-chart/src/components/AnalyticsChart.vue index fce9127208..b440eec5dd 100644 --- a/packages/analytics/analytics-chart/src/components/AnalyticsChart.vue +++ b/packages/analytics/analytics-chart/src/components/AnalyticsChart.vue @@ -26,6 +26,26 @@
+
+ + {{ i18n.t('csvExport.exportButton') }} + +
+ , required: true, @@ -133,7 +159,13 @@ const props = defineProps({ }, chartTitle: { type: String, - required: true, + required: false, + default: undefined, + }, + filenamePrefix: { + type: String, + required: false, + default: undefined, }, legendPosition: { type: String as PropType<`${ChartLegendPosition}`>, @@ -175,6 +207,7 @@ const props = defineProps({ const { i18n } = composables.useI18n() const heightRef = ref(props.height) +const csvFilename = computed(() => props.filenamePrefix ?? (props.chartTitle || i18n.t('csvExport.defaultFilename'))) const handleHeightUpdate = (height: number) => { heightRef.value = `${height}px` @@ -184,6 +217,8 @@ watch(() => props.height, (newHeight) => { heightRef.value = newHeight }) +const rawChartData = toRef(props, 'chartData') + const computedChartData = computed(() => { return isTimeSeriesChart.value ? composables.useExploreResultToTimeDataset( @@ -307,6 +342,17 @@ const timeSeriesGranularity = computed(() => { return msToGranularity(props.chartData.meta.granularity.duration) || GranularityKeys.HOURLY }) +// CSV Export Modal +const exportModalVisible = ref(false) +const selectedRange = composables.useChartSelectedRange(rawChartData) + +const setModalVisibility = (val: boolean) => { + exportModalVisible.value = val +} +const exportCsv = () => { + setModalVisibility(true) +} + provide('showLegendValues', showLegendValues) provide('legendPosition', toRef(props, 'legendPosition')) @@ -328,14 +374,21 @@ provide('legendPosition', toRef(props, 'legendPosition')) padding: $kui-space-70 $kui-space-0 $kui-space-60 $kui-space-0; } .chart-header { + align-items: center; display: flex; - justify-content: space-between; + justify-content: flex-start; padding-bottom: $kui-space-60; .chart-header-icons-wrapper { display: flex; justify-content: end; } + + .chart-export-button { + display: flex; + margin-left: auto; + margin-right: 0; + } } .chart-title { diff --git a/packages/analytics/analytics-chart/src/components/CsvExportModal.cy.ts b/packages/analytics/analytics-chart/src/components/CsvExportModal.cy.ts new file mode 100644 index 0000000000..90bb088318 --- /dev/null +++ b/packages/analytics/analytics-chart/src/components/CsvExportModal.cy.ts @@ -0,0 +1,83 @@ +// Cypress component test spec file +import CsvExportModal from './CsvExportModal.vue' +import composables from '../composables' +import { computed } from 'vue' +import { exploreResult, exploreV2Result, emptyExploreResult } from '../../fixtures/mockData' + +const DOWNLOADS_FOLDER = Cypress.config('downloadsFolder') +const MAX_ROWS = 3 + +describe('', () => { + beforeEach(() => { + cy.viewport(800, 800) + }) + + it('Export Modal with empty dataset', () => { + cy.mount(CsvExportModal, { + props: { + chartData: emptyExploreResult, + filename: 'Total requests', + selectedRange: composables.useChartSelectedRange(computed(() => emptyExploreResult)), + }, + }) + + cy.getTestId('csv-export-modal').should('exist') + cy.get('.k-table-empty-state').should('be.visible') + cy.getTestId('csv-download-button').should('be.disabled') + }) + + it('Export Modal with v1 explore data', () => { + cy.mount(CsvExportModal, { + props: { + chartData: exploreResult, + filename: 'Total requests', + selectedRange: composables.useChartSelectedRange(computed(() => exploreResult)), + }, + }) + + cy.getTestId('csv-export-modal').should('exist') + cy.get('.k-table-empty-state').should('not.exist') + cy.get('.modal-body .vitals-table').should('exist') + cy.get('.modal-body .vitals-table').should('exist') + cy.getTestId('csv-download-button').should('not.be.disabled') + + // Timestamp should be naive localtime + cy.getTestId('csv-export-modal').find('.k-table tbody td').eq(0).invoke('text').should('match', /\d{4}-\d\d-\d\d \d\d:\d\d:\d\d/) + + // Table should contain the max number of rows allowed + 1 Header row + const numTableRows = MAX_ROWS + 1 + + cy.getTestId('csv-export-modal').find('.k-table tr').should('have.length', numTableRows) + + // Column headers should be as expected. + cy.getTestId('csv-export-modal').find('.k-table thead th').should(th => { + const elements = Array.from(th, e => e.innerText) + + expect(elements).eql(['Timestamp', 'UtcOffset', 'StatusCode', 'TotalRequests']) + }) + + // Save to CSV and check actual contents + cy.getTestId('csv-download-button').click() + + cy.readFile(`${DOWNLOADS_FOLDER}/total-requests-${new Date().toISOString().slice(0, 10)}.csv`).then(contents => { + expect(contents).contain('Timestamp,UtcOffset,StatusCode,TotalRequests') + expect(contents).contain(',300,1239') + }) + }) + + it('Export Modal with v2 explore data', () => { + cy.mount(CsvExportModal, { + props: { + filename: 'Total requests', + chartData: exploreV2Result, + selectedRange: composables.useChartSelectedRange(computed(() => exploreV2Result)), + }, + }) + + cy.getTestId('csv-export-modal').should('exist') + cy.get('.k-table-empty-state').should('not.exist') + cy.get('.modal-body .vitals-table').should('exist') + cy.get('.modal-body .vitals-table').should('exist') + cy.getTestId('csv-download-button').should('not.be.disabled') + }) +}) diff --git a/packages/analytics/analytics-chart/src/components/CsvExportModal.vue b/packages/analytics/analytics-chart/src/components/CsvExportModal.vue new file mode 100644 index 0000000000..ed9c0fdb65 --- /dev/null +++ b/packages/analytics/analytics-chart/src/components/CsvExportModal.vue @@ -0,0 +1,253 @@ + + + + + diff --git a/packages/analytics/analytics-chart/src/components/DownloadCsv.vue b/packages/analytics/analytics-chart/src/components/DownloadCsv.vue new file mode 100644 index 0000000000..e8baf8ea25 --- /dev/null +++ b/packages/analytics/analytics-chart/src/components/DownloadCsv.vue @@ -0,0 +1,26 @@ + + + diff --git a/packages/analytics/analytics-chart/src/components/SimpleChart.cy.ts b/packages/analytics/analytics-chart/src/components/SimpleChart.cy.ts index 19c6d3e2ec..cd07fdfd5d 100644 --- a/packages/analytics/analytics-chart/src/components/SimpleChart.cy.ts +++ b/packages/analytics/analytics-chart/src/components/SimpleChart.cy.ts @@ -1,25 +1,7 @@ // Cypress component test spec file import { ChartMetricDisplay, ChartLegendPosition, ChartTypesSimple } from '../enums/' import SimpleChart from './SimpleChart.vue' - -const emptyExploreResult = { - records: [], - meta: { - start: 1685452140.987, - end: 1685473740.987, - queryId: '12345', - dimensions: { - StatusCode: ['200', '300', '400', '500'], - }, - metricNames: ['TotalRequests'], - metricUnits: { - TotalRequests: 'requests', - }, - granularity: 3600000, - truncated: false, - limit: 10, - }, -} +import { emptyExploreResult } from '../../fixtures/mockData' const exploreResultTruncated = { records: [ diff --git a/packages/analytics/analytics-chart/src/components/vue-json-csv/LICENSE b/packages/analytics/analytics-chart/src/components/vue-json-csv/LICENSE new file mode 100644 index 0000000000..8a375d05fd --- /dev/null +++ b/packages/analytics/analytics-chart/src/components/vue-json-csv/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Antoine Aflalo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/analytics/analytics-chart/src/components/vue-json-csv/VueJsonCsv.cy.ts b/packages/analytics/analytics-chart/src/components/vue-json-csv/VueJsonCsv.cy.ts new file mode 100644 index 0000000000..2369389443 --- /dev/null +++ b/packages/analytics/analytics-chart/src/components/vue-json-csv/VueJsonCsv.cy.ts @@ -0,0 +1,71 @@ +import VueJsonCsv from './VueJsonCsv.vue' + +const CSV_DATA = [ + { requests: 206, tzOffset: '-08:00', timestamp: '2022-10-15 13:43:27' }, + { requests: 381, tzOffset: '-08:00', timestamp: '2022-12-15 06:00:53' }, + { requests: 648, tzOffset: '-08:00', timestamp: '2022-04-26 06:26:28' }, + { requests: 925, tzOffset: '-08:00', timestamp: '2022-04-10 10:28:46' }, + { requests: 134, tzOffset: '-08:00', timestamp: '2022-12-06 14:38:38' }, +] + +const LABELS = { timestamp: 'Timestamp', tzOffset: 'UtcOffset', requests: 'TotalRequests' } +const FIELDS = ['timestamp', 'tzOffset', 'requests'] + +describe('', () => { + it('loads the table data and emits export events', () => { + cy.mount(VueJsonCsv, { + props: { + data: CSV_DATA, + labels: LABELS, + fields: FIELDS, + testing: true, + onUpdate: cy.spy().as('onUpdateExport'), + }, + }) + + // eslint-disable-next-line cypress/unsafe-to-chain-command + cy.getTestId('export-csv').click().then(() => { + // eslint-disable-next-line cypress/unsafe-to-chain-command + cy.wrap(Cypress.vueWrapper.emitted()).should('have.property', 'export-started').then((evt) => { + const incomingData = evt[0][0] + const firstRow = incomingData[0] + + cy.wrap(firstRow).should('have.property', LABELS.requests) + cy.wrap(firstRow).should('have.property', LABELS.timestamp) + cy.wrap(firstRow).should('not.have.property', 'id') + cy.wrap(incomingData).should('have.length', CSV_DATA.length) + }) + + // eslint-disable-next-line cypress/unsafe-to-chain-command + cy.wrap(Cypress.vueWrapper.emitted()).should('have.property', 'export-finished').then((evt) => { + const exportedFilename = evt[0][0] + + // No filename was provided, shoudl have defaulted to 'report-data.csv' + cy.wrap(exportedFilename).should('be.equal', 'report-data.csv') + }) + }) + }) + + it('emits event on export', () => { + cy.mount(VueJsonCsv, { + props: { + data: CSV_DATA, + labels: LABELS, + fields: FIELDS, + testing: true, + filename: 'custom-export.csv', + }, + }) + + // eslint-disable-next-line cypress/unsafe-to-chain-command + cy.getTestId('export-csv').click().then(() => { + // eslint-disable-next-line cypress/unsafe-to-chain-command + cy.wrap(Cypress.vueWrapper.emitted()).should('have.property', 'export-finished').then((evt) => { + const exportedFilename = evt[0][0] + + // No filename was provided, shoudl have defaulted to 'report-data.csv' + cy.wrap(exportedFilename).should('be.equal', 'custom-export.csv') + }) + }) + }) +}) diff --git a/packages/analytics/analytics-chart/src/components/vue-json-csv/VueJsonCsv.vue b/packages/analytics/analytics-chart/src/components/vue-json-csv/VueJsonCsv.vue new file mode 100644 index 0000000000..ec9e4a5f03 --- /dev/null +++ b/packages/analytics/analytics-chart/src/components/vue-json-csv/VueJsonCsv.vue @@ -0,0 +1,175 @@ + + + diff --git a/packages/analytics/analytics-chart/src/composables/index.ts b/packages/analytics/analytics-chart/src/composables/index.ts index cc60ef7145..a5e0902ff8 100644 --- a/packages/analytics/analytics-chart/src/composables/index.ts +++ b/packages/analytics/analytics-chart/src/composables/index.ts @@ -2,6 +2,7 @@ import useI18n from './useI18n' import useBarChartOptions from './useBarChartOptions' import useChartJSCommon from './useChartJSCommon' import useChartLegendValues from './useChartLegendValues' +import useChartSelectedRange from './useChartSelectedRange' import useDoughnutChartOptions from './useDoughnutChartOptions' import useLinechartOptions from './useLineChartOptions' import useExploreResultToDatasets from './useExploreResultToDatasets' @@ -13,6 +14,7 @@ export default { useBarChartOptions, useChartJSCommon, useChartLegendValues, + useChartSelectedRange, useExploreResultToDatasets, useExploreResultToTimeDataset, useDoughnutChartOptions, diff --git a/packages/analytics/analytics-chart/src/composables/useChartSelectedRange.ts b/packages/analytics/analytics-chart/src/composables/useChartSelectedRange.ts new file mode 100644 index 0000000000..bef510b190 --- /dev/null +++ b/packages/analytics/analytics-chart/src/composables/useChartSelectedRange.ts @@ -0,0 +1,19 @@ +import type { Ref } from 'vue' +import { computed } from 'vue' +import type { AnalyticsExploreResult, AnalyticsExploreV2Result } from '@kong-ui-public/analytics-utilities' +import { formatTime } from '../utils' + +export default function useChartSelectedRange(chartData: Ref): Ref { + const formattedTimeRange = computed(() => { + if (!chartData.value?.meta) { return '' } + + const start = 'startMs' in chartData.value.meta ? chartData.value.meta.startMs : chartData.value.meta?.start * 1000 || '' + const end = 'endMs' in chartData.value.meta ? chartData.value.meta.endMs : chartData.value.meta?.end * 1000 || '' + + return start && end + ? `${formatTime(start)} - ${formatTime(end, { includeTZ: true })}` + : '' + }) + + return formattedTimeRange +} diff --git a/packages/analytics/analytics-chart/src/index.ts b/packages/analytics/analytics-chart/src/index.ts index cb56166f9f..27517783a5 100644 --- a/packages/analytics/analytics-chart/src/index.ts +++ b/packages/analytics/analytics-chart/src/index.ts @@ -1,8 +1,9 @@ import AnalyticsChart from './components/AnalyticsChart.vue' import SimpleChart from './components/SimpleChart.vue' import TopNTable from './components/TopNTable.vue' +import CsvExportModal from './components/CsvExportModal.vue' -export { AnalyticsChart, SimpleChart, TopNTable } +export { AnalyticsChart, SimpleChart, TopNTable, CsvExportModal } export * from './types' export * from './enums' diff --git a/packages/analytics/analytics-chart/src/locales/en.json b/packages/analytics/analytics-chart/src/locales/en.json index 69aa03b5da..b9fc6cc9e8 100644 --- a/packages/analytics/analytics-chart/src/locales/en.json +++ b/packages/analytics/analytics-chart/src/locales/en.json @@ -12,6 +12,19 @@ "count/minute": "rpm", "requests": "requests" }, + "csvExport" :{ + "noDataRange": "No data in selected range", + "noDataRetry": "Please adjust the time period and try again", + "timeRangeSelected": "selected", + "defaultFilename": "Chart Export", + "exportPreview": "Export Preview", + "exportButton": "Export", + "exportTimeRange": "Time range", + "exportDescription": "Exports a CSV of the data represented in the chart.", + "previewRows": "Previewing {rowsMax} of {rowsTotal} row{plural}", + "downloadButton": "Export", + "cancelButton": "Cancel" + }, "legend": { "datapointValueDisplay": "{value} {unit}" }, diff --git a/packages/analytics/analytics-chart/src/types/chart-export.ts b/packages/analytics/analytics-chart/src/types/chart-export.ts new file mode 100644 index 0000000000..220ff3031c --- /dev/null +++ b/packages/analytics/analytics-chart/src/types/chart-export.ts @@ -0,0 +1,23 @@ +export interface CsvKeyValuePair { + [key: string]: string +} + +export enum ValidType { + String = 'string', + Number = 'number', + Boolean = 'boolean', + Object = 'object', + Undefined = 'undefined', +} + +export interface Header { + label: string + key: string +} + +export interface TimeseriesColumn { + label: string; + key: string; +} + +export type CsvData = CsvKeyValuePair[] diff --git a/packages/analytics/analytics-chart/src/types/index.ts b/packages/analytics/analytics-chart/src/types/index.ts index 5fc231796f..9e494dbea5 100644 --- a/packages/analytics/analytics-chart/src/types/index.ts +++ b/packages/analytics/analytics-chart/src/types/index.ts @@ -1,4 +1,5 @@ export * from './chart-data' +export * from './chart-export' export * from './chartjs-options' export * from './explore-to-dataset-deps' export * from './dataset-generation-types' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d19d543aa8..1cd2edb81b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,6 +200,21 @@ importers: '@kong/icons': specifier: ^1.8.3 version: 1.8.3(vue@3.3.11) + '@types/file-saver': + specifier: ^2.0.7 + version: 2.0.7 + '@types/lodash.mapkeys': + specifier: ^4.6.9 + version: 4.6.9 + '@types/lodash.pick': + specifier: ^4.4.9 + version: 4.4.9 + '@types/lodash.pickby': + specifier: ^4.6.9 + version: 4.6.9 + '@types/papaparse': + specifier: ^5.3.14 + version: 5.3.14 approximate-number: specifier: ^2.1.1 version: 2.1.1 @@ -243,6 +258,21 @@ importers: '@types/uuid': specifier: ^9.0.7 version: 9.0.7 + file-saver: + specifier: ^2.0.5 + version: 2.0.5 + lodash.mapkeys: + specifier: ^4.6.0 + version: 4.6.0 + lodash.pick: + specifier: ^4.4.0 + version: 4.4.0 + lodash.pickby: + specifier: ^4.6.0 + version: 4.6.0 + papaparse: + specifier: ^5.4.1 + version: 5.4.1 vue: specifier: ^3.3.9 version: 3.3.11(typescript@5.2.2) @@ -1287,6 +1317,14 @@ packages: js-tokens: 4.0.0 dev: true + /@babel/parser@7.22.11: + resolution: {integrity: sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.6 + dev: true + /@babel/parser@7.23.4: resolution: {integrity: sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==} engines: {node: '>=6.0.0'} @@ -1371,7 +1409,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.22.10 - '@babel/parser': 7.23.4 + '@babel/parser': 7.22.11 '@babel/types': 7.23.6 dev: true @@ -1385,7 +1423,7 @@ packages: '@babel/helper-function-name': 7.22.5 '@babel/helper-hoist-variables': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.23.4 + '@babel/parser': 7.22.11 '@babel/types': 7.23.6 debug: 4.3.4(supports-color@8.1.1) globals: 11.12.0 @@ -3601,6 +3639,10 @@ packages: '@types/serve-static': 1.15.1 dev: true + /@types/file-saver@2.0.7: + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + dev: false + /@types/flat@5.0.5: resolution: {integrity: sha512-nPLljZQKSnac53KDUDzuzdRfGI0TDb5qPrb+SrQyN3MtdQrOnGsKniHN1iYZsJEBIVQve94Y6gNz22sgISZq+Q==} dev: true @@ -3661,6 +3703,24 @@ packages: '@types/lodash': 4.14.202 dev: true + /@types/lodash.mapkeys@4.6.9: + resolution: {integrity: sha512-6/ERBCabeDI656LsV+oopLjdnJ/x1PCAE6kkkssH8e4i0K7Pw307noxHCbUc6cAVfTo9vx0Z+k3QZwy1IrUZcA==} + dependencies: + '@types/lodash': 4.14.202 + dev: false + + /@types/lodash.pick@4.4.9: + resolution: {integrity: sha512-hDpr96x9xHClwy1KX4/RXRejqjDFTEGbEMT3t6wYSYeFDzxmMnSKB/xHIbktRlPj8Nii2g8L5dtFDRaNFBEzUQ==} + dependencies: + '@types/lodash': 4.14.202 + dev: false + + /@types/lodash.pickby@4.6.9: + resolution: {integrity: sha512-SPI248FYnyd3jOxDeJq2vX2UKQnDzqacuqdeOVqwE1MPSk8gN8TA3FcHSMQWLlpBnuHgXvgKInvywbOFbidpJA==} + dependencies: + '@types/lodash': 4.14.202 + dev: false + /@types/lodash@4.14.202: resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} @@ -3684,12 +3744,17 @@ packages: resolution: {integrity: sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==} dependencies: undici-types: 5.26.5 - dev: true /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: true + /@types/papaparse@5.3.14: + resolution: {integrity: sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==} + dependencies: + '@types/node': 18.19.3 + dev: false + /@types/parse-json@4.0.0: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} dev: true @@ -5738,7 +5803,7 @@ packages: /constantinople@4.0.1: resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} dependencies: - '@babel/parser': 7.23.4 + '@babel/parser': 7.22.11 '@babel/types': 7.23.6 dev: true @@ -5912,7 +5977,6 @@ packages: /cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.3)(cosmiconfig@8.3.6)(typescript@5.2.2): resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} engines: {node: '>=v16'} - requiresBuild: true peerDependencies: '@types/node': '*' cosmiconfig: '>=8.2' @@ -6009,12 +6073,12 @@ packages: peerDependencies: webpack: ^5.0.0 dependencies: - icss-utils: 5.1.0(postcss@8.4.32) - postcss: 8.4.32 - postcss-modules-extract-imports: 3.0.0(postcss@8.4.32) - postcss-modules-local-by-default: 4.0.3(postcss@8.4.32) - postcss-modules-scope: 3.0.0(postcss@8.4.32) - postcss-modules-values: 4.0.0(postcss@8.4.32) + icss-utils: 5.1.0(postcss@8.4.31) + postcss: 8.4.31 + postcss-modules-extract-imports: 3.0.0(postcss@8.4.31) + postcss-modules-local-by-default: 4.0.3(postcss@8.4.31) + postcss-modules-scope: 3.0.0(postcss@8.4.31) + postcss-modules-values: 4.0.0(postcss@8.4.31) postcss-value-parser: 4.2.0 semver: 7.5.1 webpack: 5.89.0(webpack-cli@5.1.4) @@ -7450,6 +7514,10 @@ packages: flat-cache: 3.1.1 dev: true + /file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + dev: true + /file-url@3.0.0: resolution: {integrity: sha512-g872QGsHexznxkIAdK8UiZRe7SkE6kvylShU4Nsj8NvfvZag7S0QuQ4IgvPDkk75HxgjIVDwycFTDAgIiO4nDA==} engines: {node: '>=8'} @@ -8451,13 +8519,13 @@ packages: safer-buffer: 2.1.2 dev: true - /icss-utils@5.1.0(postcss@8.4.32): + /icss-utils@5.1.0(postcss@8.4.31): resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.32 + postcss: 8.4.31 dev: true /ieee754@1.2.1: @@ -9549,6 +9617,10 @@ packages: resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==} dev: true + /lodash.mapkeys@4.6.0: + resolution: {integrity: sha512-0Al+hxpYvONWtg+ZqHpa/GaVzxuN3V7Xeo2p+bY06EaK/n+Y9R7nBePPN2o1LxmL0TWQSwP8LYZ008/hc9JzhA==} + dev: true + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true @@ -9562,6 +9634,14 @@ packages: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} dev: true + /lodash.pick@4.4.0: + resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} + dev: true + + /lodash.pickby@4.6.0: + resolution: {integrity: sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==} + dev: true + /lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} dev: true @@ -10190,6 +10270,12 @@ packages: dev: false optional: true + /nanoid@3.3.6: + resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + /nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -10355,7 +10441,7 @@ packages: engines: {node: '>=10'} dependencies: hosted-git-info: 4.1.0 - is-core-module: 2.13.1 + is-core-module: 2.13.0 semver: 7.5.4 validate-npm-package-license: 3.0.4 dev: true @@ -10365,7 +10451,7 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} dependencies: hosted-git-info: 5.2.1 - is-core-module: 2.13.1 + is-core-module: 2.13.0 semver: 7.5.4 validate-npm-package-license: 3.0.4 dev: true @@ -10375,7 +10461,7 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dependencies: hosted-git-info: 6.1.1 - is-core-module: 2.13.1 + is-core-module: 2.13.0 semver: 7.5.4 validate-npm-package-license: 3.0.4 dev: true @@ -10914,6 +11000,10 @@ packages: - supports-color dev: true + /papaparse@5.4.1: + resolution: {integrity: sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==} + dev: true + /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -10954,7 +11044,7 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.23.4 + '@babel/code-frame': 7.22.10 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -11183,45 +11273,45 @@ packages: resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} dev: true - /postcss-modules-extract-imports@3.0.0(postcss@8.4.32): + /postcss-modules-extract-imports@3.0.0(postcss@8.4.31): resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.32 + postcss: 8.4.31 dev: true - /postcss-modules-local-by-default@4.0.3(postcss@8.4.32): + /postcss-modules-local-by-default@4.0.3(postcss@8.4.31): resolution: {integrity: sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - icss-utils: 5.1.0(postcss@8.4.32) - postcss: 8.4.32 + icss-utils: 5.1.0(postcss@8.4.31) + postcss: 8.4.31 postcss-selector-parser: 6.0.13 postcss-value-parser: 4.2.0 dev: true - /postcss-modules-scope@3.0.0(postcss@8.4.32): + /postcss-modules-scope@3.0.0(postcss@8.4.31): resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.32 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 dev: true - /postcss-modules-values@4.0.0(postcss@8.4.32): + /postcss-modules-values@4.0.0(postcss@8.4.31): resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - icss-utils: 5.1.0(postcss@8.4.32) - postcss: 8.4.32 + icss-utils: 5.1.0(postcss@8.4.31) + postcss: 8.4.31 dev: true /postcss-resolve-nested-selector@0.1.1: @@ -11266,6 +11356,15 @@ packages: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} dev: true + /postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + /postcss@8.4.32: resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==} engines: {node: ^10 || ^12 || >=14} @@ -12118,7 +12217,7 @@ packages: resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} hasBin: true dependencies: - is-core-module: 2.13.1 + is-core-module: 2.13.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 dev: true @@ -13920,7 +14019,6 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true /undici@5.25.4: resolution: {integrity: sha512-450yJxT29qKMf3aoudzFpIciqpx6Pji3hEWaXqXmanbXF58LTAGCKxcJjxMXWu3iG+Mudgo3ZUfDB6YDFd/dAw==} @@ -14737,7 +14835,7 @@ packages: resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} engines: {node: '>= 10.0.0'} dependencies: - '@babel/parser': 7.23.4 + '@babel/parser': 7.22.11 '@babel/types': 7.23.6 assert-never: 1.2.1 babel-walk: 3.0.0-canary-5