diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml
index e06280b48..91ef62be0 100644
--- a/.github/workflows/terraform.yml
+++ b/.github/workflows/terraform.yml
@@ -33,6 +33,8 @@ jobs:
API_URL=https://gcp9dahm4c.execute-api.ca-central-1.amazonaws.com/test/app
API_AUTH_SECRET=${{ secrets.API_AUTH_SECRET }}
APP_ENV=development
+ GRAFANA_API_URL=https://sso-grafana-sandbox.apps.gold.devops.gov.bc.ca/api
+ GRAFANA_API_TOKEN=${{ secrets.DEV_GRAFANA_API_TOKEN }}
TF_STATE_BUCKET=xgr00q-dev-sso-requests
TF_STATE_BUCKET_KEY=css-tf-dev-state
TF_STATE_DYNAMODB_TABLE=xgr00q-dev-sso-requests-state-locking
@@ -69,6 +71,8 @@ jobs:
API_URL=https://kgodz1zmk2.execute-api.ca-central-1.amazonaws.com/test/app
API_AUTH_SECRET=${{ secrets.API_AUTH_SECRET }}
APP_ENV=production
+ GRAFANA_API_URL=https://sso-grafana.apps.gold.devops.gov.bc.ca/api
+ GRAFANA_API_TOKEN=${{ secrets.PROD_GRAFANA_API_TOKEN }}
TF_STATE_BUCKET=xgr00q-prod-sso-requests
TF_STATE_BUCKET_KEY=css-tf-prod-state
TF_STATE_DYNAMODB_TABLE=xgr00q-prod-sso-requests-state-locking
@@ -209,6 +213,8 @@ jobs:
custom_domain_name="${{ env.CUSTOM_DOMAIN_NAME }}"
uptime_status_domain_name="${{ env.UPTIME_STATUS_DOMAIN_NAME }}"
aws_ecr_uri="${{ env.AWS_ECR_URI }}"
+ grafana_api_token="${{ env.GRAFANA_API_TOKEN }}"
+ grafana_api_url="${{ env.GRAFANA_API_URL }}"
include_digital_credential="${{ env.INCLUDE_DIGITAL_CREDENTIAL }}"
EOF
diff --git a/app/components/DateTimePicker.tsx b/app/components/DateTimePicker.tsx
new file mode 100644
index 000000000..e6bb61100
--- /dev/null
+++ b/app/components/DateTimePicker.tsx
@@ -0,0 +1,36 @@
+import DatePicker, { ReactDatePickerProps } from 'react-datepicker';
+import 'react-datepicker/dist/react-datepicker.css';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faCalendar } from '@fortawesome/free-solid-svg-icons';
+import { FormLabel } from 'react-bootstrap';
+
+export default function DateTimePicker(props: ReactDatePickerProps & { label: string }) {
+ return (
+ <>
+ }
+ enableTabLoop={false}
+ popperPlacement="bottom-start"
+ maxDate={new Date()}
+ {...props}
+ />
+ >
+ );
+}
+
+function DateTimeInput({ value, onClick, label }: any) {
+ return (
+ <>
+ {label}
+
+ >
+ );
+}
diff --git a/app/interfaces/Request.ts b/app/interfaces/Request.ts
index 3ef1f68c9..84bed60f9 100644
--- a/app/interfaces/Request.ts
+++ b/app/interfaces/Request.ts
@@ -103,3 +103,8 @@ export interface ClientRole {
name: string;
composites: string[];
}
+
+export interface EventCountMetric {
+ event: string;
+ count: number;
+}
diff --git a/app/package.json b/app/package.json
index 7e4bcb596..f2a4efe7e 100644
--- a/app/package.json
+++ b/app/package.json
@@ -49,10 +49,12 @@
"lodash.tolower": "^4.1.2",
"lodash.trim": "^4.5.1",
"lodash.uniq": "^4.5.0",
+ "moment": "^2.29.4",
"next": "13.2.1",
"re-resizable": "6.9.9",
"react": "18.2.0",
"react-bootstrap": "2.5.0",
+ "react-datepicker": "^4.24.0",
"react-dom": "18.2.0",
"react-idle-timer": "^5.7.2",
"react-infinite-scroller": "1.2.6",
@@ -63,6 +65,7 @@
"react-select": "^5.7.2",
"react-table": "^7.8.0",
"react-typography": "0.16.20",
+ "recharts": "^2.10.3",
"styled-components": "5.3.5",
"validator": "13.7.0",
"xlsx": "^0.18.5"
@@ -99,6 +102,7 @@
"@types/lodash.trim": "^4.5.7",
"@types/lodash.uniq": "^4.5.7",
"@types/react": "18.0.21",
+ "@types/react-datepicker": "^4.19.4",
"@types/react-infinite-scroller": "1.2.3",
"@types/react-jsonschema-form": "1.7.8",
"@types/react-select": "^5.0.1",
diff --git a/app/page-partials/my-dashboard/IntegrationInfoTabs/MetricsPanel.tsx b/app/page-partials/my-dashboard/IntegrationInfoTabs/MetricsPanel.tsx
new file mode 100644
index 000000000..e03b89b9b
--- /dev/null
+++ b/app/page-partials/my-dashboard/IntegrationInfoTabs/MetricsPanel.tsx
@@ -0,0 +1,184 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import styled from 'styled-components';
+import { EventCountMetric, Integration } from 'interfaces/Request';
+import { withTopAlert } from 'layout/TopAlert';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tabs, Tab } from '@bcgov-sso/common-react-components';
+import startCase from 'lodash.startcase';
+import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Text, Legend } from 'recharts';
+import { getMetrics } from '@app/services/grafana';
+import throttle from 'lodash.throttle';
+import moment from 'moment';
+import DateTimePicker from '@app/components/DateTimePicker';
+import { InfoMessage } from '@app/components/MessageBox';
+import { Link } from '@button-inc/bcgov-theme';
+import { subtractDaysFromDate } from '@app/utils/helpers';
+
+export const DatePickerContainer = styled.div`
+ height: 100%;
+ display: flex;
+ align-items: center;
+ padding-right: 15px;
+ & > * {
+ margin-left: 15px;
+ }
+`;
+
+const Label = styled.label`
+ margin-bottom: 2px;
+`;
+
+const TopMargin = styled.div`
+ height: var(--field-top-spacing);
+`;
+
+const LeftTitle = styled.span`
+ color: #000;
+ font-size: 1.1rem;
+ font-weight: bold;
+`;
+
+const PaddedIcon = styled(FontAwesomeIcon)`
+ margin-right: 20px;
+`;
+
+const StyledP = styled.div`
+ margin-bottom: 5px;
+ display: flex;
+ align-items: center;
+`;
+
+const StyledHr = styled.hr`
+ background-color: black;
+`;
+
+interface Props {
+ integration: Integration;
+}
+
+const getFormattedDateString = (d: Date) => {
+ return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
+};
+
+const metricsStartDate = 'December 01, 2023';
+const MetricsPanel = ({ integration }: Props) => {
+ const [environment, setEnvironment] = useState('dev');
+ const environments = integration?.environments || [];
+ const [metrics, setMetrics] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [fromDate, setFromDate] = useState(subtractDaysFromDate(14));
+ const [toDate, setToDate] = useState(new Date());
+ let selectedFromDate: Date = new Date();
+
+ const handleTabSelect = (key: string) => {
+ setEnvironment(key);
+ };
+
+ const handleFromDateChange = (val: Date) => {
+ setFromDate(val);
+ };
+
+ const handleToDateChange = (val: Date) => {
+ setToDate(val);
+ };
+
+ const fetchMetrics = useCallback(
+ throttle(async (fromDate: string, toDate: string, environment: string) => {
+ const [metricsData, err] = await getMetrics(integration?.id as number, environment, fromDate, toDate);
+
+ if (err || metricsData.length === 0) {
+ setMetrics([]);
+ } else {
+ setMetrics(metricsData);
+ }
+ }),
+ [integration?.clientId, environment, fromDate, toDate],
+ );
+
+ useEffect(() => {
+ fetchMetrics(getFormattedDateString(fromDate), getFormattedDateString(toDate), environment);
+ }, [integration?.clientId, environment, fromDate, toDate]);
+
+ return (
+ <>
+
+
+
+
+ handleFromDateChange(date)}
+ minDate={new Date(metricsStartDate)}
+ maxDate={toDate}
+ label="Start Date"
+ />
+ handleToDateChange(date)}
+ minDate={fromDate}
+ label="End Date"
+ />
+
+
+
+
+
+
+ {environments.map((env) => (
+
+
+ {metrics?.length > 0 ? (
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ No data available yet!
+
+ )}
+
+
+ ))}
+
+
+ This tab was released {metricsStartDate}. Please refer to{' '}
+
+ here
+ {' '}
+ for event type details.
+
+ >
+ );
+};
+
+export default withTopAlert(MetricsPanel);
diff --git a/app/page-partials/my-dashboard/IntegrationInfoTabs/index.tsx b/app/page-partials/my-dashboard/IntegrationInfoTabs/index.tsx
index 8b86be6bd..cdb6da20a 100644
--- a/app/page-partials/my-dashboard/IntegrationInfoTabs/index.tsx
+++ b/app/page-partials/my-dashboard/IntegrationInfoTabs/index.tsx
@@ -24,6 +24,7 @@ import BceidStatusPanel from './BceidStatusPanel';
import GithubStatusPanel from './GithubStatusPanel';
import ServiceAccountRoles from 'page-partials/my-dashboard/ServiceAccountRoles';
import DigitalCredentialPanel from './DigitalCredentialPanel';
+import MetricsPanel from './MetricsPanel';
const TabWrapper = styled.div<{ short?: boolean }>`
padding-left: 1rem;
@@ -44,6 +45,7 @@ const TAB_USER_ROLE_MANAGEMENT = 'user-role-management';
const TAB_SERVICE_ACCOUNT_ROLE_MANAGEMENT = 'service-account-role-management';
const TAB_SECRET = 'secret';
const TAB_HISTORY = 'history';
+const TAB_METRICS = 'metrics';
const joinEnvs = (integration: Integration) => {
if (!integration?.environments) return '';
@@ -210,6 +212,16 @@ const getSecretsTab = ({ integration }: { integration: Integration }) => {
);
};
+const getMetricsTab = ({ integration }: { integration: Integration }) => {
+ return (
+
+
+
+
+
+ );
+};
+
const getHistoryTab = ({ integration }: { integration: Integration }) => {
return (
@@ -327,6 +339,9 @@ function IntegrationInfoTabs({ integration }: Props) {
tabs.push(getHistoryTab({ integration }));
allowedTabs.push(TAB_HISTORY);
+
+ tabs.push(getMetricsTab({ integration }));
+ allowedTabs.push(TAB_METRICS);
}
let activeKey = activeTab;
diff --git a/app/services/grafana.ts b/app/services/grafana.ts
new file mode 100644
index 000000000..4f1b44fba
--- /dev/null
+++ b/app/services/grafana.ts
@@ -0,0 +1,14 @@
+import { instance } from './axios';
+
+export const getMetrics = async (id: number, env: string, fromDate?: string, toDate?: string) => {
+ try {
+ const result = await instance
+ .get(`requests/${id}/metrics?env=${env}&fromDate=${fromDate}&toDate=${toDate}`)
+ .then((res: any) => res?.data);
+
+ return [result, null];
+ } catch (err) {
+ console.error(err);
+ return [null, err];
+ }
+};
diff --git a/app/utils/helpers.tsx b/app/utils/helpers.tsx
index ad1d28b88..be477ea2e 100644
--- a/app/utils/helpers.tsx
+++ b/app/utils/helpers.tsx
@@ -321,3 +321,9 @@ export const checkIfDigitalCredentialProdApplying = (integration: Integration) =
const prodApplying = checkIfProdApplying(integration, 'digitalCredentialApproved');
return prodApplying;
};
+
+export const subtractDaysFromDate = (days: number) => {
+ const d = new Date();
+ d.setDate(d.getDate() - days);
+ return d;
+};
diff --git a/app/yarn.lock b/app/yarn.lock
index 0732fa3cd..abf4b6dd0 100644
--- a/app/yarn.lock
+++ b/app/yarn.lock
@@ -1054,6 +1054,13 @@
core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.1.2":
+ version "7.23.5"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.5.tgz#11edb98f8aeec529b82b211028177679144242db"
+ integrity sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==
+ dependencies:
+ regenerator-runtime "^0.14.0"
+
"@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.14.6", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.4.2", "@babel/runtime@^7.7.6":
version "7.22.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438"
@@ -1068,6 +1075,13 @@
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.21.0":
+ version "7.23.6"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d"
+ integrity sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==
+ dependencies:
+ regenerator-runtime "^0.14.0"
+
"@babel/template@^7.18.10", "@babel/template@^7.3.3":
version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
@@ -1874,6 +1888,11 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64"
integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==
+"@popperjs/core@^2.11.8", "@popperjs/core@^2.9.2":
+ version "2.11.8"
+ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
+ integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
+
"@react-aria/ssr@^3.2.0":
version "3.3.0"
resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.3.0.tgz#25e81daf0c7a270a4a891159d8d984578e4512d8"
@@ -2203,6 +2222,57 @@
dependencies:
"@babel/types" "^7.3.0"
+"@types/d3-array@^3.0.3":
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5"
+ integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==
+
+"@types/d3-color@*":
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2"
+ integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
+
+"@types/d3-ease@^3.0.0":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b"
+ integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
+
+"@types/d3-interpolate@^3.0.1":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
+ integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
+ dependencies:
+ "@types/d3-color" "*"
+
+"@types/d3-path@*":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.2.tgz#4327f4a05d475cf9be46a93fc2e0f8d23380805a"
+ integrity sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==
+
+"@types/d3-scale@^4.0.2":
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb"
+ integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==
+ dependencies:
+ "@types/d3-time" "*"
+
+"@types/d3-shape@^3.1.0":
+ version "3.1.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72"
+ integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==
+ dependencies:
+ "@types/d3-path" "*"
+
+"@types/d3-time@*", "@types/d3-time@^3.0.0":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be"
+ integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==
+
+"@types/d3-timer@^3.0.0":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70"
+ integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
+
"@types/graceful-fs@^4.1.3":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15"
@@ -2490,6 +2560,16 @@
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
+"@types/react-datepicker@^4.19.4":
+ version "4.19.4"
+ resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-4.19.4.tgz#07807187ead4c7025bc0039fe05eb5713298f3e4"
+ integrity sha512-HRD0LHTxBVe61LRJgTdPscbapLQl7+jI/7bxnPGpvzdJ/iXN9q7ucYv8HKULeIAN84O5LzFhwTMOkO4QnIUJaQ==
+ dependencies:
+ "@popperjs/core" "^2.9.2"
+ "@types/react" "*"
+ date-fns "^2.0.1"
+ react-popper "^2.2.5"
+
"@types/react-dom@^18.0.0":
version "18.0.6"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.6.tgz#36652900024842b74607a17786b6662dd1e103a1"
@@ -3187,6 +3267,11 @@ clsx@1.2.1, clsx@^1.1.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
+clsx@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
+ integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==
+
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -3413,6 +3498,77 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
+"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
+ integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
+ dependencies:
+ internmap "1 - 2"
+
+"d3-color@1 - 3":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
+ integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+d3-ease@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
+ integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+"d3-format@1 - 3":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
+ integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
+
+"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
+ integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+ dependencies:
+ d3-color "1 - 3"
+
+d3-path@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
+ integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
+
+d3-scale@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
+ integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
+ dependencies:
+ d3-array "2.10.0 - 3"
+ d3-format "1 - 3"
+ d3-interpolate "1.2.0 - 3"
+ d3-time "2.1.1 - 3"
+ d3-time-format "2 - 4"
+
+d3-shape@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
+ integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
+ dependencies:
+ d3-path "^3.1.0"
+
+"d3-time-format@2 - 4":
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
+ integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+ dependencies:
+ d3-time "1 - 3"
+
+"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
+ integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
+ dependencies:
+ d3-array "2 - 3"
+
+d3-timer@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+ integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
damerau-levenshtein@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -3427,6 +3583,13 @@ data-urls@^3.0.2:
whatwg-mimetype "^3.0.0"
whatwg-url "^11.0.0"
+date-fns@^2.0.1, date-fns@^2.30.0:
+ version "2.30.0"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
+ integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
+ dependencies:
+ "@babel/runtime" "^7.21.0"
+
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
@@ -3453,6 +3616,11 @@ decamelize@^1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
+decimal.js-light@^2.4.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
+ integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
+
decimal.js@^10.3.1:
version "10.3.1"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
@@ -3542,6 +3710,13 @@ dom-align@^1.7.0:
resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.4.tgz#3503992eb2a7cfcb2ed3b2a6d21e0b9c00d54511"
integrity sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==
+dom-helpers@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
+ integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
+ dependencies:
+ "@babel/runtime" "^7.1.2"
+
dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
@@ -3946,6 +4121,11 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+eventemitter3@^4.0.1:
+ version "4.0.7"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+ integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
execa@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@@ -3982,6 +4162,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+fast-equals@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.0.1.tgz#a4eefe3c5d1c0d021aeed0bc10ba5e0c12ee405d"
+ integrity sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==
+
fast-glob@^3.2.11:
version "3.2.12"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
@@ -4502,6 +4687,11 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"
+"internmap@1 - 2":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
+ integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@@ -5508,7 +5698,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
-lodash@^4.13.1, lodash@^4.17.15, lodash@^4.17.21, lodash@^4.17.4:
+lodash@^4.13.1, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -5645,6 +5835,11 @@ modularscale@^1.0.2:
dependencies:
lodash.isnumber "^3.0.0"
+moment@^2.29.4:
+ version "2.29.4"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
+ integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
+
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -6263,6 +6458,18 @@ react-bootstrap@2.5.0:
uncontrollable "^7.2.1"
warning "^4.0.3"
+react-datepicker@^4.24.0:
+ version "4.24.0"
+ resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.24.0.tgz#dfb12e277993f1ae2d350b7ba4dd6bba7d21bfb1"
+ integrity sha512-2QUC2pP+x4v3Jp06gnFllxKsJR0yoT/K6y86ItxEsveTXUpsx+NBkChWXjU0JsGx/PL8EQnsxN0wHl4zdA1m/g==
+ dependencies:
+ "@popperjs/core" "^2.11.8"
+ classnames "^2.2.6"
+ date-fns "^2.30.0"
+ prop-types "^15.7.2"
+ react-onclickoutside "^6.13.0"
+ react-popper "^2.3.0"
+
react-dom@18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
@@ -6271,6 +6478,11 @@ react-dom@18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
+react-fast-compare@^3.0.1:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
+ integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
+
react-idle-timer@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/react-idle-timer/-/react-idle-timer-5.7.2.tgz#f506db28a86645dd1b87987116501703e512142b"
@@ -6283,7 +6495,7 @@ react-infinite-scroller@1.2.6:
dependencies:
prop-types "^15.5.8"
-react-is@^16.12.0, react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.4, react-is@^16.9.0:
+react-is@^16.10.2, react-is@^16.12.0, react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.4, react-is@^16.9.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -6333,11 +6545,24 @@ react-multi-select-component@4.3.3:
resolved "https://registry.yarnpkg.com/react-multi-select-component/-/react-multi-select-component-4.3.3.tgz#142c0c9b51a3dbe99d117e90eaa14f48041174b6"
integrity sha512-V8cDJC3M7F27PWv1baV8FpJReHa/SbpJGL80CmXwnlMkDK2KMlQSRDmDzBnmCjcbROIgoztdW+gYBpqo9BIF4g==
+react-onclickoutside@^6.13.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz#e165ea4e5157f3da94f4376a3ab3e22a565f4ffc"
+ integrity sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==
+
react-placeholder@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/react-placeholder/-/react-placeholder-4.1.0.tgz#943128820b3b0a6f94371655aadec18d306e05e3"
integrity sha512-z1HGD86NWJTYTQumHsmGH9jkozv4QHa9dju/vHVUd4f1svu23pf5v7QoBLBfs3kA1S9GLJaCeRMHLbO2SCdz5A==
+react-popper@^2.2.5, react-popper@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.3.0.tgz#17891c620e1320dce318bad9fede46a5f71c70ba"
+ integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==
+ dependencies:
+ react-fast-compare "^3.0.1"
+ warning "^4.0.2"
+
react-property@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-property/-/react-property-2.0.0.tgz#2156ba9d85fa4741faf1918b38efc1eae3c6a136"
@@ -6368,6 +6593,14 @@ react-sizeme@^3.0.1:
shallowequal "^1.1.0"
throttle-debounce "^3.0.1"
+react-smooth@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-2.0.5.tgz#d153b7dffc7143d0c99e82db1532f8cf93f20ecd"
+ integrity sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA==
+ dependencies:
+ fast-equals "^5.0.0"
+ react-transition-group "2.9.0"
+
react-syntax-highlighter@^15.4.3:
version "15.5.0"
resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20"
@@ -6384,6 +6617,16 @@ react-table@^7.8.0:
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.8.0.tgz#07858c01c1718c09f7f1aed7034fcfd7bda907d2"
integrity sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==
+react-transition-group@2.9.0:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
+ integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
+ dependencies:
+ dom-helpers "^3.4.0"
+ loose-envify "^1.4.0"
+ prop-types "^15.6.2"
+ react-lifecycles-compat "^3.0.4"
+
react-transition-group@^4.3.0, react-transition-group@^4.4.2:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
@@ -6419,6 +6662,27 @@ readable-stream@^2.2.2:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
+recharts-scale@^0.4.4:
+ version "0.4.5"
+ resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9"
+ integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==
+ dependencies:
+ decimal.js-light "^2.4.1"
+
+recharts@^2.10.3:
+ version "2.10.3"
+ resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.10.3.tgz#a5dbe219354d744701e8bbd116fe42393af92f6b"
+ integrity sha512-G4J96fKTZdfFQd6aQnZjo2nVNdXhp+uuLb00+cBTGLo85pChvm1+E67K3wBOHDE/77spcYb2Cy9gYWVqiZvQCg==
+ dependencies:
+ clsx "^2.0.0"
+ eventemitter3 "^4.0.1"
+ lodash "^4.17.19"
+ react-is "^16.10.2"
+ react-smooth "^2.0.5"
+ recharts-scale "^0.4.4"
+ tiny-invariant "^1.3.1"
+ victory-vendor "^36.6.8"
+
redent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
@@ -6458,6 +6722,11 @@ regenerator-runtime@^0.13.7:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
+regenerator-runtime@^0.14.0:
+ version "0.14.0"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
+ integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
+
regenerator-transform@^0.15.0:
version "0.15.0"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537"
@@ -6971,6 +7240,11 @@ tiny-glob@^0.2.9:
globalyzer "0.1.0"
globrex "^0.1.2"
+tiny-invariant@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642"
+ integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==
+
tmpl@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
@@ -7228,6 +7502,26 @@ validator@13.7.0:
resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857"
integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==
+victory-vendor@^36.6.8:
+ version "36.7.0"
+ resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.7.0.tgz#e02af33e249e74e659fa65c6d5936042c42e7aa8"
+ integrity sha512-nqYuTkLSdTTeACyXcCLbL7rl0y6jpzLPtTNGOtSnajdR+xxMxBdjMxDjfNJNlhR+ZU8vbXz+QejntcbY7h9/ZA==
+ dependencies:
+ "@types/d3-array" "^3.0.3"
+ "@types/d3-ease" "^3.0.0"
+ "@types/d3-interpolate" "^3.0.1"
+ "@types/d3-scale" "^4.0.2"
+ "@types/d3-shape" "^3.1.0"
+ "@types/d3-time" "^3.0.0"
+ "@types/d3-timer" "^3.0.0"
+ d3-array "^3.1.6"
+ d3-ease "^3.0.1"
+ d3-interpolate "^3.0.1"
+ d3-scale "^4.0.2"
+ d3-shape "^3.1.0"
+ d3-time "^3.0.0"
+ d3-timer "^3.0.1"
+
w3c-hr-time@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
@@ -7249,7 +7543,7 @@ walker@^1.0.8:
dependencies:
makeerror "1.0.12"
-warning@^4.0.0, warning@^4.0.3:
+warning@^4.0.0, warning@^4.0.2, warning@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
diff --git a/lambda/__tests__/15.sso-logs.test.ts b/lambda/__tests__/15.sso-logs.test.ts
new file mode 100644
index 000000000..3b1593dfd
--- /dev/null
+++ b/lambda/__tests__/15.sso-logs.test.ts
@@ -0,0 +1,169 @@
+import app from './helpers/server';
+import supertest from 'supertest';
+import { APP_BASE_PATH } from './helpers/constants';
+import { cleanUpDatabaseTables, createMockAuth } from './helpers/utils';
+import { getUpdateIntegrationData } from './helpers/fixtures';
+import { models } from '@lambda-shared/sequelize/models/models';
+import { queryGrafana } from '../app/src/grafana';
+
+jest.mock('../app/src/authenticate');
+jest.mock('../app/src/grafana', () => {
+ return {
+ queryGrafana: jest.fn(() => Promise.resolve(['log', 'log'])),
+ };
+});
+
+describe('Fetch SSO Logs', () => {
+ const integration = getUpdateIntegrationData({ integration: { projectName: 'test_project' } });
+
+ afterEach(async () => {
+ await cleanUpDatabaseTables();
+ });
+
+ const MOCK_USER_ID = -1;
+ const MOCK_USER_EMAIL = 'test@user.com';
+ const INTEGRATION_ID = -1;
+
+ // Create integration owned by mock user to test against
+ const setupIntegrationAndUser = async () => {
+ await models.user.create({
+ id: MOCK_USER_ID,
+ idirEmail: MOCK_USER_EMAIL,
+ });
+ await models.request.create({
+ ...integration,
+ id: INTEGRATION_ID,
+ usesTeam: false,
+ userId: MOCK_USER_ID,
+ });
+ };
+
+ // A valid query string to use
+ const queryString = `env=dev&start=2020-10-10&end=2020-10-12&eventType=LOGIN`;
+
+ it('Handles validation correctly for directly owned requests', async () => {
+ await setupIntegrationAndUser();
+ // Create session with a different user login, expect 401
+ createMockAuth('1', 'wrong@user.com');
+ let response = await supertest(app)
+ .get(`${APP_BASE_PATH}/requests/${INTEGRATION_ID}/logs?${queryString}`)
+ .set('Accept', 'application/json');
+
+ expect(response.status).toBe(401);
+
+ // Create session with actual user, expect 200
+ createMockAuth('2', MOCK_USER_EMAIL);
+ response = await supertest(app)
+ .get(`${APP_BASE_PATH}/requests/${INTEGRATION_ID}/logs?${queryString}`)
+ .set('Accept', 'application/json');
+
+ expect(response.status).toBe(200);
+ });
+
+ it('Handles authentication correctly for team ownership', async () => {
+ const TEAM_MEMBER_EMAIL = 'team@member.com';
+ const NOT_A_TEAM_EMAIL = 'not_a_team@member.com';
+ const INTEGRATION_ID = -1;
+
+ await models.team.create({
+ id: -1,
+ name: 'test_team',
+ });
+ await models.user.create({
+ id: -2,
+ idirEmail: TEAM_MEMBER_EMAIL,
+ });
+ await models.request.create({
+ ...integration,
+ id: INTEGRATION_ID,
+ usesTeam: true,
+ teamId: -1,
+ });
+ await models.usersTeam.create({
+ userId: -2,
+ teamId: -1,
+ role: 'member',
+ pending: false,
+ });
+
+ // Create session as team member, expect 200
+ createMockAuth('1', TEAM_MEMBER_EMAIL);
+ let response = await supertest(app)
+ .get(`${APP_BASE_PATH}/requests/${INTEGRATION_ID}/logs?${queryString}`)
+ .set('Accept', 'application/json');
+
+ expect(response.status).toBe(200);
+
+ // Create session not on the team. Expect 401
+ createMockAuth('2', NOT_A_TEAM_EMAIL);
+ response = await supertest(app)
+ .get(`${APP_BASE_PATH}/requests/${INTEGRATION_ID}/logs?${queryString}`)
+ .set('Accept', 'application/json');
+
+ expect(response.status).toBe(401);
+ });
+
+ it('Responds with 400 if supplied query parameters are wrong', async () => {
+ await setupIntegrationAndUser();
+
+ // Covering some common invalid cases
+ const invalidParams = [
+ // Missing params
+ '',
+ 'env=dev',
+ 'env=dev&eventType=LOGIN',
+ 'env=dev&eventType=LOGIN&start=2022-10-10',
+ // Invalid values
+ 'env=development&eventType=LOGIN&start=2022-10-10&end=2022-10-10',
+ 'env=dev&eventType=MISSING&start=2022-10-10&end=2022-10-10',
+ 'env=dev&eventType=LOGIN&start=somedate&end=2022-10-10',
+ 'env=dev&eventType=LOGIN&start=2022-10-10&end=anotherDate',
+ 'env=dev&eventType=LOGIN&start=2022-14-10&end=2022-10-42',
+ ];
+
+ const validParams = [
+ 'env=dev&eventType=LOGIN&start=2022-10-10&end=2022-10-10',
+ 'env=test&eventType=LOGIN&start=2022-10-10&end=2022-10-10',
+ 'env=prod&eventType=LOGIN&start=2022-10-10&end=2022-10-10',
+ 'env=dev&eventType=REFRESH_TOKEN&start=2022-10-10&end=2022-10-10',
+ 'env=dev&eventType=CODE_TO_TOKEN&start=2022-12-10&end=2022-10-10',
+ ];
+
+ createMockAuth('2', MOCK_USER_EMAIL);
+ for (const params of invalidParams) {
+ const response = await supertest(app)
+ .get(`${APP_BASE_PATH}/requests/${INTEGRATION_ID}/logs?${params}`)
+ .set('Accept', 'application/json');
+ expect(response.status).toBe(400);
+ }
+
+ for (const params of validParams) {
+ const response = await supertest(app)
+ .get(`${APP_BASE_PATH}/requests/${INTEGRATION_ID}/logs?${params}`)
+ .set('Accept', 'application/json');
+ expect(response.status).toBe(200);
+ }
+ });
+
+ it('Returns 500 if unexpected error when fetching logs', async () => {
+ await setupIntegrationAndUser();
+ (queryGrafana as jest.Mock).mockImplementationOnce(() => Promise.reject(new Error('err')));
+
+ const response = await supertest(app)
+ .get(`${APP_BASE_PATH}/requests/${INTEGRATION_ID}/logs?${queryString}`)
+ .set('Accept', 'application/json');
+
+ expect(response.status).toBe(500);
+ });
+
+ it('Returns the expected logs in JSON array if successful', async () => {
+ await setupIntegrationAndUser();
+
+ const response = await supertest(app)
+ .get(`${APP_BASE_PATH}/requests/${INTEGRATION_ID}/logs?${queryString}`)
+ .set('Accept', 'application/json');
+
+ expect(response.status).toBe(200);
+ expect(response.body.data).toEqual(['log', 'log']);
+ });
+});
diff --git a/lambda/__tests__/16.integration-metrics.test.ts b/lambda/__tests__/16.integration-metrics.test.ts
new file mode 100644
index 000000000..af67618c1
--- /dev/null
+++ b/lambda/__tests__/16.integration-metrics.test.ts
@@ -0,0 +1,104 @@
+import { TEAM_ADMIN_IDIR_USERID_01, TEAM_ADMIN_IDIR_EMAIL_01, postTeam } from './helpers/fixtures';
+import { cleanUpDatabaseTables, createMockAuth } from './helpers/utils';
+import { createTeam } from './helpers/modules/teams';
+import supertest from 'supertest';
+import app from './helpers/server';
+import { API_BASE_PATH } from './helpers/constants';
+import { buildIntegration } from './helpers/modules/common';
+import { findClientRole } from '@lambda-app/keycloak/users';
+import { Integration } from 'app/interfaces/Request';
+import {
+ createIntegration,
+ fetchMetrics,
+ getIntegration,
+ getIntegrations,
+ updateIntegration,
+} from './helpers/modules/integrations';
+
+jest.mock('../app/src/grafana', () => {
+ return {
+ clientEventsAggregationQuery: jest.fn(() => Promise.resolve([{ event: 'LOGIN', count: 10 }])),
+ };
+});
+
+jest.mock('@lambda-app/authenticate');
+jest.mock('@lambda-app/github', () => {
+ return {
+ dispatchRequestWorkflow: jest.fn(() => ({ status: 204 })),
+ closeOpenPullRequests: jest.fn(() => Promise.resolve()),
+ };
+});
+
+jest.mock('../actions/src/github', () => {
+ return {
+ mergePR: jest.fn(),
+ };
+});
+
+jest.mock('@lambda-shared/utils/ches');
+
+jest.mock('@lambda-app/authenticate');
+
+jest.mock('@lambda-actions/authenticate', () => {
+ return {
+ authenticate: jest.fn(() => {
+ return Promise.resolve(true);
+ }),
+ };
+});
+
+describe('create/manage integration by authenticated user', () => {
+ let integration: Integration;
+ try {
+ beforeAll(async () => {
+ jest.clearAllMocks();
+ createMockAuth(TEAM_ADMIN_IDIR_USERID_01, TEAM_ADMIN_IDIR_EMAIL_01);
+
+ const integrationRes = await buildIntegration({
+ projectName: 'TEST CSS Metrics Tab',
+ prodEnv: true,
+ submitted: true,
+ planned: true,
+ applied: true,
+ });
+ integration = integrationRes.body;
+ });
+
+ afterAll(async () => {
+ await cleanUpDatabaseTables();
+ });
+
+ it('should reject if date inputs are not valid', async () => {
+ createMockAuth(TEAM_ADMIN_IDIR_USERID_01, TEAM_ADMIN_IDIR_EMAIL_01);
+ const result = await fetchMetrics(integration?.id, '', '', 'dev');
+ expect(result.status).toEqual(400);
+ });
+
+ it('should reject if start date is greater than end date', async () => {
+ createMockAuth(TEAM_ADMIN_IDIR_USERID_01, TEAM_ADMIN_IDIR_EMAIL_01);
+ const result = await fetchMetrics(integration?.id, '2023-01-20', '2023-01-10', 'dev');
+ expect(result.status).toEqual(400);
+ });
+
+ it('should reject if end date is a future date', async () => {
+ createMockAuth(TEAM_ADMIN_IDIR_USERID_01, TEAM_ADMIN_IDIR_EMAIL_01);
+ const result = await fetchMetrics(integration?.id, '2023-01-20', '9999-12-01', 'dev');
+ expect(result.status).toEqual(400);
+ });
+
+ it('should reject if env input is not valid', async () => {
+ createMockAuth(TEAM_ADMIN_IDIR_USERID_01, TEAM_ADMIN_IDIR_EMAIL_01);
+ const result = await fetchMetrics(integration?.id, '2023-01-01', '2023-01-10', 'fake');
+ expect(result.status).toEqual(400);
+ });
+
+ it('should fetch metrics if all the inputs are valid', async () => {
+ createMockAuth(TEAM_ADMIN_IDIR_USERID_01, TEAM_ADMIN_IDIR_EMAIL_01);
+ const result = await fetchMetrics(integration?.id, '2023-01-01', '2023-01-10', 'dev');
+ expect(result.status).toEqual(200);
+ expect(result.body.length).toEqual(1);
+ });
+ } catch (err) {
+ console.error('EXCEPTION: ', err);
+ }
+});
diff --git a/lambda/__tests__/helpers/fixtures.ts b/lambda/__tests__/helpers/fixtures.ts
index 2a3617244..ca96c702d 100644
--- a/lambda/__tests__/helpers/fixtures.ts
+++ b/lambda/__tests__/helpers/fixtures.ts
@@ -128,8 +128,8 @@ export const getUpdateIntegrationData = (args: {
}) => {
const {
projectName = args.integration.projectName,
- envs = args.integration.environments.length > 1 ? args.integration.environments : ['dev'],
- identityProviders = args.integration.devIdps.length > 1 ? args.integration.devIdps : ['idir'],
+ envs = args.integration.environments?.length > 1 ? args.integration.environments : ['dev'],
+ identityProviders = args.integration.devIdps?.length > 1 ? args.integration.devIdps : ['idir'],
protocol = args.integration.protocol || 'oidc',
authType = args.integration.authType || 'browser-login',
publicAccess = args.integration.publicAccess || true,
diff --git a/lambda/__tests__/helpers/modules/integrations.ts b/lambda/__tests__/helpers/modules/integrations.ts
index 1b1da4fd1..628d086aa 100644
--- a/lambda/__tests__/helpers/modules/integrations.ts
+++ b/lambda/__tests__/helpers/modules/integrations.ts
@@ -52,3 +52,9 @@ export const createCompositeRoles = async (data: {
export const deleteIntegration = async (integrationId: number) => {
return await supertest(app).del(`${APP_BASE_PATH}/requests?id=${integrationId}`);
};
+
+export const fetchMetrics = async (integrationId: number, fromDate: string, toDate: string, env: string) => {
+ return await supertest(app).get(
+ `${APP_BASE_PATH}/requests/${integrationId}/metrics?fromDate=${fromDate}&toDate=${toDate}&env=${env}`,
+ );
+};
diff --git a/lambda/app/src/controllers/logs.ts b/lambda/app/src/controllers/logs.ts
new file mode 100644
index 000000000..7155170a1
--- /dev/null
+++ b/lambda/app/src/controllers/logs.ts
@@ -0,0 +1,99 @@
+import { getAllowedRequest } from '@lambda-app/queries/request';
+import { Session } from '@lambda-shared/interfaces';
+import { clientEventsAggregationQuery, queryGrafana } from '@lambda-app/grafana';
+import axios from 'axios';
+
+const LOG_SIZE_LIMIT = 2000;
+
+const allowedEnvs = ['dev', 'test', 'prod'];
+
+export const fetchLogs = async (
+ session: Session,
+ env: string,
+ id: number,
+ start: string,
+ end: string,
+ eventType: string,
+) => {
+ // Check user owns requested logs
+ const userRequest = await getAllowedRequest(session, id);
+ if (!userRequest) return { status: 401, message: "You are not authorized to view this integration's logs" };
+
+ const { clientId } = userRequest;
+ // Validate user supplied inputs
+ const hasRequiredQueryParams = start && end && env && eventType;
+
+ const allowedEvents = ['LOGIN', 'CODE_TO_TOKEN', 'REFRESH_TOKEN'];
+
+ if (!hasRequiredQueryParams) {
+ return { status: 400, message: 'Not all query params sent. Please include start, end, env, and eventType.' };
+ }
+ if (!allowedEnvs.includes(env)) {
+ return { status: 400, message: `The env query param must be one of ${allowedEnvs.join(', ')}.` };
+ }
+ if (!allowedEvents.includes(eventType)) {
+ return { status: 400, message: "The eventType query param must be one of ${allowedEvents.join(', ')}" };
+ }
+
+ const unixStartTime = new Date(start).getTime();
+ const unixEndTime = new Date(end).getTime();
+
+ const validTime = (time) => !Number.isNaN(time) && time > 0;
+
+ if (!validTime(unixStartTime) || !validTime(unixEndTime)) {
+ return { status: 400, message: 'Include parsable dates for the start and end parameters, e.g YYYY-MM-DD.' };
+ }
+
+ try {
+ const query = `{environment="${env}", client_id="${clientId}", event_type="${eventType}"}`;
+ const allLogs = await queryGrafana(query, unixStartTime, unixEndTime, LOG_SIZE_LIMIT);
+ let message = 'All logs retrieved';
+ if (allLogs.length === LOG_SIZE_LIMIT)
+ message = 'Log limit reached. There may be more logs available, try a more restricted date range.';
+ return { status: 200, message, data: allLogs };
+ } catch (err) {
+ console.info(`Error while fetching logs from loki: ${err}`);
+ return { status: 500, message: 'Unexpected error fetching logs.' };
+ }
+};
+
+export const fetchMetrics = async (
+ session: Session,
+ id: number,
+ environment: string,
+ fromDate: string,
+ toDate: string,
+) => {
+ try {
+ let result = [];
+ // Check user owns requested logs
+ const userRequest = await getAllowedRequest(session, id);
+ if (!userRequest) return { status: 401, message: "You are not authorized to view this integration's metrics" };
+
+ const { clientId } = userRequest;
+
+ if (!allowedEnvs.includes(environment)) {
+ return { status: 400, message: `The env query param must be one of ${allowedEnvs.join(', ')}.` };
+ }
+
+ const startDate = Date.parse(fromDate);
+ const endDate = Date.parse(toDate);
+
+ if (isNaN(startDate) || isNaN(endDate)) {
+ return { status: 400, message: 'Include parsable dates for the start and end dates, e.g YYYY-MM-DD.' };
+ }
+
+ if (startDate > endDate) {
+ return { status: 400, message: 'Start date cannot be greater than end date.' };
+ } else if (endDate > new Date().getTime()) {
+ return { status: 400, message: 'End date cannot be a future date.' };
+ }
+
+ result = await clientEventsAggregationQuery(clientId, environment, fromDate, toDate);
+
+ return { status: 200, message: null, data: result };
+ } catch (err) {
+ console.error(err);
+ return { status: 500, message: 'Unable to fetch metrics at this moment!', data: null };
+ }
+};
diff --git a/lambda/app/src/grafana.ts b/lambda/app/src/grafana.ts
new file mode 100644
index 000000000..c2e9ff32a
--- /dev/null
+++ b/lambda/app/src/grafana.ts
@@ -0,0 +1,86 @@
+import axios from 'axios';
+
+export const fetchDatasourceUID = async (datasourceName: string) => {
+ return axios
+ .get(`${process.env.GRAFANA_API_URL}/datasources`, {
+ headers: {
+ Authorization: `Bearer ${process.env.GRAFANA_API_TOKEN}`,
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ })
+ .then((res) => res.data.find((source) => source.name === datasourceName)?.uid);
+};
+
+export const queryGrafana = async (query: string, start: number, end: number, limit: number) => {
+ const lokiUID = await fetchDatasourceUID('SSO Loki');
+ const response = await axios.post(
+ `${process.env.GRAFANA_API_URL}/ds/query`,
+ {
+ queries: [
+ {
+ refId: 'logs',
+ expr: query,
+ queryType: 'range',
+ datasource: {
+ type: 'loki',
+ uid: lokiUID,
+ },
+ maxLines: limit,
+ },
+ ],
+ from: String(start),
+ to: String(end),
+ },
+ {
+ params: {
+ ds_type: 'loki',
+ },
+ headers: {
+ Authorization: `Bearer ${process.env.GRAFANA_API_TOKEN}`,
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ return response.data.results.logs.frames.reduce((acc, curr) => {
+ // Raw logs are in third array position. First entries are filenames and IDs
+ return acc.concat(curr.data.values[2]);
+ }, []);
+};
+
+export const clientEventsAggregationQuery = async (
+ clientId: string,
+ environment: string,
+ fromDate: string,
+ toDate: string,
+) => {
+ let result = [];
+
+ const aggregatorUID = await fetchDatasourceUID('SSO Aggregator');
+
+ const query = {
+ queries: [
+ {
+ datasource: { type: 'postgres', uid: aggregatorUID },
+ rawSql: `select json_build_object('event', event_type, 'count', count) from (select distinct event_type, SUM(\"count\") OVER (PARTITION BY \"event_type\") as count from client_events where client_id = '${clientId}' and environment = '${environment}' and date(date) >= '${fromDate}' and date(date) <= '${toDate}') client_event_data;`,
+ format: 'table',
+ },
+ ],
+ };
+
+ const headers = {
+ Authorization: `Bearer ${process.env.GRAFANA_API_TOKEN}`,
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ };
+ const res: any = await axios.post(`${process.env.GRAFANA_API_URL}/ds/query`, query, { headers });
+
+ const values = res?.data?.results?.A?.frames[0]?.data?.values;
+
+ if (values.length > 0) {
+ result = values[0].map((item) => JSON.parse(item));
+ }
+
+ return result;
+};
diff --git a/lambda/app/src/routes.ts b/lambda/app/src/routes.ts
index 2a6e08d51..1e6602fe4 100644
--- a/lambda/app/src/routes.ts
+++ b/lambda/app/src/routes.ts
@@ -64,6 +64,7 @@ import { assertSessionRole } from './helpers/permissions';
import { fetchDiscussions } from './graphql';
import { sendTemplate } from '@lambda-shared/templates';
import { EMAILS } from '@lambda-shared/enums';
+import { fetchLogs, fetchMetrics } from '@lambda-app/controllers/logs';
const APP_URL = process.env.APP_URL || '';
@@ -270,6 +271,31 @@ export const setRoutes = (app: any) => {
}
});
+ app.get(`/requests/:id/logs`, async (req, res) => {
+ try {
+ const { id } = req.params || {};
+ const { start, end, env, eventType } = req.query || {};
+ const { status, message, data } = await fetchLogs(req.session, env, id, start, end, eventType);
+
+ if (status === 200) res.status(status).send({ message, data });
+ else res.status(status).send({ message });
+ } catch (err) {
+ handleError(res, err);
+ }
+ });
+
+ app.get(`/requests/:id/metrics`, async (req, res) => {
+ try {
+ const { id } = req.params || {};
+ const { fromDate, toDate, env } = req.query || {};
+ const { status, message, data } = await fetchMetrics(req.session, id, env, fromDate, toDate);
+ if (status === 200) res.status(status).send(data);
+ else res.status(status).send({ message });
+ } catch (err) {
+ handleError(res, err);
+ }
+ });
+
app.delete(`/requests`, async (req, res) => {
try {
const { id } = req.query || {};
diff --git a/localserver/.env.example b/localserver/.env.example
index 329d3ce3e..9d9a669a9 100644
--- a/localserver/.env.example
+++ b/localserver/.env.example
@@ -26,3 +26,5 @@ CHES_PASSWORD=
VERIFY_USER_SECRET=asdf
API_URL=http://localhost:8080
REALM_REGISTRY_API=http://localhost:3000/api
+GRAFANA_API_TOKEN=
+GRAFANA_API_URL=
diff --git a/terraform/lambda-app.tf b/terraform/lambda-app.tf
index 956903ba2..66d37ffc8 100644
--- a/terraform/lambda-app.tf
+++ b/terraform/lambda-app.tf
@@ -51,6 +51,8 @@ resource "aws_lambda_function" "app" {
CHES_PASSWORD = var.ches_password
CHES_USERNAME = var.ches_username
INCLUDE_DIGITAL_CREDENTIAL = var.include_digital_credential
+ GRAFANA_API_TOKEN = var.grafana_api_token
+ GRAFANA_API_URL = var.grafana_api_url
}
}
diff --git a/terraform/variables.tf b/terraform/variables.tf
index 806e8cb49..d9e3c5f70 100644
--- a/terraform/variables.tf
+++ b/terraform/variables.tf
@@ -28,6 +28,16 @@ variable "include_digital_credential" {
default = "false"
}
+variable "grafana_api_token" {
+ description = "API token for the grafana service account."
+ type = string
+}
+
+variable "grafana_api_url" {
+ description = "Base url to call the grafana api"
+ type = string
+}
+
variable "sso_client_id" {
description = "The required audience for authentication"
type = string