diff --git a/config.template.js b/config.template.js index 2d4849254..8773d0496 100644 --- a/config.template.js +++ b/config.template.js @@ -23,6 +23,7 @@ window.env = { REPORT_API_HOST: '$REPORT_API_HOST', RESOURCE_API_HOST: '$RESOURCE_API_HOST', SEARCH_SERVICE_HOST: '$SEARCH_SERVICE_HOST', + STATISTICS_SERVICE_HOST: '$STATISTICS_SERVICE_HOST', USE_DEMO_LOGO: '$USE_DEMO_LOGO', CATALOG_PORTAL_BASE_URI: '$CATALOG_PORTAL_BASE_URI', ACCESS_REQUEST_API_HOST: '$ACCESS_REQUEST_API_HOST' diff --git a/deploy/demo/env.yaml b/deploy/demo/env.yaml index d67282666..582af9bb1 100644 --- a/deploy/demo/env.yaml +++ b/deploy/demo/env.yaml @@ -39,6 +39,11 @@ spec: secretKeyRef: name: commonurl-demo key: FDK_SEARCH_SERVICE_BASE_URI + - name: STATISTICS_SERVICE_HOST + valueFrom: + secretKeyRef: + name: commonurl-demo + key: FDK_STATISTICS_SERVICE_BASE_URI - name: CMS_API_HOST valueFrom: secretKeyRef: diff --git a/deploy/prod/env.yaml b/deploy/prod/env.yaml index a72007af3..0bf14d479 100644 --- a/deploy/prod/env.yaml +++ b/deploy/prod/env.yaml @@ -39,6 +39,11 @@ spec: secretKeyRef: name: commonurl-prod key: FDK_SEARCH_SERVICE_BASE_URI + - name: STATISTICS_SERVICE_HOST + valueFrom: + secretKeyRef: + name: commonurl-prod + key: FDK_STATISTICS_SERVICE_BASE_URI - name: CMS_API_HOST valueFrom: secretKeyRef: diff --git a/deploy/staging/env.yaml b/deploy/staging/env.yaml index 1258e65ca..881343bcb 100644 --- a/deploy/staging/env.yaml +++ b/deploy/staging/env.yaml @@ -39,6 +39,11 @@ spec: secretKeyRef: name: commonurl-staging key: FDK_SEARCH_SERVICE_BASE_URI + - name: STATISTICS_SERVICE_HOST + valueFrom: + secretKeyRef: + name: commonurl-staging + key: FDK_STATISTICS_SERVICE_BASE_URI - name: CMS_API_HOST valueFrom: secretKeyRef: diff --git a/package-lock.json b/package-lock.json index e713761cd..62b58aadd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@redux-devtools/extension": "^3.3.0", "@reduxjs/toolkit": "^1.9.7", "@types/react-document-meta": "^3.0.5", - "apexcharts": "^3.49.1", + "apexcharts": "^3.54.0", "axios": "^1.7.3", "bootstrap": "^5.3.3", "buffer": "^6.0.3", @@ -8306,9 +8306,9 @@ } }, "node_modules/apexcharts": { - "version": "3.49.1", - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.49.1.tgz", - "integrity": "sha512-MqGtlq/KQuO8j0BBsUJYlRG8VBctKwYdwuBtajHgHTmSgUU3Oai+8oYN/rKCXwXzrUlYA+GiMgotAIbXY2BCGw==", + "version": "3.54.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.54.0.tgz", + "integrity": "sha512-ZgI/seScffjLpwNRX/gAhIkAhpCNWiTNsdICv7qxnF0xisI23XSsaENUKIcMlyP1rbe8ECgvybDnp7plZld89A==", "dependencies": { "@yr/monotone-cubic-spline": "^1.0.3", "svg.draggable.js": "^2.2.2", diff --git a/package.json b/package.json index 4ad0ec934..3460ec337 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@redux-devtools/extension": "^3.3.0", "@reduxjs/toolkit": "^1.9.7", "@types/react-document-meta": "^3.0.5", - "apexcharts": "^3.49.1", + "apexcharts": "^3.54.0", "axios": "^1.7.3", "bootstrap": "^5.3.3", "buffer": "^6.0.3", diff --git a/src/api/statistics-api/host.ts b/src/api/statistics-api/host.ts new file mode 100644 index 000000000..aa8660a97 --- /dev/null +++ b/src/api/statistics-api/host.ts @@ -0,0 +1,24 @@ +import axios from 'axios'; +import cleanDeep from 'clean-deep'; +import { getConfig } from '../../config'; +import type { TimeSeriesRequest } from '../../types'; + +interface Props { + path: string; + method: any; + data?: any; + params?: URLSearchParams; +} + +export const statisticsApi = ({ path, method, data, params }: Props) => + axios({ + url: `${getConfig().statisticsApi.host}/${path}`, + method, + data, + params + }) + .then(response => cleanDeep(response.data)) + .catch(() => null); + +export const statisticsApiPost = (path: string, body: TimeSeriesRequest) => + statisticsApi({ path, method: 'POST', data: body }); diff --git a/src/api/statistics-api/time-series.ts b/src/api/statistics-api/time-series.ts new file mode 100644 index 000000000..aa1ca622f --- /dev/null +++ b/src/api/statistics-api/time-series.ts @@ -0,0 +1,76 @@ +import { statisticsApiPost } from './host'; +import type { TimeSeriesPoint, TimeSeriesRequest } from '../../types'; +import { getConfig } from '../../config'; + +const timeSeriesBody = ( + resourceType: string, + orgPath: string | undefined +): TimeSeriesRequest => { + let start = '2023-02-01'; + if (resourceType === 'DATASET') { + start = '2022-11-01'; + } + if (resourceType === 'INFORMATION_MODEL') { + start = '2024-01-01'; + } + const now = new Date(); + const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1, 12) + .toISOString() + .split('T')[0]; + const body: TimeSeriesRequest = { + start, + end: firstDayOfMonth, + interval: 'MONTH', + filters: { + resourceType: { value: resourceType }, + orgPath: null, + transport: null + } + }; + + if (orgPath !== undefined) { + body.filters.orgPath = { value: orgPath }; + } + + if (getConfig().isNapProfile) { + body.filters.transport = { value: true }; + } + + return body; +}; + +const extractLabelsAndData = ( + timeSeries: Partial | null +): number[][] => { + let timeSeriesData: number[][] = []; + if (Array.isArray(timeSeries)) { + timeSeriesData = timeSeries.map(({ date, count }) => [ + Date.parse(date), + count + ]); + } + return timeSeriesData; +}; + +const timeSeriesRequest = (body: TimeSeriesRequest) => + statisticsApiPost('time-series', body); + +export const conceptTimeSeriesRequest = (orgPath: string | undefined) => + timeSeriesRequest(timeSeriesBody('CONCEPT', orgPath)).then( + extractLabelsAndData + ); + +export const dataServiceTimeSeriesRequest = (orgPath: string | undefined) => + timeSeriesRequest(timeSeriesBody('DATA_SERVICE', orgPath)).then( + extractLabelsAndData + ); + +export const datasetTimeSeriesRequest = (orgPath: string | undefined) => + timeSeriesRequest(timeSeriesBody('DATASET', orgPath)).then( + extractLabelsAndData + ); + +export const infoModelTimeSeriesRequest = (orgPath: string | undefined) => + timeSeriesRequest(timeSeriesBody('INFORMATION_MODEL', orgPath)).then( + extractLabelsAndData + ); diff --git a/src/config.js b/src/config.js index a52190c04..627025868 100644 --- a/src/config.js +++ b/src/config.js @@ -10,6 +10,8 @@ const env = window.env || { // env.FDK_PORTAL_BASE_URI = 'https://staging.fellesdatakatalog.digdir.no'; // env.SEARCH_SERVICE_HOST = // 'https://search.api.staging.fellesdatakatalog.digdir.no'; +// env.STATISTICS_SERVICE_HOST = +// 'https://statistics.api.staging.fellesdatakatalog.digdir.no'; // env.CMS_API_HOST = 'https://cms-fellesdatakatalog.digdir.no'; // env.FDK_CMS_BASE_URI = 'https://cms.staging.fellesdatakatalog.digdir.no'; // env.ORGANIZATION_HOST = @@ -50,6 +52,7 @@ const config = { reportApi: { host: env.REPORT_API_HOST }, resourceApi: { host: env.RESOURCE_API_HOST }, searchApi: { host: env.SEARCH_SERVICE_HOST }, + statisticsApi: { host: env.STATISTICS_SERVICE_HOST }, store: { useLogger: env.REDUX_LOG === 'true' }, isNapProfile: isNapProfile(env.NAP_HOST), useDemoLogo: env.USE_DEMO_LOGO, diff --git a/src/pages/report-page/components/conceptReport/conceptReport.component.tsx b/src/pages/report-page/components/conceptReport/conceptReport.component.tsx index 3875ca2e0..009fe9235 100644 --- a/src/pages/report-page/components/conceptReport/conceptReport.component.tsx +++ b/src/pages/report-page/components/conceptReport/conceptReport.component.tsx @@ -24,6 +24,7 @@ import NewIcon from '../../../../images/icon-new-md.svg'; import { PATHNAME_CONCEPTS } from '../../../../constants/constants'; import { patchSearchQuery } from '../../../../lib/addOrReplaceUrlParam'; import localization from '../../../../lib/localization'; +import { Line } from '../../../../components/charts'; import { getTranslateText } from '../../../../lib/translateText'; import { ContainerBoxRegular, ContainerPaneContent } from '../../styled'; @@ -32,6 +33,7 @@ interface AllReferencedConceptIdentifiers { } interface Props extends RouteComponentProps { conceptsReport?: Partial & AllReferencedConceptIdentifiers; + conceptsTimeSeries: any; } const ConceptReport: FC = ({ @@ -41,74 +43,98 @@ const ConceptReport: FC = ({ newLastWeek = 0, organizationCount = 0, allReferencedConcepts = [] - } = {} -}) => ( - -
- - - - - } - count={totalObjects} - /> - - {localization.report.conceptsDescription} - - - - - - - - } count={newLastWeek} /> - - {localization.report.newPastWeek} - - - - - - -
-
- - - } - count={organizationCount} - /> - - {localization.report.organizationsConcept} - - - -
-
+ } = {}, + conceptsTimeSeries = [] +}) => { + conceptsTimeSeries.push([Date.now(), totalObjects]); + return ( + +
+ + + + + } + count={totalObjects} + /> + + {localization.report.conceptsDescription} + + + + + + + + } count={newLastWeek} /> + + {localization.report.newPastWeek} + + + + + - {allReferencedConcepts?.length > 0 && (
- - {allReferencedConcepts.map( - ({ id, prefLabel }: Partial) => ( - - {getTranslateText(prefLabel)} - - ) - )} + + + } + count={organizationCount} + /> + + {localization.report.organizationsConcept} + +
- )} -
-
-); + + {conceptsTimeSeries?.length > 0 && conceptsTimeSeries?.length > 0 && ( +
+
+ + + +
+
+ )} + + {allReferencedConcepts?.length > 0 && ( +
+
+ + {allReferencedConcepts.map( + ({ id, prefLabel }: Partial) => ( + + {getTranslateText(prefLabel)} + + ) + )} + +
+
+ )} +
+
+ ); +}; export default withRouter(memo(ConceptReport)); diff --git a/src/pages/report-page/components/dataserviceReport/dataserviceReport.component.tsx b/src/pages/report-page/components/dataserviceReport/dataserviceReport.component.tsx index 95b4771ee..34db961b7 100644 --- a/src/pages/report-page/components/dataserviceReport/dataserviceReport.component.tsx +++ b/src/pages/report-page/components/dataserviceReport/dataserviceReport.component.tsx @@ -25,6 +25,7 @@ import { PATHNAME_DATA_SERVICES } from '../../../../constants/constants'; import { patchSearchQuery } from '../../../../lib/addOrReplaceUrlParam'; import localization from '../../../../lib/localization'; import { DataServiceReport, KeyWithCountObject } from '../../../../types'; +import { Line } from '../../../../components/charts'; import { List } from '../../../../components/list/list'; import { sortKeyWithCount } from '../../sort-helper'; import { translatePrefixedFormat } from '../../../../utils/common'; @@ -32,6 +33,7 @@ import { ContainerBoxRegular, ContainerPaneContent } from '../../styled'; interface ExternalProps { dataServicesReport?: Partial; + dataServicesTimeSeries?: any; } interface Props extends ExternalProps, RouteComponentProps {} @@ -43,9 +45,11 @@ const DataserviceReport: FC = ({ newLastWeek = 0, organizationCount = 0, formats = [] - } = {} + } = {}, + dataServicesTimeSeries = [] }) => { const { search: searchParams } = location; + dataServicesTimeSeries.push([Date.now(), totalObjects]); const topMostUsedFormats: KeyWithCountObject[] = sortKeyWithCount(formats) .filter( @@ -106,6 +110,26 @@ const DataserviceReport: FC = ({ + {dataServicesTimeSeries?.length > 0 && + dataServicesTimeSeries?.length > 0 && ( +
+
+ + + +
+
+ )} + {Array.isArray(topMostUsedFormats) && topMostUsedFormats?.length > 0 && (
diff --git a/src/pages/report-page/components/datasetReport/datasetReport.component.tsx b/src/pages/report-page/components/datasetReport/datasetReport.component.tsx index 6afb25c9c..7713c3981 100644 --- a/src/pages/report-page/components/datasetReport/datasetReport.component.tsx +++ b/src/pages/report-page/components/datasetReport/datasetReport.component.tsx @@ -30,6 +30,7 @@ import { PATHNAME_DATASETS } from '../../../../constants/constants'; import { patchSearchQuery } from '../../../../lib/addOrReplaceUrlParam'; import localization from '../../../../lib/localization'; import { getTranslateText as translate } from '../../../../lib/translateText'; +import { Line } from '../../../../components/charts'; import withReferenceData, { Props as ReferenceDataProps } from '../../../../components/with-reference-data'; @@ -48,6 +49,7 @@ import { interface ExternalProps { datasetsReport: Partial; + datasetsTimeSeries: any; } interface Props @@ -69,7 +71,8 @@ const DatasetReport: FC = ({ accessRights = [], formats = [], themesAndTopicsCount = [] - } = {} + } = {}, + datasetsTimeSeries = [] }) => { useEffect(() => { if (!los) { @@ -77,6 +80,7 @@ const DatasetReport: FC = ({ } }, []); + datasetsTimeSeries.push([Date.now(), totalObjects]); const accessRightsPublic = accessRights?.find((item: KeyWithCountObject) => item.key === 'PUBLIC') ?.count ?? 0; @@ -171,6 +175,23 @@ const DatasetReport: FC = ({
+ {datasetsTimeSeries?.length > 0 && datasetsTimeSeries?.length > 0 && ( +
+
+ + + +
+
+ )} + {Number(totalObjects) > 0 && (
diff --git a/src/pages/report-page/components/informationModelReport/informationModelReport.component.tsx b/src/pages/report-page/components/informationModelReport/informationModelReport.component.tsx index 07d5a6912..f69a65033 100644 --- a/src/pages/report-page/components/informationModelReport/informationModelReport.component.tsx +++ b/src/pages/report-page/components/informationModelReport/informationModelReport.component.tsx @@ -22,11 +22,13 @@ import NewIcon from '../../../../images/icon-new-md.svg'; import { patchSearchQuery } from '../../../../lib/addOrReplaceUrlParam'; import { PATHNAME_INFORMATIONMODELS } from '../../../../constants/constants'; import localization from '../../../../lib/localization'; +import { Line } from '../../../../components/charts'; import { Report } from '../../../../types'; import { ContainerBoxRegular, ContainerPaneContent } from '../../styled'; interface Props extends RouteComponentProps { informationModelsReport?: Partial; + informationModelsTimeSeries: any; } const InformationModelReport: FC = ({ @@ -35,60 +37,86 @@ const InformationModelReport: FC = ({ totalObjects = 0, newLastWeek = 0, organizationCount = 0 - } = {} -}) => ( - -
- - - - - } - count={totalObjects} - /> - - {localization.report.informationModelsDescription} - - - - - - - - } count={newLastWeek} /> - - {localization.report.newPastWeek} - - - - - + } = {}, + informationModelsTimeSeries = [] +}) => { + informationModelsTimeSeries.push([Date.now(), totalObjects]); + return ( + +
+ + + + + } + count={totalObjects} + /> + + {localization.report.informationModelsDescription} + + + + + + + + } count={newLastWeek} /> + + {localization.report.newPastWeek} + + + + + -
-
- - - } - count={organizationCount} - /> - - {localization.report.organizationsInformationModel} - - - +
+
+ + + } + count={organizationCount} + /> + + {localization.report.organizationsInformationModel} + + + +
-
-
-
-); + + {informationModelsTimeSeries?.length > 0 && + informationModelsTimeSeries?.length > 0 && ( +
+
+ + + +
+
+ )} +
+
+ ); +}; export default withRouter(memo(InformationModelReport)); diff --git a/src/pages/report-page/report-page-pure.tsx b/src/pages/report-page/report-page-pure.tsx index 226346925..d3f677015 100644 --- a/src/pages/report-page/report-page-pure.tsx +++ b/src/pages/report-page/report-page-pure.tsx @@ -31,6 +31,10 @@ interface Props { informationModelsReport?: any; datasetsReport?: any; conceptsReport?: any; + datasetsTimeSeries?: any; + dataServicesTimeSeries?: any; + informationModelsTimeSeries?: any; + conceptsTimeSeries?: any; } export const ReportPagePure: FC = ({ @@ -39,7 +43,11 @@ export const ReportPagePure: FC = ({ dataServicesReport, informationModelsReport, datasetsReport, - conceptsReport + conceptsReport, + datasetsTimeSeries, + dataServicesTimeSeries, + informationModelsTimeSeries, + conceptsTimeSeries }) => { const history = useHistory(); const { search } = useLocation(); @@ -148,7 +156,10 @@ export const ReportPagePure: FC = ({
{getConfig().isNapProfile ? ( - + ) : ( @@ -184,19 +195,29 @@ export const ReportPagePure: FC = ({ /> - + - + diff --git a/src/pages/report-page/report-page-resolver.js b/src/pages/report-page/report-page-resolver.js index fe023d4fd..3e6bd255a 100644 --- a/src/pages/report-page/report-page-resolver.js +++ b/src/pages/report-page/report-page-resolver.js @@ -6,15 +6,27 @@ import { getInformationModelsReport, getDataServicesReport } from '../../api/report-api/reports'; +import { + conceptTimeSeriesRequest, + dataServiceTimeSeriesRequest, + datasetTimeSeriesRequest, + infoModelTimeSeriesRequest +} from '../../api/statistics-api/time-series'; import { parseSearchParams } from '../../lib/location-history-helper'; import { extractConcepts, searchConcepts } from '../../api/search-api/concepts'; import { paramsToSearchBody } from '../../utils/common/index'; const memoizedGetDatasetsReport = memoize(getDatasetsReport); +const memoizedGetDatasetsTimeSeries = memoize(datasetTimeSeriesRequest); const memoizedGetDataServicesReport = memoize(getDataServicesReport); +const memoizedGetDataServicesTimeSeries = memoize(dataServiceTimeSeriesRequest); const memoizedGetConceptsReport = memoize(getConceptsReport); +const memoizedGetConceptsTimeSeries = memoize(conceptTimeSeriesRequest); const memoizedGetInformationModelsReport = memoize(getInformationModelsReport); +const memoizedGetInformationModelsTimeSeries = memoize( + infoModelTimeSeriesRequest +); const memoizedSearchConcepts = memoize(searchConcepts); const mapProps = { @@ -22,10 +34,18 @@ const mapProps = { const { orgPath, losTheme: los } = parseSearchParams(location); return memoizedGetDatasetsReport({ orgPath, los }); }, + datasetsTimeSeries: ({ location }) => { + const { orgPath } = parseSearchParams(location); + return memoizedGetDatasetsTimeSeries(orgPath); + }, dataServicesReport: ({ location }) => { const { orgPath } = parseSearchParams(location); return memoizedGetDataServicesReport({ orgPath }); }, + dataServicesTimeSeries: ({ location }) => { + const { orgPath } = parseSearchParams(location); + return memoizedGetDataServicesTimeSeries(orgPath); + }, conceptsReport: async ({ location }) => { const { orgPath, losTheme: los } = parseSearchParams(location); @@ -39,9 +59,17 @@ const mapProps = { return { ...reportItems, allReferencedConcepts }; }, + conceptsTimeSeries: ({ location }) => { + const { orgPath } = parseSearchParams(location); + return memoizedGetConceptsTimeSeries(orgPath); + }, informationModelsReport: ({ location }) => { const { orgPath, losTheme: los } = parseSearchParams(location); return memoizedGetInformationModelsReport({ orgPath, los }); + }, + informationModelsTimeSeries: ({ location }) => { + const { orgPath } = parseSearchParams(location); + return memoizedGetInformationModelsTimeSeries(orgPath); } }; diff --git a/src/types/domain.d.ts b/src/types/domain.d.ts index fabc531cb..5931eaaa5 100644 --- a/src/types/domain.d.ts +++ b/src/types/domain.d.ts @@ -819,9 +819,22 @@ interface QualifiedAttribution { role: string; } -export interface DataPoint { - xAxis: string; - yAxis: string; +export interface TimeSeriesPoint { + date: string; + count: number; +} + +export interface TimeSeriesRequest { + start: string; + end: string; + interval: string; + filters: TimeSeriesFilters; +} + +interface TimeSeriesFilters { + resourceType: { value: string } | null; + orgPath: { value: string } | null; + transport: { value: boolean } | null; } interface Report {