From c17d6b4d523210075c8c7bd556a24d65b32e0a19 Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Tue, 24 Dec 2024 00:53:03 -0800 Subject: [PATCH] Add daily applications summary charts for admins - Upgrade package for Cloudscape components - Add summary charts to the Applicants page on the Admin site - Show daily applications grouped by school as a stacked bar chart - Show daily cumulative applications by role as a stacked area chart - Add `Hacker` to `ParticipantRole` enum --- apps/api/src/admin/summary_handler.py | 91 +++++++++++- apps/api/src/routers/admin.py | 23 ++- apps/api/tests/test_summary_handler.py | 83 ++++++++++- apps/site/package.json | 6 +- .../(summary)/ApplicantsSummary.tsx | 47 ++++++ .../(summary)/ApplicationsByRoleChart.tsx | 88 ++++++++++++ .../(summary)/ApplicationsBySchoolChart.tsx | 84 +++++++++++ .../admin/applicants/{ => (summary)}/page.tsx | 0 .../(summary)/useApplicationsSummary.ts | 24 ++++ .../admin/applicants/ApplicantsSummary.tsx | 3 - apps/site/src/lib/userRecord.ts | 1 + pnpm-lock.yaml | 135 ++++++++---------- 12 files changed, 496 insertions(+), 89 deletions(-) create mode 100644 apps/site/src/app/admin/applicants/(summary)/ApplicantsSummary.tsx create mode 100644 apps/site/src/app/admin/applicants/(summary)/ApplicationsByRoleChart.tsx create mode 100644 apps/site/src/app/admin/applicants/(summary)/ApplicationsBySchoolChart.tsx rename apps/site/src/app/admin/applicants/{ => (summary)}/page.tsx (100%) create mode 100644 apps/site/src/app/admin/applicants/(summary)/useApplicationsSummary.ts delete mode 100644 apps/site/src/app/admin/applicants/ApplicantsSummary.tsx diff --git a/apps/api/src/admin/summary_handler.py b/apps/api/src/admin/summary_handler.py index 4f4f9ce0..d5fe0469 100644 --- a/apps/api/src/admin/summary_handler.py +++ b/apps/api/src/admin/summary_handler.py @@ -1,11 +1,16 @@ -from collections import Counter +from collections import Counter, defaultdict +from datetime import date, datetime +from typing import Iterable +from zoneinfo import ZoneInfo -from pydantic import BaseModel, TypeAdapter +from pydantic import BaseModel, TypeAdapter, ValidationError from models.user_record import ApplicantStatus, Role from services import mongodb_handler from services.mongodb_handler import Collection +LOCAL_TIMEZONE = ZoneInfo("America/Los_Angeles") + class ApplicantSummaryRecord(BaseModel): status: ApplicantStatus @@ -21,3 +26,85 @@ async def applicant_summary() -> Counter[ApplicantStatus]: applicants = TypeAdapter(list[ApplicantSummaryRecord]).validate_python(records) return Counter(applicant.status for applicant in applicants) + + +class ApplicationSubmissionTime(BaseModel): + submission_time: datetime + + +class ApplicationSchoolAndTime(ApplicationSubmissionTime): + school: str + + +class ApplicantSchoolStats(BaseModel): + application_data: ApplicationSchoolAndTime + + +async def applications_by_school() -> dict[str, dict[date, int]]: + """Get daily number of applications by school.""" + records = await mongodb_handler.retrieve( + Collection.USERS, + {"roles": Role.APPLICANT}, + ["application_data.school", "application_data.submission_time"], + ) + + try: + applicant_stats_adapter = TypeAdapter(list[ApplicantSchoolStats]) + applicants = applicant_stats_adapter.validate_python(records) + except ValidationError: + raise RuntimeError("Could not parse applicant data.") + + grouped_applications: dict[str, dict[date, int]] = defaultdict( + lambda: defaultdict(int) + ) + + for applicant in applicants: + school = applicant.application_data.school + day = applicant.application_data.submission_time.astimezone( + LOCAL_TIMEZONE + ).date() + grouped_applications[school][day] += 1 + + return grouped_applications + + +class ApplicantRoleStats(BaseModel): + roles: tuple[Role, ...] + application_data: ApplicationSubmissionTime + + +async def applications_by_role() -> dict[str, dict[date, int]]: + """Get daily number of applications by role.""" + records: list[dict[str, object]] = await mongodb_handler.retrieve( + Collection.USERS, + {"roles": Role.APPLICANT}, + ["roles", "application_data.submission_time"], + ) + + try: + applicant_stats_adapter = TypeAdapter(list[ApplicantRoleStats]) + applicants = applicant_stats_adapter.validate_python(records) + except ValidationError: + raise RuntimeError("Could not parse applicant data.") + + return { + role.value: _count_applications_by_day( + applicant.application_data + for applicant in applicants + if role in applicant.roles + ) + for role in [Role.HACKER, Role.MENTOR, Role.VOLUNTEER] + } + + +def _count_applications_by_day( + application_data: Iterable[ApplicationSubmissionTime], +) -> dict[date, int]: + """Group the applications by the date of submission.""" + daily_applications = defaultdict[date, int](int) + + for data in application_data: + day = data.submission_time.astimezone(LOCAL_TIMEZONE).date() + daily_applications[day] += 1 + + return daily_applications diff --git a/apps/api/src/routers/admin.py b/apps/api/src/routers/admin.py index 239b38eb..0c25ab13 100644 --- a/apps/api/src/routers/admin.py +++ b/apps/api/src/routers/admin.py @@ -1,13 +1,13 @@ import asyncio -from datetime import datetime +from datetime import date, datetime from logging import getLogger -from typing import Annotated, Any, Optional, Sequence +from typing import Annotated, Any, Literal, Optional, Sequence from fastapi import APIRouter, Body, Depends, HTTPException, status from pydantic import BaseModel, EmailStr, TypeAdapter, ValidationError +from typing_extensions import assert_never -from admin import participant_manager, summary_handler -from admin import applicant_review_processor +from admin import applicant_review_processor, participant_manager, summary_handler from admin.participant_manager import Participant from auth.authorization import require_role from auth.user_identity import User, utc_now @@ -147,6 +147,21 @@ async def applicant_summary() -> dict[ApplicantStatus, int]: return await summary_handler.applicant_summary() +@router.get( + "/summary/applications", + response_model=dict[str, object], + dependencies=[Depends(require_manager)], +) +async def applications( + group_by: Literal["school", "role"] +) -> dict[str, dict[date, int]]: + if group_by == "school": + return await summary_handler.applications_by_school() + elif group_by == "role": + return await summary_handler.applications_by_role() + assert_never(group_by) + + @router.post("/review") async def submit_review( applicant: str = Body(), diff --git a/apps/api/tests/test_summary_handler.py b/apps/api/tests/test_summary_handler.py index c1066e71..9df107b3 100644 --- a/apps/api/tests/test_summary_handler.py +++ b/apps/api/tests/test_summary_handler.py @@ -1,6 +1,7 @@ +from datetime import date, datetime, timezone from unittest.mock import AsyncMock, patch -from admin.summary_handler import applicant_summary +from admin import summary_handler @patch("services.mongodb_handler.retrieve", autospec=True) @@ -12,7 +13,8 @@ async def test_applicant_summary(mock_mongodb_handler_retrieve: AsyncMock) -> No + [{"status": "WAITLISTED"}, {"status": "WAIVER_SIGNED"}] * 3 ) - summary = await applicant_summary() + summary = await summary_handler.applicant_summary() + mock_mongodb_handler_retrieve.assert_awaited_once() assert dict(summary) == { "REJECTED": 20, @@ -21,3 +23,80 @@ async def test_applicant_summary(mock_mongodb_handler_retrieve: AsyncMock) -> No "WAIVER_SIGNED": 3, "CONFIRMED": 24, } + + +@patch("services.mongodb_handler.retrieve", autospec=True) +async def test_applications_by_school(mock_mongodb_handler_retrieve: AsyncMock) -> None: + """Daily number of applications are grouped by school.""" + mock_mongodb_handler_retrieve.return_value = [ + { + "application_data": { + "school": "UC Irvine", + "submission_time": datetime(1965, 10, 4, 20, 2, 4, tzinfo=timezone.utc), + }, + }, + { + "application_data": { + "school": "UC Irvine", + "submission_time": datetime( + 1965, 10, 4, 20, 15, 26, tzinfo=timezone.utc + ), + }, + }, + { + "application_data": { + "school": "Cal State Long Beach", + "submission_time": datetime( + 2024, 12, 17, 18, 4, 11, tzinfo=timezone.utc + ), + }, + }, + ] + + applications = await summary_handler.applications_by_school() + + mock_mongodb_handler_retrieve.assert_awaited_once() + assert applications == { + "UC Irvine": {date(1965, 10, 4): 2}, + "Cal State Long Beach": {date(2024, 12, 17): 1}, + } + + +@patch("services.mongodb_handler.retrieve", autospec=True) +async def test_applications_by_role(mock_mongodb_handler_retrieve: AsyncMock) -> None: + """Daily number of applications are grouped by role.""" + mock_mongodb_handler_retrieve.return_value = [ + { + "roles": ["Applicant", "Hacker"], + "application_data": { + "submission_time": datetime( + 2024, 12, 12, 17, 0, 0, tzinfo=timezone.utc + ), + }, + }, + { + "roles": ["Applicant", "Hacker"], + "application_data": { + "submission_time": datetime( + 2024, 12, 12, 19, 0, 0, tzinfo=timezone.utc + ), + }, + }, + { + "roles": ["Applicant", "Mentor"], + "application_data": { + "submission_time": datetime( + 2024, 12, 14, 18, 0, 0, tzinfo=timezone.utc + ), + }, + }, + ] + + applications = await summary_handler.applications_by_role() + + mock_mongodb_handler_retrieve.assert_awaited_once() + assert applications == { + "Hacker": {date(2024, 12, 12): 2}, + "Mentor": {date(2024, 12, 14): 1}, + "Volunteer": {}, + } diff --git a/apps/site/package.json b/apps/site/package.json index 1a0f8b53..5e877b69 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -9,9 +9,9 @@ "lint": "next lint" }, "dependencies": { - "@cloudscape-design/collection-hooks": "^1.0.34", - "@cloudscape-design/components": "^3.0.475", - "@cloudscape-design/global-styles": "^1.0.20", + "@cloudscape-design/collection-hooks": "^1.0.56", + "@cloudscape-design/components": "^3.0.856", + "@cloudscape-design/global-styles": "^1.0.33", "@fireworks-js/react": "^2.10.7", "@portabletext/react": "^3.0.11", "@radix-ui/react-accordion": "^1.1.2", diff --git a/apps/site/src/app/admin/applicants/(summary)/ApplicantsSummary.tsx b/apps/site/src/app/admin/applicants/(summary)/ApplicantsSummary.tsx new file mode 100644 index 00000000..3bff6772 --- /dev/null +++ b/apps/site/src/app/admin/applicants/(summary)/ApplicantsSummary.tsx @@ -0,0 +1,47 @@ +"use client"; + +import Container from "@cloudscape-design/components/container"; +import ContentLayout from "@cloudscape-design/components/content-layout"; +import Header from "@cloudscape-design/components/header"; +import SpaceBetween from "@cloudscape-design/components/space-between"; + +import ApplicationsByRoleChart from "./ApplicationsByRoleChart"; +import ApplicationsBySchoolChart from "./ApplicationsBySchoolChart"; + +function ApplicantsSummary() { + return ( + Applicants} + > + + + Applications Submitted + + } + > + + + + Cumulative Applications Submitted + + } + > + + + + + ); +} + +export default ApplicantsSummary; diff --git a/apps/site/src/app/admin/applicants/(summary)/ApplicationsByRoleChart.tsx b/apps/site/src/app/admin/applicants/(summary)/ApplicationsByRoleChart.tsx new file mode 100644 index 00000000..d2e69c15 --- /dev/null +++ b/apps/site/src/app/admin/applicants/(summary)/ApplicationsByRoleChart.tsx @@ -0,0 +1,88 @@ +import AreaChart from "@cloudscape-design/components/area-chart"; +import Box from "@cloudscape-design/components/box"; + +import { ParticipantRole } from "@/lib/userRecord"; + +import useApplicationsSummary from "./useApplicationsSummary"; + +const TIME_SPEC = "T00:00:00-08:00"; +const START_DAY = new Date("2024-12-10" + TIME_SPEC); +const END_DAY = new Date("2025-01-11" + TIME_SPEC); + +const ROLES = [ + ParticipantRole.Hacker, + ParticipantRole.Mentor, + ParticipantRole.Volunteer, +] as const; + +function ApplicationsByRoleChart() { + const { loading, applications, error } = useApplicationsSummary("role"); + + const today = new Date(); + const end = today < END_DAY ? today : END_DAY; + + const cumulativeApplications = Object.fromEntries( + ROLES.map((role) => [ + role, + getCumulativeApplications(applications[role] ?? {}, end), + ]), + ); + + return ( + ({ + title: role, + type: "area", + data: cumulativeApplications[role].map(([d, count]) => { + return { x: d, y: count }; + }), + }))} + xDomain={[START_DAY, end]} + i18nStrings={{ + filterLabel: "Filter displayed data", + filterPlaceholder: "Filter data", + filterSelectedAriaLabel: "selected", + xTickFormatter: (e) => + e.toLocaleDateString("en-US", { month: "short", day: "numeric" }), + }} + ariaLabel="Stacked area chart." + errorText="Error loading data." + statusType={loading ? "loading" : error ? "error" : "finished"} + fitHeight + height={300} + loadingText="Loading chart" + xScaleType="time" + xTitle="Date (Pacific Time)" + yTitle="Cumulative total applications submitted" + empty={ + + No data available + + There is no data available + + + } + /> + ); +} + +function getCumulativeApplications( + applications: Record, + end: Date, +): [Date, number][] { + const cumulativeApplications: [Date, number][] = []; + + let prev = 0; + for (let d = new Date(START_DAY); d <= end; d.setDate(d.getDate() + 1)) { + cumulativeApplications.push([ + new Date(d), + // Index as YYYY-MM-DD and accumulate with previous value + prev + (applications[d.toISOString().substring(0, 10)] ?? 0), + ]); + prev = cumulativeApplications.at(-1)![1]; + } + + return cumulativeApplications; +} + +export default ApplicationsByRoleChart; diff --git a/apps/site/src/app/admin/applicants/(summary)/ApplicationsBySchoolChart.tsx b/apps/site/src/app/admin/applicants/(summary)/ApplicationsBySchoolChart.tsx new file mode 100644 index 00000000..f93d318e --- /dev/null +++ b/apps/site/src/app/admin/applicants/(summary)/ApplicationsBySchoolChart.tsx @@ -0,0 +1,84 @@ +import BarChart from "@cloudscape-design/components/bar-chart"; +import Box from "@cloudscape-design/components/box"; + +import useApplicationsSummary from "./useApplicationsSummary"; + +const TIME_SPEC = "T00:00:00-08:00"; +const START_DAY = new Date("2024-12-11" + TIME_SPEC); +const END_DAY = new Date("2025-01-11" + TIME_SPEC); + +// In reverse of desired order +const KNOWN_SCHOOLS = [ + "UC San Diego", + "UCLA", + "UC Riverside", + "Cal State Fullerton", + "Cal State Long Beach", + "Orange Coast College", + "UC Irvine", +]; + +function ApplicationsBySchoolChart() { + const { loading, applications, error } = useApplicationsSummary("school"); + + const sortedBySchool = Object.entries(applications).sort((a, b) => { + const schoolA = a[0]; + const schoolB = b[0]; + if (KNOWN_SCHOOLS.includes(schoolA) || KNOWN_SCHOOLS.includes(schoolB)) { + return KNOWN_SCHOOLS.indexOf(schoolB) - KNOWN_SCHOOLS.indexOf(schoolA); + } + if (schoolA < schoolB) { + return -1; + } + if (schoolB > schoolA) { + return 1; + } + return 0; + }); + + const today = new Date(); + const end = today < END_DAY ? today : END_DAY; + + const xDomain = []; + for (let d = new Date(START_DAY); d <= end; d.setDate(d.getDate() + 1)) { + xDomain.push(new Date(d)); + } + + return ( + ({ + title: school, + type: "bar", + data: Object.entries(events).map(([d, count]) => { + return { x: new Date(d + TIME_SPEC), y: count }; + }), + }))} + xDomain={xDomain} + i18nStrings={{ + xTickFormatter: (e) => + e.toLocaleDateString("en-US", { month: "short", day: "numeric" }), + }} + ariaLabel="Stacked bar chart." + errorText="Error loading data." + statusType={loading ? "loading" : error ? "error" : "finished"} + fitHeight + height={300} + loadingText="Loading chart" + stackedBars + hideFilter + xScaleType="categorical" + xTitle="Date (Pacific Time)" + yTitle="Total applications submitted" + empty={ + + No data available + + There is no data available + + + } + /> + ); +} + +export default ApplicationsBySchoolChart; diff --git a/apps/site/src/app/admin/applicants/page.tsx b/apps/site/src/app/admin/applicants/(summary)/page.tsx similarity index 100% rename from apps/site/src/app/admin/applicants/page.tsx rename to apps/site/src/app/admin/applicants/(summary)/page.tsx diff --git a/apps/site/src/app/admin/applicants/(summary)/useApplicationsSummary.ts b/apps/site/src/app/admin/applicants/(summary)/useApplicationsSummary.ts new file mode 100644 index 00000000..8295749f --- /dev/null +++ b/apps/site/src/app/admin/applicants/(summary)/useApplicationsSummary.ts @@ -0,0 +1,24 @@ +import axios from "axios"; +import useSWR from "swr"; + +interface ApplicationStats { + [key: string]: { + [key: string]: number; + }; +} + +const fetcher = async (url: string) => { + const res = await axios.get(url); + return res.data; +}; + +function useApplicationsSummary(groupBy: "school" | "role") { + const { data, error, isLoading } = useSWR( + `/api/admin/summary/applications?group_by=${groupBy}`, + fetcher, + ); + + return { applications: data ?? {}, loading: isLoading, error }; +} + +export default useApplicationsSummary; diff --git a/apps/site/src/app/admin/applicants/ApplicantsSummary.tsx b/apps/site/src/app/admin/applicants/ApplicantsSummary.tsx deleted file mode 100644 index eff5f099..00000000 --- a/apps/site/src/app/admin/applicants/ApplicantsSummary.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function ApplicantsSummary() { - return

PLACEHOLDER: Various summary charts

; -} diff --git a/apps/site/src/lib/userRecord.ts b/apps/site/src/lib/userRecord.ts index bd1420ca..af01bf9a 100644 --- a/apps/site/src/lib/userRecord.ts +++ b/apps/site/src/lib/userRecord.ts @@ -6,6 +6,7 @@ export type Uid = string; /** The possible roles of general participants. */ export enum ParticipantRole { Applicant = "Applicant", + Hacker = "Hacker", Mentor = "Mentor", Volunteer = "Volunteer", Sponsor = "Sponsor", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df5e5c42..074991aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,3 @@ - lockfileVersion: '9.0' settings: @@ -86,14 +85,14 @@ importers: apps/site: dependencies: '@cloudscape-design/collection-hooks': - specifier: ^1.0.34 - version: 1.0.34(react@18.2.0) + specifier: ^1.0.56 + version: 1.0.56(react@18.2.0) '@cloudscape-design/components': - specifier: ^3.0.475 - version: 3.0.475(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: ^3.0.856 + version: 3.0.856(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@cloudscape-design/global-styles': - specifier: ^1.0.20 - version: 1.0.20 + specifier: ^1.0.33 + version: 1.0.33 '@fireworks-js/react': specifier: ^2.10.7 version: 2.10.7(@types/react@18.2.38)(react@18.2.0) @@ -879,28 +878,28 @@ packages: resolution: {integrity: sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==} engines: {node: '>=6.9.0'} - '@cloudscape-design/collection-hooks@1.0.34': - resolution: {integrity: sha512-8ggOJRX4PpiT6YCv7cHSjyx27VUgJX7s25KiLaUbVvm5EeWBpKxrwDwE6w9JjNxrkXeepjs4xyNjaNie2ZiCeQ==} + '@cloudscape-design/collection-hooks@1.0.56': + resolution: {integrity: sha512-1nDayJZTXMwb/MDcPzmfr12t423V+leKQI+apA0rb5j19SJhqz9AMUYF9QWBGmHsTV2FlKZI6yghbZBkVWDL6Q==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 - '@cloudscape-design/component-toolkit@1.0.0-beta.30': - resolution: {integrity: sha512-RFbLd8YXNz2QFv2Cb0lN7k8USCjE4lgVjSk5Kn6PZn77dtZrh7WGT313qEeOAB1IH40A/ZhtKYt265fC9Q5f/Q==} + '@cloudscape-design/component-toolkit@1.0.0-beta.81': + resolution: {integrity: sha512-//WS5C+DSi6vbD17gdyUO9hikFWjID8FXSmzqtAxV6FsO2HRiSJkAMLt6jRCMZKo9JpOnDaEhtx5pkuiY/Ez+g==} - '@cloudscape-design/components@3.0.475': - resolution: {integrity: sha512-sn7xCQpufgYafK+9RUxNlzoThGoj65b5RzIgKE3VZVNvGfaUmrAXu7p6Vhs4KcVckXkwaE8FqP3IUsGyrnX1RQ==} + '@cloudscape-design/components@3.0.856': + resolution: {integrity: sha512-e0dK7mibvvdsJOppuCCe29XJzsYcTCDqiuf8bXCafDgaomsgTahPA8TXj3aQyC3rZwEjuLbFj0Uil407mfALIw==} peerDependencies: react: ^16.8 || ^17 || ^18 react-dom: ^16.8 || ^17 || ^18 - '@cloudscape-design/global-styles@1.0.20': - resolution: {integrity: sha512-eEU3o7fZSRtIQVcFj1vRtheDFktRdAMMIidihrpNHqWVZzazFe0Z22vll684Yzy9TQ5R0HXKKupZLgKyC+Ia8w==} + '@cloudscape-design/global-styles@1.0.33': + resolution: {integrity: sha512-6bg18XIxkRS2ojMNGxVA8mV35rqkiHDXwOJjfHhYPzg6LjFagZWyg/hRRGuP5MExszB748m2HYYdXT0EejxiPA==} - '@cloudscape-design/test-utils-core@1.0.21': - resolution: {integrity: sha512-Kjxtl1ImQLmJ5SJ3PNF0hrtEbKidcZqk3E+iY4dLbJOI3sWh2TwoEpH5i4QEfO2GMtkMNzi41V4mmI4e4sydAw==} + '@cloudscape-design/test-utils-core@1.0.47': + resolution: {integrity: sha512-x8D+t/H8LXhEtWHrzLvBoCWPiwBb7uJQGSYeqC+J6WKB0MmDpcEJXPkxJ2n0h1HBAC0n9wgkO64MDzRVtAf1fw==} - '@cloudscape-design/theming-runtime@1.0.39': - resolution: {integrity: sha512-bc3ATkJhQ2PVomnLtBINcT3szYk6daPzBDCM+rwR9RZQ1a7T611R+EsPjyoKm355Z4xn/3hWKtw+IArg6vn0+A==} + '@cloudscape-design/theming-runtime@1.0.66': + resolution: {integrity: sha512-EJWRxL8ELd/5TPp4wmIMcQIWLrSBtBqmp4whtmq9P4fdMXcq46lJ/hHFSTxQPh21CAFlsJ+Hv5HMTktIxqvsXQ==} '@codemirror/autocomplete@6.11.0': resolution: {integrity: sha512-LCPH3W+hl5vcO7OzEQgX6NpKuKVyiKFLGAy7FXROF6nUpsWUdQEgUb3fe/g7B0E1KZCRFfgzdKASt6Wly2UOBg==} @@ -1337,20 +1336,20 @@ packages: '@floating-ui/utils@0.1.6': resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==} - '@formatjs/ecma402-abstract@1.18.0': - resolution: {integrity: sha512-PEVLoa3zBevWSCZzPIM/lvPCi8P5l4G+NXQMc/CjEiaCWgyHieUoo0nM7Bs0n/NbuQ6JpXEolivQ9pKSBHaDlA==} + '@formatjs/ecma402-abstract@2.3.1': + resolution: {integrity: sha512-Ip9uV+/MpLXWRk03U/GzeJMuPeOXpJBSB5V1tjA6kJhvqssye5J5LoYLc7Z5IAHb7nR62sRoguzrFiVCP/hnzw==} - '@formatjs/fast-memoize@2.2.0': - resolution: {integrity: sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==} + '@formatjs/fast-memoize@2.2.5': + resolution: {integrity: sha512-6PoewUMrrcqxSoBXAOJDiW1m+AmkrAj0RiXnOMD59GRaswjXhm3MDhgepXPBgonc09oSirAJTsAggzAGQf6A6g==} - '@formatjs/icu-messageformat-parser@2.7.3': - resolution: {integrity: sha512-X/jy10V9S/vW+qlplqhMUxR8wErQ0mmIYSq4mrjpjDl9mbuGcCILcI1SUYkL5nlM4PJqpc0KOS0bFkkJNPxYRw==} + '@formatjs/icu-messageformat-parser@2.9.7': + resolution: {integrity: sha512-cuEHyRM5VqLQobANOjtjlgU7+qmk9Q3fDQuBiRRJ3+Wp3ZoZhpUPtUfuimZXsir6SaI2TaAJ+SLo9vLnV5QcbA==} - '@formatjs/icu-skeleton-parser@1.7.0': - resolution: {integrity: sha512-Cfdo/fgbZzpN/jlN/ptQVe0lRHora+8ezrEeg2RfrNjyp+YStwBy7cqDY8k5/z2LzXg6O0AdzAV91XS0zIWv+A==} + '@formatjs/icu-skeleton-parser@1.8.11': + resolution: {integrity: sha512-8LlHHE/yL/zVJZHAX3pbKaCjZKmBIO6aJY1mkVh4RMSEu/2WRZ4Ysvv3kKXJ9M8RJLBHdnk1/dUQFdod1Dt7Dw==} - '@formatjs/intl-localematcher@0.5.2': - resolution: {integrity: sha512-txaaE2fiBMagLrR4jYhxzFO6wEdEG4TPMqrzBAcbr4HFUYzH/YC+lg6OIzKCHm8WgDdyQevxbAAV1OgcXctuGw==} + '@formatjs/intl-localematcher@0.5.9': + resolution: {integrity: sha512-8zkGu/sv5euxbjfZ/xmklqLyDGQSxsLqg8XOq88JW3cmJtzhCP8EtSJXlaKZnVO4beEaoiT9wj4eIoCQ9smwxA==} '@hookform/resolvers@3.3.2': resolution: {integrity: sha512-Tw+GGPnBp+5DOsSg4ek3LCPgkBOuOgS5DsDV7qsWNH9LZc433kgsWICjlsh2J9p04H2K66hsXPPb9qn9ILdUtA==} @@ -1857,9 +1856,6 @@ packages: '@radix-ui/rect@1.0.1': resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} - '@reach/observe-rect@1.2.0': - resolution: {integrity: sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==} - '@reduxjs/toolkit@1.9.7': resolution: {integrity: sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==} peerDependencies: @@ -2317,8 +2313,8 @@ packages: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} deprecated: Use your platform's native atob() and btoa() methods instead - ace-builds@1.32.3: - resolution: {integrity: sha512-ptSTUmDEU+LuwGiPY3/qQPmmAWE27vuv5sASL8swLRyLGJb7Ye7a8MrJ4NnAkFh1sJgVUqKTEGWRRFDmqYPw2Q==} + ace-builds@1.37.1: + resolution: {integrity: sha512-6/jxFucA1z1C3hgLlVkTE5/znZ+iYvD301vfwtybiMc3k76IDykliCD0xh/eYZMJUfsJtaOQHZ2AJO5ey0PHWw==} acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} @@ -3722,8 +3718,8 @@ packages: resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} engines: {node: '>= 0.4'} - intl-messageformat@10.5.8: - resolution: {integrity: sha512-NRf0jpBWV0vd671G5b06wNofAN8tp7WWDogMZyaU8GUAsmbouyvgwmFJI7zLjfAMpm3zK+vSwRP3jzaoIcMbaA==} + intl-messageformat@10.7.10: + resolution: {integrity: sha512-hp7iejCBiJdW3zmOe18FdlJu8U/JsADSDiBPQhfdSeI8B9POtvPRvPh3nMlvhYayGMKLv6maldhR7y3Pf1vkpw==} into-stream@6.0.0: resolution: {integrity: sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==} @@ -4840,11 +4836,6 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' - react-virtual@2.10.4: - resolution: {integrity: sha512-Ir6+oPQZTVHfa6+JL9M7cvMILstFZH/H3jqeYeKI4MSUX+rIruVwFC6nGVXw9wqAw8L0Kg2KvfXxI85OvYQdpQ==} - peerDependencies: - react: ^16.6.3 || ^17.0.0 - react-virtuoso@4.6.2: resolution: {integrity: sha512-vvlqvzPif+MvBrJ09+hJJrVY0xJK9yran+A+/1iwY78k0YCVKsyoNPqoLxOxzYPggspNBNXqUXEcvckN29OxyQ==} engines: {node: '>=10'} @@ -6630,48 +6621,47 @@ snapshots: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 - '@cloudscape-design/collection-hooks@1.0.34(react@18.2.0)': + '@cloudscape-design/collection-hooks@1.0.56(react@18.2.0)': dependencies: react: 18.2.0 - '@cloudscape-design/component-toolkit@1.0.0-beta.30': + '@cloudscape-design/component-toolkit@1.0.0-beta.81': dependencies: '@juggle/resize-observer': 3.4.0 tslib: 2.5.0 - '@cloudscape-design/components@3.0.475(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@cloudscape-design/components@3.0.856(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@cloudscape-design/collection-hooks': 1.0.34(react@18.2.0) - '@cloudscape-design/component-toolkit': 1.0.0-beta.30 - '@cloudscape-design/test-utils-core': 1.0.21 - '@cloudscape-design/theming-runtime': 1.0.39 + '@cloudscape-design/collection-hooks': 1.0.56(react@18.2.0) + '@cloudscape-design/component-toolkit': 1.0.0-beta.81 + '@cloudscape-design/test-utils-core': 1.0.47 + '@cloudscape-design/theming-runtime': 1.0.66 '@dnd-kit/core': 6.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0) '@dnd-kit/utilities': 3.2.2(react@18.2.0) '@juggle/resize-observer': 3.4.0 - ace-builds: 1.32.3 + ace-builds: 1.37.1 balanced-match: 1.0.2 clsx: 1.2.1 d3-shape: 1.3.7 date-fns: 2.30.0 - intl-messageformat: 10.5.8 + intl-messageformat: 10.7.10 mnth: 2.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-keyed-flatten-children: 1.3.0(react@18.2.0) react-transition-group: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - react-virtual: 2.10.4(react@18.2.0) tslib: 2.5.0 weekstart: 1.1.0 - '@cloudscape-design/global-styles@1.0.20': {} + '@cloudscape-design/global-styles@1.0.33': {} - '@cloudscape-design/test-utils-core@1.0.21': + '@cloudscape-design/test-utils-core@1.0.47': dependencies: css-selector-tokenizer: 0.8.0 css.escape: 1.5.1 - '@cloudscape-design/theming-runtime@1.0.39': + '@cloudscape-design/theming-runtime@1.0.66': dependencies: tslib: 2.5.0 @@ -7035,27 +7025,29 @@ snapshots: '@floating-ui/utils@0.1.6': {} - '@formatjs/ecma402-abstract@1.18.0': + '@formatjs/ecma402-abstract@2.3.1': dependencies: - '@formatjs/intl-localematcher': 0.5.2 + '@formatjs/fast-memoize': 2.2.5 + '@formatjs/intl-localematcher': 0.5.9 + decimal.js: 10.4.3 tslib: 2.5.0 - '@formatjs/fast-memoize@2.2.0': + '@formatjs/fast-memoize@2.2.5': dependencies: tslib: 2.5.0 - '@formatjs/icu-messageformat-parser@2.7.3': + '@formatjs/icu-messageformat-parser@2.9.7': dependencies: - '@formatjs/ecma402-abstract': 1.18.0 - '@formatjs/icu-skeleton-parser': 1.7.0 + '@formatjs/ecma402-abstract': 2.3.1 + '@formatjs/icu-skeleton-parser': 1.8.11 tslib: 2.5.0 - '@formatjs/icu-skeleton-parser@1.7.0': + '@formatjs/icu-skeleton-parser@1.8.11': dependencies: - '@formatjs/ecma402-abstract': 1.18.0 + '@formatjs/ecma402-abstract': 2.3.1 tslib: 2.5.0 - '@formatjs/intl-localematcher@0.5.2': + '@formatjs/intl-localematcher@0.5.9': dependencies: tslib: 2.5.0 @@ -7576,8 +7568,6 @@ snapshots: dependencies: '@babel/runtime': 7.23.4 - '@reach/observe-rect@1.2.0': {} - '@reduxjs/toolkit@1.9.7(react-redux@7.2.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)': dependencies: immer: 9.0.21 @@ -8290,7 +8280,7 @@ snapshots: abab@2.0.6: {} - ace-builds@1.32.3: {} + ace-builds@1.37.1: {} acorn-globals@7.0.1: dependencies: @@ -10112,11 +10102,11 @@ snapshots: has: 1.0.3 side-channel: 1.0.4 - intl-messageformat@10.5.8: + intl-messageformat@10.7.10: dependencies: - '@formatjs/ecma402-abstract': 1.18.0 - '@formatjs/fast-memoize': 2.2.0 - '@formatjs/icu-messageformat-parser': 2.7.3 + '@formatjs/ecma402-abstract': 2.3.1 + '@formatjs/fast-memoize': 2.2.5 + '@formatjs/icu-messageformat-parser': 2.9.7 tslib: 2.5.0 into-stream@6.0.0: @@ -11232,11 +11222,6 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-virtual@2.10.4(react@18.2.0): - dependencies: - '@reach/observe-rect': 1.2.0 - react: 18.2.0 - react-virtuoso@4.6.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: react: 18.2.0