diff --git a/.changeset/modern-tabular-data-upload.md b/.changeset/modern-tabular-data-upload.md new file mode 100644 index 00000000..07ff7634 --- /dev/null +++ b/.changeset/modern-tabular-data-upload.md @@ -0,0 +1,5 @@ +--- +"@usace-watermanagement/groundwork-water": minor +--- + +Add a React and Groundwork-based CWMS tabular data upload form with workbook validation, existing-data comparison, batch submission, deletion, and generated templates. diff --git a/docs/src/bundles/route-bundle.js b/docs/src/bundles/route-bundle.js index 4877fa6d..ca8358e9 100644 --- a/docs/src/bundles/route-bundle.js +++ b/docs/src/bundles/route-bundle.js @@ -40,6 +40,7 @@ import FormsDocs from "../pages/docs/forms"; import CWMSFormDocs from "../pages/docs/forms/cwms-form"; import CWMSInputDocs from "../pages/docs/forms/cwms-input"; import CWMSFileUploadDocs from "../pages/docs/forms/cwms-file-upload"; +import CWMSDataUploadDocs from "../pages/docs/forms/cwms-data-upload"; import CWMSTextareaDocs from "../pages/docs/forms/cwms-textarea"; import CWMSCheckboxesDocs from "../pages/docs/forms/cwms-checkboxes"; import CWMSRadioGroupDocs from "../pages/docs/forms/cwms-radio-group"; @@ -87,6 +88,7 @@ export default createRouteBundle( "/docs/forms/cwms-form": CWMSFormDocs, "/docs/forms/cwms-input": CWMSInputDocs, "/docs/forms/cwms-file-upload": CWMSFileUploadDocs, + "/docs/forms/cwms-data-upload": CWMSDataUploadDocs, "/docs/forms/cwms-textarea": CWMSTextareaDocs, "/docs/forms/cwms-checkboxes": CWMSCheckboxesDocs, "/docs/forms/cwms-radio-group": CWMSRadioGroupDocs, diff --git a/docs/src/nav-links.js b/docs/src/nav-links.js index 4576d989..6c66802c 100644 --- a/docs/src/nav-links.js +++ b/docs/src/nav-links.js @@ -206,6 +206,11 @@ export default [ text: "CWMS File Upload", href: `${BASE_URL}#/docs/forms/cwms-file-upload`, }, + { + id: "cwms-data-upload", + text: "CWMS Data Upload", + href: `${BASE_URL}#/docs/forms/cwms-data-upload`, + }, { id: "cwms-input-table", text: "CWMS Input Table", diff --git a/docs/src/pages/docs/forms/cwms-data-upload.jsx b/docs/src/pages/docs/forms/cwms-data-upload.jsx new file mode 100644 index 00000000..56ad5f02 --- /dev/null +++ b/docs/src/pages/docs/forms/cwms-data-upload.jsx @@ -0,0 +1,246 @@ +import { Badge, Code, Text, UsaceBox } from "@usace/groundwork"; +import { + CWMSDataUpload, + CWMSForm, + CWMS_DATA_UPLOAD_HEADERS, +} from "@usace-watermanagement/groundwork-water"; +import { Code as CodeBlock } from "../../components/code"; +import PropsTable from "../../components/props-table"; +import Divider from "../../components/divider"; +import DocsPage from "../_docs-wrapper"; + +const DEMO_ROWS = [ + CWMS_DATA_UPLOAD_HEADERS, + [ + "MVS", + "DEMO.Stage.Inst.1Hour.0.TEST", + "2026-01-01 00:00", + "10.25", + "0", + "", + "Observed", + ], + ["MVS", "DEMO.Stage.Inst.1Hour.0.TEST", "2026-01-01 01:00", "10.5", "0", "", ""], + [ + "MVS", + "DEMO.Stage.Inst.1Hour.0.TEST", + "2026-01-01 02:00", + "10.75", + "3", + "2026-01-01 02:05", + "Questionable", + ], +]; + +const componentProps = [ + { + name: "office", + type: "string", + default: "workbook OFFICE", + desc: "Expected CWMS office. When supplied, every workbook row must match it.", + }, + { + name: "cdaUrl", + type: "string", + default: "CdaUrlProvider or cwmsjs default", + desc: "CWMS Data API base URL used to compare, submit, and delete values.", + }, + { + name: "unit", + type: "string", + default: "ft", + desc: "Unit used to retrieve and submit numeric time-series values.", + }, + { + name: "timezone", + type: "string", + default: "America/Chicago", + desc: "IANA timezone used to interpret workbook DATE TIME values.", + }, + { + name: "storeRule", + type: "string", + default: "REPLACE_ALL", + desc: "CDA store rule used by CWMSForm for numeric batch submission.", + }, + { + name: "defaultFilter", + type: "all | existing | new", + default: "all", + desc: "Initial row filter. The visible rows are also the rows submitted or deleted.", + }, + { + name: "maxRows", + type: "number", + default: "200000", + desc: "Maximum number of workbook data rows accepted.", + }, + { + name: "maxIntradayYears", + type: "number", + default: "10", + desc: "Maximum span for minute and hourly time-series workbooks.", + }, + { + name: "maxPreviewRows", + type: "number", + default: "250", + desc: "Maximum number of selected rows rendered in the preview table.", + }, + { + name: "showPlot", + type: "boolean", + default: "true", + desc: "Show a CWMSPlot comparison of workbook and existing values.", + }, + { + name: "showDeleteButton", + type: "boolean", + default: "true", + desc: "Show the two-step authenticated deletion control for the selected range.", + }, + { + name: "loadExistingData", + type: "boolean", + default: "true", + desc: "Retrieve matching CDA values for classification and comparison.", + }, + { + name: "initialData", + type: "array | parsed model", + default: "undefined", + desc: "Preload worksheet rows or a parsed model. Useful for tests and controlled examples.", + }, + { + name: "onChange", + type: "function", + default: "undefined", + desc: "Called with the parsed workbook model, or null when cleared or invalid.", + }, +]; + +function CWMSDataUploadDocs() { + return ( + + + CWMSDataUpload converts a standard Excel workbook into validated + numeric and text time-series batches. It uses Groundwork components and + Groundwork Water's form, authentication, CDA query, plotting, and toast + infrastructure rather than a standalone page or copied vendor scripts. + + +
+ React + Groundwork + Tailwind CSS + TanStack Query + cwmsjs +
+ + + + This documentation example uses controlled in-memory rows. Existing-data reads, + submission buttons, and deletion are disabled, so it exercises the complete + presentation and form-registration path without changing CDA. + + + + + + + + The component reads the first worksheet, preferring a sheet named{" "} + Sheet1. These seven headers must appear in order: + +
    + {CWMS_DATA_UPLOAD_HEADERS.map((header) => ( +
  1. + {header} +
  2. + ))} +
+ + All rows must use one office and one TSID. Dates use{" "} + YYYY-MM-DD HH:mm and are interpreted in the configured IANA + timezone. Each row requires a numeric value, a text value, or both. Duplicate + local timestamps, malformed values, invalid quality codes, and oversized ranges + are rejected before submission. + + + + + {`import { + AuthProvider, + CdaUrlProvider, + CWMSDataUpload, + CWMSForm, +} from "@usace-watermanagement/groundwork-water"; + +const cdaUrl = "https://cwms-data.usace.army.mil/cwms-data"; + + + + console.log(result)} + > + + + +`} + + + +
    +
  • Numeric rows are grouped into one CDA time-series payload per TSID.
  • +
  • Text rows are grouped into CWMS regular text time-series payloads.
  • +
  • The current all/existing/new filter controls the submitted rows.
  • +
  • Authentication comes from AuthProvider; no login polling is required.
  • +
  • Successful writes invalidate matching TanStack Query caches.
  • +
  • Deletion requires a second explicit confirmation click.
  • +
+
+ + + + Use useCwmsDataUpload when an application needs its own layout. The + hook retrieves existing values, classifies parsed rows, applies the selected + filter, refreshes the comparison query, and provides the authenticated range + deletion mutation. + + + {`const { + classifiedRows, + filteredRows, + existingData, + isLoadingExisting, + refreshExisting, + deleteRows, +} = useCwmsDataUpload({ + model: parsedWorkbook, + filter: "new", + unit: "ft", +});`} + + + + +
+ ); +} + +export { CWMSDataUploadDocs }; +export default CWMSDataUploadDocs; diff --git a/docs/src/pages/docs/forms/index.jsx b/docs/src/pages/docs/forms/index.jsx index 10dd1b76..70415ccf 100644 --- a/docs/src/pages/docs/forms/index.jsx +++ b/docs/src/pages/docs/forms/index.jsx @@ -58,6 +58,15 @@ function FormsDocs() { {" "} - Drag-and-drop blob upload input to CWMS Data API (CDA) +
  • + + CWMSDataUpload + {" "} + - Validated Excel upload, CDA comparison, plotting, and batch submission +
  • { plugins: [react(), tailwindcss()], base: base, resolve: { + // The development alias loads library source from the parent package. Dedupe + // shared React peers back to the docs app so linked/file installs do not create + // a second hooks runtime. + dedupe: [ + "react", + "react-dom", + "react/jsx-runtime", + "@tanstack/react-query", + "@usace/groundwork", + "react-toastify", + ], alias: isDevelopment ? [ // During development, alias to the source files for easier debugging diff --git a/lib/components/data/forms/CWMSForm.jsx b/lib/components/data/forms/CWMSForm.jsx index 1b9e560f..b873b219 100644 --- a/lib/components/data/forms/CWMSForm.jsx +++ b/lib/components/data/forms/CWMSForm.jsx @@ -286,7 +286,9 @@ export function CWMSForm({ } // Submit using TanStack Query mutation - const submitInputs = formData.filter((input) => input.tsid || input.blobId); + const submitInputs = formData.filter( + (input) => input.tsid || input.blobId || input.kind === "timeseries-batch", + ); if (submitInputs.length > 0) { mutation.mutate(submitInputs); diff --git a/lib/components/data/forms/helpers/__tests__/dataUpload.test.js b/lib/components/data/forms/helpers/__tests__/dataUpload.test.js new file mode 100644 index 00000000..124e72f0 --- /dev/null +++ b/lib/components/data/forms/helpers/__tests__/dataUpload.test.js @@ -0,0 +1,118 @@ +import { describe, expect, it } from "vitest"; +import ExcelJS from "exceljs"; +import { + CWMS_DATA_UPLOAD_HEADERS, + buildCwmsDataUploadPayloads, + classifyCwmsDataUploadRows, + createCwmsDataUploadTemplate, + filterCwmsDataUploadRows, + parseCwmsDataUploadRows, +} from "../dataUpload"; + +const validRows = [ + CWMS_DATA_UPLOAD_HEADERS, + [ + "MVS", + "TEST.Stage.Inst.1Hour.0.TEST", + "2026-01-01 00:00", + "10.25", + "0", + "", + "first", + ], + [ + "MVS", + "TEST.Stage.Inst.1Hour.0.TEST", + "2026-01-01 01:00", + "10.5", + "3", + "2026-01-01 01:05", + "", + ], +]; + +describe("CWMS tabular data helpers", () => { + it("parses and normalizes a valid workbook", () => { + const model = parseCwmsDataUploadRows(validRows, { office: "MVS" }); + + expect(model.office).toBe("MVS"); + expect(model.tsid).toBe("TEST.Stage.Inst.1Hour.0.TEST"); + expect(model.rows).toHaveLength(2); + expect(model.rows[0]).toMatchObject({ + value: 10.25, + qualityCode: 0, + timestamp: "2026-01-01T06:00:00.000Z", + }); + expect(model.rows[1].dataEntryDate).toBeInstanceOf(Date); + }); + + it("reports structural and row validation errors together", () => { + const rows = [ + CWMS_DATA_UPLOAD_HEADERS, + ["MVS", "A.Stage.Inst.1Hour.0.TEST", "not-a-date", "bad", "-1", "", ""], + ["SWT", "B.Stage.Inst.1Hour.0.TEST", "2026-01-01 01:00", "1", "0", "", ""], + ]; + + expect(() => parseCwmsDataUploadRows(rows, { office: "MVS" })).toThrowError( + expect.objectContaining({ + issues: expect.arrayContaining([ + expect.stringContaining("DATE TIME must use"), + expect.stringContaining("VALUE bad is not numeric"), + expect.stringContaining("QUALITY CODE"), + expect.stringContaining("does not match configured office"), + expect.stringContaining("All rows must use one OFFICE"), + expect.stringContaining("All rows must use one TSID"), + ]), + }), + ); + }); + + it("builds grouped numeric and text CDA payloads", () => { + const model = parseCwmsDataUploadRows(validRows); + const payloads = buildCwmsDataUploadPayloads(model.rows, { + unit: "ft", + valueUrlBase: "https://example.test/cwms-data/", + }); + + expect(payloads.numeric).toHaveLength(1); + expect(payloads.numeric[0]).toMatchObject({ + name: model.tsid, + officeId: "MVS", + units: "ft", + }); + expect(payloads.numeric[0].values).toEqual([ + [model.rows[0].epoch, 10.25, 0], + [model.rows[1].epoch, 10.5, 3], + ]); + expect(payloads.text[0].regularTextValues[0]).toMatchObject({ + textValue: "first", + qualityCode: 0, + valueUrl: "https://example.test/cwms-data/timeseries/text/ignored", + }); + }); + + it("classifies and filters values already present in CDA", () => { + const model = parseCwmsDataUploadRows(validRows); + const classified = classifyCwmsDataUploadRows(model.rows, [ + [model.rows[0].epoch, 10.25, 0], + ]); + + expect(classified.map((row) => row.status)).toEqual(["existing", "new"]); + expect(filterCwmsDataUploadRows(classified, "existing")).toHaveLength(1); + expect(filterCwmsDataUploadRows(classified, "new")).toHaveLength(1); + }); + + it("generates the reusable workbook template", async () => { + const buffer = await createCwmsDataUploadTemplate({ + office: "MVS", + tsid: "TEST.Stage.Inst.1Hour.0.TEST", + }); + const workbook = new ExcelJS.Workbook(); + await workbook.xlsx.load(buffer); + const worksheet = workbook.getWorksheet("Sheet1"); + + expect(worksheet.getRow(1).values.slice(1)).toEqual(CWMS_DATA_UPLOAD_HEADERS); + expect(worksheet.getRow(2).getCell(1).value).toBe("MVS"); + expect(worksheet.getRow(2).getCell(2).value).toBe("TEST.Stage.Inst.1Hour.0.TEST"); + }); +}); diff --git a/lib/components/data/forms/helpers/dataUpload.js b/lib/components/data/forms/helpers/dataUpload.js new file mode 100644 index 00000000..e2e70514 --- /dev/null +++ b/lib/components/data/forms/helpers/dataUpload.js @@ -0,0 +1,360 @@ +import dayjs from "dayjs"; +import customParseFormat from "dayjs/plugin/customParseFormat.js"; +import timezonePlugin from "dayjs/plugin/timezone.js"; +import utc from "dayjs/plugin/utc.js"; + +dayjs.extend(customParseFormat); +dayjs.extend(utc); +dayjs.extend(timezonePlugin); + +const DATE_TIME_FORMAT = "YYYY-MM-DD HH:mm"; +const DEFAULT_TIMEZONE = "America/Chicago"; +const CWMS_DATA_UPLOAD_HEADERS = [ + "OFFICE", + "TSID", + "DATE TIME", + "VALUE", + "QUALITY CODE", + "DATA ENTRY DATE", + "TEXT VALUE", +]; +const INTRADAY_INTERVALS = new Set([ + "1Minute", + "5Minutes", + "10Minutes", + "15Minutes", + "30Minutes", + "1Hour", +]); + +class CwmsDataUploadValidationError extends Error { + constructor(issues) { + const normalizedIssues = Array.isArray(issues) ? issues : [String(issues)]; + super(normalizedIssues[0] || "The workbook is not a valid CWMS data upload."); + this.name = "CwmsDataUploadValidationError"; + this.issues = normalizedIssues; + } +} + +const cellText = (value) => { + if (value === null || value === undefined) return ""; + if (value instanceof Date) return dayjs(value).format(DATE_TIME_FORMAT); + if (typeof value === "object") { + if (value.text !== undefined) return String(value.text).trim(); + if (value.result !== undefined) return cellText(value.result); + if (Array.isArray(value.richText)) { + return value.richText + .map((part) => part.text || "") + .join("") + .trim(); + } + } + return String(value).trim(); +}; + +const parseLocalTimestamp = (value, timezone, rowNumber, label = "DATE TIME") => { + const text = cellText(value); + if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(text)) { + throw new CwmsDataUploadValidationError( + `Row ${rowNumber}: ${label} must use ${DATE_TIME_FORMAT}.`, + ); + } + + const parsed = dayjs.tz(text, DATE_TIME_FORMAT, timezone); + if (!parsed.isValid() || parsed.format(DATE_TIME_FORMAT) !== text) { + throw new CwmsDataUploadValidationError( + `Row ${rowNumber}: ${label} is not a valid time in ${timezone}.`, + ); + } + return parsed; +}; + +const parseDataEntryDate = (value, timezone, rowNumber) => { + const text = cellText(value); + if (!text || text.toUpperCase() === "NA") return undefined; + + if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(text)) { + return parseLocalTimestamp(text, timezone, rowNumber, "DATA ENTRY DATE").toDate(); + } + + const parsed = dayjs(text); + if (!parsed.isValid()) { + throw new CwmsDataUploadValidationError( + `Row ${rowNumber}: DATA ENTRY DATE must be blank, NA, an ISO timestamp, or ${DATE_TIME_FORMAT}.`, + ); + } + return parsed.toDate(); +}; + +const validateHeaders = (headerRow) => { + const issues = []; + CWMS_DATA_UPLOAD_HEADERS.forEach((header, index) => { + const actual = cellText(headerRow?.[index]); + if (actual !== header) { + issues.push( + `Column ${index + 1} must be ${header}; found ${actual || "a blank header"}.`, + ); + } + }); + if (issues.length) throw new CwmsDataUploadValidationError(issues); +}; + +function parseCwmsDataUploadRows( + worksheetRows, + { + timezone = DEFAULT_TIMEZONE, + maxRows = 200000, + maxIntradayYears = 10, + office: expectedOffice, + } = {}, +) { + if (!Array.isArray(worksheetRows) || worksheetRows.length < 2) { + throw new CwmsDataUploadValidationError( + "The workbook must contain a header and at least one data row.", + ); + } + if (worksheetRows.length - 1 > maxRows) { + throw new CwmsDataUploadValidationError( + `The workbook contains ${worksheetRows.length - 1} data rows; the limit is ${maxRows}.`, + ); + } + + validateHeaders(worksheetRows[0]); + + const rows = []; + const issues = []; + const seenLocalTimes = new Set(); + const offices = new Set(); + const tsids = new Set(); + + worksheetRows.slice(1).forEach((sourceRow, index) => { + const rowNumber = index + 2; + const values = Array.from({ length: CWMS_DATA_UPLOAD_HEADERS.length }, (_, col) => + cellText(sourceRow?.[col]), + ); + if (values.every((value) => value === "")) return; + + const [office, tsid, dateTime, rawValue, rawQuality, dataEntryDate, textValue] = + values; + + if (!office) issues.push(`Row ${rowNumber}: OFFICE is required.`); + if (!tsid) issues.push(`Row ${rowNumber}: TSID is required.`); + if (expectedOffice && office && office !== expectedOffice) { + issues.push( + `Row ${rowNumber}: OFFICE ${office} does not match configured office ${expectedOffice}.`, + ); + } + if (!rawValue && !textValue) { + issues.push(`Row ${rowNumber}: provide VALUE, TEXT VALUE, or both.`); + } + + let parsedTime; + try { + parsedTime = parseLocalTimestamp(dateTime, timezone, rowNumber); + } catch (error) { + issues.push(...(error.issues || [error.message])); + } + + const duplicateKey = `${office}|${tsid}|${dateTime}`; + if (seenLocalTimes.has(duplicateKey)) { + issues.push(`Row ${rowNumber}: duplicate DATE TIME ${dateTime} for ${tsid}.`); + } + seenLocalTimes.add(duplicateKey); + + const value = rawValue === "" ? null : Number(rawValue); + if (rawValue !== "" && !Number.isFinite(value)) { + issues.push(`Row ${rowNumber}: VALUE ${rawValue} is not numeric.`); + } + + const qualityCode = rawQuality === "" ? 0 : Number(rawQuality); + if (!Number.isSafeInteger(qualityCode) || qualityCode < 0) { + issues.push(`Row ${rowNumber}: QUALITY CODE must be a non-negative integer.`); + } + + let parsedEntryDate; + try { + parsedEntryDate = parseDataEntryDate(dataEntryDate, timezone, rowNumber); + } catch (error) { + issues.push(...(error.issues || [error.message])); + } + + if (office) offices.add(office); + if (tsid) tsids.add(tsid); + if (office && tsid && parsedTime?.isValid()) { + rows.push({ + rowNumber, + office, + tsid, + dateTime, + timestamp: parsedTime.toISOString(), + epoch: parsedTime.valueOf(), + value: Number.isFinite(value) ? value : null, + qualityCode: Number.isSafeInteger(qualityCode) ? qualityCode : 0, + dataEntryDate: parsedEntryDate, + textValue, + }); + } + }); + + if (offices.size > 1) { + issues.push(`All rows must use one OFFICE; found ${[...offices].join(", ")}.`); + } + if (tsids.size > 1) { + issues.push(`All rows must use one TSID; found ${[...tsids].join(", ")}.`); + } + if (!rows.length && !issues.length) issues.push("The workbook has no data rows."); + + if (rows.length) { + const tsid = rows[0].tsid; + const interval = tsid.split(".")[3]; + const spanYears = (rows.at(-1).epoch - rows[0].epoch) / 31557600000; + if (INTRADAY_INTERVALS.has(interval) && spanYears > maxIntradayYears) { + issues.push( + `${interval} uploads may span at most ${maxIntradayYears} years; this workbook spans ${spanYears.toFixed(2)} years.`, + ); + } + } + + if (issues.length) throw new CwmsDataUploadValidationError(issues); + + const sortedRows = [...rows].sort((a, b) => a.epoch - b.epoch); + return { + rows: sortedRows, + office: sortedRows[0].office, + tsid: sortedRows[0].tsid, + begin: sortedRows[0].timestamp, + end: sortedRows.at(-1).timestamp, + timezone, + }; +} + +async function getExcelModule() { + const excel = await import("exceljs"); + return excel.default || excel; +} + +async function readCwmsDataUploadFile(file, options) { + const ExcelJS = await getExcelModule(); + const workbook = new ExcelJS.Workbook(); + const source = + typeof file?.arrayBuffer === "function" ? await file.arrayBuffer() : file; + await workbook.xlsx.load(source); + const worksheet = workbook.getWorksheet("Sheet1") || workbook.worksheets[0]; + if (!worksheet) { + throw new CwmsDataUploadValidationError( + "The workbook does not contain a worksheet.", + ); + } + + const rows = []; + worksheet.eachRow({ includeEmpty: true }, (row) => { + rows.push( + Array.from({ length: CWMS_DATA_UPLOAD_HEADERS.length }, (_, index) => + cellText(row.getCell(index + 1).value), + ), + ); + }); + return parseCwmsDataUploadRows(rows, options); +} + +async function createCwmsDataUploadTemplate({ office = "", tsid = "" } = {}) { + const ExcelJS = await getExcelModule(); + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet("Sheet1"); + worksheet.addRow(CWMS_DATA_UPLOAD_HEADERS); + worksheet.addRow([office, tsid, "2026-01-01 00:00", "", "0", "", ""]); + worksheet.getRow(1).font = { bold: true }; + worksheet.columns = [12, 46, 20, 14, 16, 22, 30].map((width) => ({ width })); + return workbook.xlsx.writeBuffer(); +} + +function buildCwmsDataUploadPayloads( + rows, + { unit = "ft", textFilename = "cwms-data-upload.txt", valueUrlBase } = {}, +) { + const numericGroups = new Map(); + const textGroups = new Map(); + + rows.forEach((row) => { + const key = `${row.office}|${row.tsid}`; + if (row.value !== null && row.value !== undefined) { + if (!numericGroups.has(key)) { + numericGroups.set(key, { + name: row.tsid, + officeId: row.office, + units: unit, + values: [], + }); + } + numericGroups.get(key).values.push([row.epoch, row.value, row.qualityCode || 0]); + } + + if (row.textValue) { + if (!textGroups.has(key)) { + textGroups.set(key, { + name: row.tsid, + officeId: row.office, + intervalOffset: 0, + timeZone: "UTC", + dateVersionType: "MAX_AGGREGATE", + regularTextValues: [], + }); + } + textGroups.get(key).regularTextValues.push({ + dateTime: new Date(row.epoch), + dataEntryDate: row.dataEntryDate || new Date(), + textValue: row.textValue, + filename: textFilename, + mediaType: "text/plain", + qualityCode: row.qualityCode || 0, + destFlag: 0, + valueUrl: valueUrlBase + ? `${valueUrlBase.replace(/\/$/, "")}/timeseries/text/ignored` + : undefined, + }); + } + }); + + return { + numeric: [...numericGroups.values()], + text: [...textGroups.values()], + }; +} + +function classifyCwmsDataUploadRows(rows, existingValues = []) { + const existing = new Set( + existingValues + .filter((item) => item?.[0] !== undefined && Number.isFinite(Number(item?.[1]))) + .map((item) => { + const time = Number.isFinite(Number(item[0])) + ? Number(item[0]) + : new Date(item[0]).getTime(); + return `${time}|${Number(item[1]).toFixed(2)}`; + }), + ); + + return rows.map((row) => ({ + ...row, + status: + row.value !== null && existing.has(`${row.epoch}|${Number(row.value).toFixed(2)}`) + ? "existing" + : "new", + })); +} + +function filterCwmsDataUploadRows(rows, filter = "all") { + if (filter === "existing") return rows.filter((row) => row.status === "existing"); + if (filter === "new") return rows.filter((row) => row.status !== "existing"); + return rows; +} + +export { + CWMS_DATA_UPLOAD_HEADERS, + CwmsDataUploadValidationError, + buildCwmsDataUploadPayloads, + classifyCwmsDataUploadRows, + createCwmsDataUploadTemplate, + filterCwmsDataUploadRows, + parseCwmsDataUploadRows, + readCwmsDataUploadFile, +}; diff --git a/lib/components/data/forms/hooks/useCwmsFormSubmit.js b/lib/components/data/forms/hooks/useCwmsFormSubmit.js index c42331c8..1f1f73c7 100644 --- a/lib/components/data/forms/hooks/useCwmsFormSubmit.js +++ b/lib/components/data/forms/hooks/useCwmsFormSubmit.js @@ -1,6 +1,7 @@ import React from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { TimeSeriesApi, TextTimeSeriesApi, BlobApi, Configuration } from "cwmsjs"; +import { buildCwmsDataUploadPayloads } from "../helpers/dataUpload"; /** * Custom hook for submitting CWMS form data using TanStack Query @@ -28,9 +29,9 @@ export function useCwmsFormSubmit({ const isSubmittingRef = React.useRef(false); // Create configuration for CWMS APIs - const createApiConfig = () => { + const createApiConfig = (inputCdaUrl = cdaUrl) => { const configParams = { - basePath: cdaUrl, + basePath: inputCdaUrl, headers: { accept: "application/json;version=2", }, @@ -72,9 +73,66 @@ export function useCwmsFormSubmit({ const results = []; const errors = []; - // Process each input with TSID or blob + // Process each input with TSID, blob, or a parsed tabular upload. for (const input of formInputs) { - if (input.kind === "blob" || input.blobId) { + if (input.kind === "timeseries-batch") { + const batchConfig = input.cdaUrl ? createApiConfig(input.cdaUrl) : config; + const batchTsApi = new TimeSeriesApi(batchConfig); + const batchTextApi = new TextTimeSeriesApi(batchConfig); + const payloads = buildCwmsDataUploadPayloads(input.rows || [], { + unit: input.unit, + valueUrlBase: input.cdaUrl || cdaUrl, + }); + + for (const timeSeries of payloads.numeric) { + try { + await batchTsApi.postTimeSeries({ + timeSeries, + timezone: "UTC", + createAsLrts: false, + storeRule: input.storeRule || storeRule, + overrideProtection: input.overrideProtection ?? true, + }); + results.push({ + tsid: timeSeries.name, + type: "numeric-batch", + status: "success", + valueCount: timeSeries.values.length, + }); + } catch (error) { + errors.push({ + tsid: timeSeries.name, + error: error.message || "Unknown error", + details: error, + response: error.response?.data, + status: error.response?.status, + }); + } + } + + for (const textTimeSeries of payloads.text) { + try { + await batchTextApi.postTimeSeriesText({ + textTimeSeries, + replaceAll: true, + }); + results.push({ + tsid: textTimeSeries.name, + type: "text-batch", + status: "success", + valueCount: textTimeSeries.regularTextValues.length, + }); + } catch (error) { + errors.push({ + tsid: textTimeSeries.name, + error: error.message || "Unknown error", + details: error, + response: error.response?.data, + status: error.response?.status, + }); + } + } + } else if (input.kind === "blob" || input.blobId) { try { const rawValue = input.value ?? input.values?.[0]; @@ -210,9 +268,17 @@ export function useCwmsFormSubmit({ onSuccess: (data, variables) => { // Invalidate only the specific TSIDs that were submitted if (variables) { - const submittedTsids = variables - .filter((input) => input.tsid) - .map((input) => input.tsid); + const submittedTsids = [ + ...new Set( + variables.flatMap((input) => + input.kind === "timeseries-batch" + ? (input.rows || []).map((row) => row.tsid).filter(Boolean) + : input.tsid + ? [input.tsid] + : [], + ), + ), + ]; const submittedBlobIds = variables .filter((input) => input.blobId) .map((input) => input.blobId); @@ -222,7 +288,7 @@ export function useCwmsFormSubmit({ queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === "cda" && - query.queryKey[1] === "timeseries" && + ["timeseries", "data-upload"].includes(query.queryKey[1]) && query.queryKey.includes(tsid), }); }); diff --git a/lib/components/data/forms/inputs/CWMSDataUpload.jsx b/lib/components/data/forms/inputs/CWMSDataUpload.jsx new file mode 100644 index 00000000..9c02facd --- /dev/null +++ b/lib/components/data/forms/inputs/CWMSDataUpload.jsx @@ -0,0 +1,459 @@ +import React, { useContext, useEffect, useId, useMemo, useRef, useState } from "react"; +import { + Badge, + Button, + Dropdown, + Field, + Label, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + UsaceBox, + gwMerge, +} from "@usace/groundwork"; +import { FormContext } from "../CWMSForm"; +import CWMSPlot from "../../plots/CWMSPlot"; +import useCwmsDataUpload from "../../hooks/useCwmsDataUpload"; +import { + createCwmsDataUploadTemplate, + parseCwmsDataUploadRows, + readCwmsDataUploadFile, +} from "../helpers/dataUpload"; + +const FILTER_OPTIONS = [ + { value: "all", label: "All data points" }, + { value: "existing", label: "Existing data points only" }, + { value: "new", label: "New data points only" }, +]; + +function toInitialModel(initialData, options) { + if (!initialData) return null; + if (Array.isArray(initialData)) return parseCwmsDataUploadRows(initialData, options); + if (Array.isArray(initialData.rows)) return initialData; + return null; +} + +function CWMSDataUpload({ + name = "cwmsDataUpload", + label = "CWMS tabular data upload", + helperText = "Upload an .xlsx workbook using the CWMS tabular data template.", + cdaUrl, + office, + unit = "ft", + timezone = "America/Chicago", + storeRule = "REPLACE_ALL", + overrideProtection = true, + maxRows = 200000, + maxIntradayYears = 10, + maxPreviewRows = 250, + defaultFilter = "all", + required = true, + disabled = false, + showPlot = true, + showDeleteButton = true, + loadExistingData = true, + initialData, + className = "", + onChange, + onDeleteSuccess, + onDeleteError, +}) { + const formContext = useContext(FormContext); + const registerInput = formContext?.registerInput; + const inputId = useId(); + const fileInputRef = useRef(null); + const options = useMemo( + () => ({ office, timezone, maxRows, maxIntradayYears }), + [maxIntradayYears, maxRows, office, timezone], + ); + const [model, setModel] = useState(() => { + try { + return toInitialModel(initialData, options); + } catch { + return null; + } + }); + const [fileName, setFileName] = useState(""); + const [filter, setFilter] = useState(defaultFilter); + const [isReading, setIsReading] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const [issues, setIssues] = useState([]); + const [isInvalid, setIsInvalid] = useState(false); + const [validationMessage, setValidationMessage] = useState(""); + const [confirmDelete, setConfirmDelete] = useState(false); + + const { + classifiedRows, + filteredRows, + existingData, + isLoadingExisting, + existingError, + refreshExisting, + deleteRows, + isDeleting, + deleteError, + } = useCwmsDataUpload({ + model, + filter, + cdaUrl, + unit, + loadExistingData, + onDeleteSuccess, + onDeleteError, + }); + + const reset = () => { + setModel(null); + setFileName(""); + setFilter(defaultFilter); + setIssues([]); + setIsInvalid(false); + setValidationMessage(""); + setConfirmDelete(false); + if (fileInputRef.current) fileInputRef.current.value = ""; + onChange?.(null); + }; + + const validate = () => { + if (issues.length) return issues[0]; + if (required && !model?.rows?.length) return `${label} is required.`; + if (model && !filteredRows.length) return "The selected filter contains no rows."; + return null; + }; + + useEffect(() => { + if (!registerInput || disabled) return undefined; + return registerInput({ + kind: "timeseries-batch", + name, + label, + required, + getValues: () => [model?.rows?.length ? String(model.rows.length) : ""], + getSubmissionData: () => ({ + kind: "timeseries-batch", + name, + rows: filteredRows, + unit, + storeRule, + overrideProtection, + cdaUrl, + }), + validate, + reset, + setInvalid: setIsInvalid, + setValidationMessage, + }); + }, [ + cdaUrl, + disabled, + filteredRows, + issues, + label, + model, + name, + overrideProtection, + registerInput, + required, + storeRule, + unit, + ]); + + const loadFile = async (file) => { + if (!file || disabled) return; + setIsReading(true); + setIssues([]); + setValidationMessage(""); + setIsInvalid(false); + setConfirmDelete(false); + try { + const nextModel = await readCwmsDataUploadFile(file, options); + setModel(nextModel); + setFileName(file.name); + onChange?.(nextModel); + } catch (error) { + const nextIssues = error?.issues || [ + error?.message || "Unable to read workbook.", + ]; + setModel(null); + setFileName(file.name); + setIssues(nextIssues); + setIsInvalid(true); + setValidationMessage(nextIssues[0]); + onChange?.(null); + } finally { + setIsReading(false); + } + }; + + const downloadTemplate = async () => { + const buffer = await createCwmsDataUploadTemplate({ office }); + const blob = new Blob([buffer], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "cwms-data-upload-template.xlsx"; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); + }; + + const handleDelete = async () => { + if (!confirmDelete) { + setConfirmDelete(true); + return; + } + await deleteRows(filteredRows); + setConfirmDelete(false); + }; + + const previewRows = filteredRows.slice(0, maxPreviewRows); + const uploadPlotRows = filteredRows.filter((row) => row.value !== null); + const existingCount = classifiedRows.filter( + (row) => row.status === "existing", + ).length; + const newCount = classifiedRows.length - existingCount; + const plotData = + existingData || + (model + ? { name: model.tsid, officeId: model.office, units: unit, values: [] } + : null); + + return ( +
    + + + loadFile(event.target.files?.[0])} + /> +
    { + event.preventDefault(); + if (!disabled) setIsDragging(true); + }} + onDragLeave={() => setIsDragging(false)} + onDrop={(event) => { + event.preventDefault(); + setIsDragging(false); + loadFile(event.dataTransfer?.files?.[0]); + }} + aria-invalid={isInvalid} + > +
    +
    + {isReading + ? "Reading workbook..." + : fileName || "Drag an .xlsx workbook here"} +
    +
    {helperText}
    +
    +
    + + + {model ? ( + + ) : null} +
    +
    +
    + + {issues.length ? ( +
    +
    Workbook validation failed
    +
      + {issues.map((issue) => ( +
    • {issue}
    • + ))} +
    +
    + ) : null} + {validationMessage && !issues.length ? ( +
    + {validationMessage} +
    + ) : null} + + {model ? ( + <> + +
    + {model.office} + + {model.tsid} + + {classifiedRows.length.toLocaleString()} rows + {newCount.toLocaleString()} new + {existingCount.toLocaleString()} existing +
    +
    + {model.begin} through {model.end} · {unit} · {timezone} +
    +
    + +
    + + + { + setFilter(event.target.value); + setConfirmDelete(false); + }} + options={FILTER_OPTIONS.map((option) => ( + + ))} + /> + + + {showDeleteButton ? ( + + ) : null} + {confirmDelete ? ( + + ) : null} +
    + + {existingError || deleteError ? ( +
    + {(existingError || deleteError)?.message || "The CDA request failed."} +
    + ) : null} + + {showPlot && uploadPlotRows.length ? ( + new Date(row.epoch)), + y: uploadPlotRows.map((row) => row.value), + text: uploadPlotRows.map((row) => row.textValue), + name: "Workbook data", + mode: "markers", + type: "scatter", + }, + ]} + layoutOptions={{ + height: 420, + title: { text: "Workbook and existing time-series values" }, + }} + /> + ) : null} + +
    + + + + Status + Office + TSID + Date time ({timezone}) + Value + Quality + Text value + + + + {previewRows.map((row) => ( + + + + {row.status} + + + {row.office} + {row.tsid} + {row.dateTime} + {row.value ?? ""} + {row.qualityCode} + {row.textValue} + + ))} + +
    + {filteredRows.length > maxPreviewRows ? ( +
    + Showing the first {maxPreviewRows.toLocaleString()} of{" "} + {filteredRows.length.toLocaleString()} selected rows. +
    + ) : null} +
    + + ) : null} +
    + ); +} + +export default CWMSDataUpload; +export { CWMSDataUpload }; diff --git a/lib/components/data/forms/inputs/__tests__/CWMSDataUpload.test.jsx b/lib/components/data/forms/inputs/__tests__/CWMSDataUpload.test.jsx new file mode 100644 index 00000000..a574f074 --- /dev/null +++ b/lib/components/data/forms/inputs/__tests__/CWMSDataUpload.test.jsx @@ -0,0 +1,88 @@ +import React from "react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { CWMSForm } from "../../CWMSForm"; +import CWMSDataUpload from "../CWMSDataUpload"; +import { CWMS_DATA_UPLOAD_HEADERS } from "../../helpers/dataUpload"; + +vi.mock("cwmsjs", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + TimeSeriesApi: class { + getTimeSeries = vi.fn(); + postTimeSeries = vi.fn().mockResolvedValue(undefined); + deleteTimeSeriesWithTimeSeries = vi.fn().mockResolvedValue(undefined); + }, + TextTimeSeriesApi: class { + postTimeSeriesText = vi.fn().mockResolvedValue(undefined); + deleteTimeSeriesTextWithName = vi.fn().mockResolvedValue(undefined); + }, + }; +}); + +const initialData = [ + CWMS_DATA_UPLOAD_HEADERS, + ["MVS", "TEST.Stage.Inst.1Hour.0.TEST", "2026-01-01 00:00", "10.25", "0", "", "note"], +]; + +const renderUpload = (onSubmit = vi.fn()) => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + render( + + + + + , + ); + return onSubmit; +}; + +describe("CWMSDataUpload", () => { + afterEach(() => vi.clearAllMocks()); + + it("renders parsed rows with Groundwork form controls", () => { + renderUpload(); + + expect(screen.getByText("Upload summary")).toBeTruthy(); + expect(screen.getAllByText("TEST.Stage.Inst.1Hour.0.TEST").length).toBeGreaterThan( + 0, + ); + expect(screen.getByText("10.25")).toBeTruthy(); + expect(screen.getByRole("button", { name: "Download template" }).disabled).toBe( + false, + ); + }); + + it("registers a batch payload with CWMSForm", async () => { + const onSubmit = renderUpload(); + fireEvent.click(screen.getByRole("button", { name: "Submit" })); + + await waitFor(() => expect(onSubmit).toHaveBeenCalled()); + expect(onSubmit.mock.calls[0][0][0]).toMatchObject({ + kind: "timeseries-batch", + unit: "ft", + rows: [ + expect.objectContaining({ + office: "MVS", + tsid: "TEST.Stage.Inst.1Hour.0.TEST", + value: 10.25, + }), + ], + }); + }); +}); diff --git a/lib/components/data/hooks/useCwmsDataUpload.js b/lib/components/data/hooks/useCwmsDataUpload.js new file mode 100644 index 00000000..1bf59a24 --- /dev/null +++ b/lib/components/data/hooks/useCwmsDataUpload.js @@ -0,0 +1,163 @@ +import { useContext, useMemo } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Configuration, TextTimeSeriesApi, TimeSeriesApi } from "cwmsjs"; +import { AuthContext } from "../utilities/auth/AuthContext"; +import useCdaUrl from "../utilities/useCdaUrl"; +import { + classifyCwmsDataUploadRows, + filterCwmsDataUploadRows, +} from "../forms/helpers/dataUpload"; + +function useCwmsDataUpload({ + model, + filter = "all", + cdaUrl, + unit = "ft", + loadExistingData = true, + onDeleteSuccess, + onDeleteError, +} = {}) { + const providedCdaUrl = useCdaUrl(); + const resolvedCdaUrl = cdaUrl ?? providedCdaUrl; + const auth = useContext(AuthContext); + const queryClient = useQueryClient(); + + const config = useMemo(() => { + const headers = { accept: "application/json;version=2" }; + if (auth?.token) headers.Authorization = `Bearer ${auth.token}`; + return new Configuration({ + basePath: resolvedCdaUrl, + headers, + credentials: "include", + }); + }, [auth?.token, resolvedCdaUrl]); + + const timeSeriesApi = useMemo(() => new TimeSeriesApi(config), [config]); + const textTimeSeriesApi = useMemo(() => new TextTimeSeriesApi(config), [config]); + const queryKey = [ + "cda", + "data-upload", + model?.office, + model?.tsid, + model?.begin, + model?.end, + unit, + ]; + + const existingQuery = useQuery({ + queryKey, + enabled: Boolean( + loadExistingData && model?.office && model?.tsid && model?.begin && model?.end, + ), + queryFn: async () => { + try { + return await timeSeriesApi.getTimeSeries({ + name: model.tsid, + office: model.office, + begin: model.begin, + end: model.end, + unit, + timezone: "UTC", + pageSize: 1000000, + }); + } catch (error) { + if (error?.response?.status === 404) { + return { + name: model.tsid, + officeId: model.office, + units: unit, + values: [], + }; + } + throw error; + } + }, + }); + + const classifiedRows = useMemo( + () => + classifyCwmsDataUploadRows(model?.rows || [], existingQuery.data?.values || []), + [existingQuery.data?.values, model?.rows], + ); + const filteredRows = useMemo( + () => filterCwmsDataUploadRows(classifiedRows, filter), + [classifiedRows, filter], + ); + + const deleteMutation = useMutation({ + mutationFn: async (rows) => { + if (!rows?.length) throw new Error("Select at least one row to delete."); + + const groups = new Map(); + rows.forEach((row) => { + const key = `${row.office}|${row.tsid}`; + const current = groups.get(key) || { + office: row.office, + tsid: row.tsid, + begin: row.timestamp, + end: row.timestamp, + hasNumeric: false, + hasText: false, + }; + if (row.epoch < new Date(current.begin).getTime()) + current.begin = row.timestamp; + if (row.epoch > new Date(current.end).getTime()) current.end = row.timestamp; + current.hasNumeric ||= row.value !== null && row.value !== undefined; + current.hasText ||= Boolean(row.textValue); + groups.set(key, current); + }); + + const operations = []; + groups.forEach((group) => { + if (group.hasNumeric) { + operations.push( + timeSeriesApi.deleteTimeSeriesWithTimeSeries({ + timeseries: group.tsid, + office: group.office, + begin: group.begin, + end: group.end, + timezone: "UTC", + startTimeInclusive: true, + endTimeInclusive: true, + overrideProtection: true, + }), + ); + } + if (group.hasText) { + operations.push( + textTimeSeriesApi.deleteTimeSeriesTextWithName({ + name: group.tsid, + office: group.office, + textMask: "*", + begin: group.begin, + end: group.end, + timezone: "UTC", + }), + ); + } + }); + await Promise.all(operations); + return { deletedRows: rows.length, operations: operations.length }; + }, + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey }); + onDeleteSuccess?.(result); + }, + onError: onDeleteError, + }); + + return { + classifiedRows, + filteredRows, + existingData: existingQuery.data, + isLoadingExisting: existingQuery.isLoading || existingQuery.isFetching, + existingError: existingQuery.error, + refreshExisting: existingQuery.refetch, + deleteRows: deleteMutation.mutateAsync, + isDeleting: deleteMutation.isPending, + deleteError: deleteMutation.error, + }; +} + +export { useCwmsDataUpload }; +export default useCwmsDataUpload; diff --git a/lib/index.jsx b/lib/index.jsx index 1ceb760e..c89ac1f2 100644 --- a/lib/index.jsx +++ b/lib/index.jsx @@ -19,6 +19,7 @@ import DataStatus from "./components/data/summary/DataStatus"; // Import input components import CWMSInput from "./components/data/forms/inputs/CWMSInput"; import CWMSFileUpload from "./components/data/forms/inputs/CWMSFileUpload"; +import CWMSDataUpload from "./components/data/forms/inputs/CWMSDataUpload"; import CWMSTextarea from "./components/data/forms/inputs/CWMSTextarea"; import CWMSCheckboxes from "./components/data/forms/inputs/CWMSCheckboxes"; import CWMSRadioGroup from "./components/data/forms/inputs/CWMSRadioGroup"; @@ -55,6 +56,7 @@ import useCdaOffices from "./components/data/hooks/useCdaOffices"; import useNwpsGauge from "./components/data/hooks/useNwpsGauge"; import useNwpsGaugeData from "./components/data/hooks/useNwpsGaugeData"; import useDataStatusFile from "./components/data/hooks/useDataStatusFile"; +import useCwmsDataUpload from "./components/data/hooks/useCwmsDataUpload"; import { fetchCdaLevelTimeSeries, fetchCdaLevelValues, @@ -66,6 +68,16 @@ import useCdaBlobs from "./components/data/hooks/useCdaBlobs"; // Utility Hooks import useDebounce from "./components/data/utilities/useDebounce"; +import { + CWMS_DATA_UPLOAD_HEADERS, + CwmsDataUploadValidationError, + buildCwmsDataUploadPayloads, + classifyCwmsDataUploadRows, + createCwmsDataUploadTemplate, + filterCwmsDataUploadRows, + parseCwmsDataUploadRows, + readCwmsDataUploadFile, +} from "./components/data/forms/helpers/dataUpload"; // Utility Functions import { PRECISION_BY_UNIT, @@ -112,9 +124,18 @@ export { useCdaTimeSeriesGroup, useCdaOffices, useDataStatusFile, + useCwmsDataUpload, fetchCdaLevelTimeSeries, fetchCdaLevelValues, useDebounce, + CWMS_DATA_UPLOAD_HEADERS, + CwmsDataUploadValidationError, + buildCwmsDataUploadPayloads, + classifyCwmsDataUploadRows, + createCwmsDataUploadTemplate, + filterCwmsDataUploadRows, + parseCwmsDataUploadRows, + readCwmsDataUploadFile, useNwpsGauge, useNwpsGaugeData, AuthProvider, @@ -127,6 +148,7 @@ export { // Input components CWMSInput, CWMSFileUpload, + CWMSDataUpload, CWMSTextarea, CWMSCheckboxes, CWMSRadioGroup, diff --git a/package-lock.json b/package-lock.json index fcc8539b..aea417e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { "name": "@usace-watermanagement/groundwork-water", - "version": "3.11.0", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@usace-watermanagement/groundwork-water", - "version": "3.11.0", + "version": "4.0.0", "license": "MIT", "dependencies": { "@tanstack/react-virtual": "^3.14.6", "cwmsjs": "^2.3.0-2024.12.10", "dayjs": "^1.11.11", "deepmerge": "^4.3.1", + "exceljs": "^4.4.0", "oidc-client-ts": "^3.5.0", "ol": "^10.0.0", "plotly.js-basic-dist": "^2.33.0", @@ -866,9 +867,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -927,6 +928,47 @@ } } }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, "node_modules/@floating-ui/core": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", @@ -1023,9 +1065,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -1562,9 +1604,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1582,9 +1621,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1602,9 +1638,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1622,9 +1655,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1642,9 +1672,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1662,9 +1689,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1899,6 +1923,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~8.3.0" + } + }, "node_modules/@types/ol": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/ol/-/ol-7.0.0.tgz", @@ -2780,6 +2816,81 @@ "node": ">= 8" } }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -2966,7 +3077,6 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, "license": "MIT" }, "node_modules/async-function": { @@ -3036,7 +3146,26 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT" }, "node_modules/baseline-browser-mapping": { @@ -3075,6 +3204,28 @@ "require-from-string": "^2.0.2" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3087,10 +3238,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -3154,6 +3322,56 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -3254,6 +3472,18 @@ "node": ">=18" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3398,11 +3628,25 @@ "dev": true, "license": "MIT" }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -3412,6 +3656,37 @@ "dev": true, "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3707,6 +3982,51 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/earcut": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", @@ -3739,6 +4059,15 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -4123,9 +4452,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -4200,9 +4529,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -4393,6 +4722,38 @@ "dev": true, "license": "MIT" }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/exceljs/node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -4410,6 +4771,19 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4627,6 +5001,12 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -4646,7 +5026,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -4663,6 +5042,35 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4878,7 +5286,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -4908,10 +5315,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -4922,7 +5328,6 @@ "version": "3.1.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -5012,7 +5417,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -5188,6 +5592,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -5198,6 +5622,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -5240,7 +5670,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -5251,7 +5680,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/internal-nav-helper": { @@ -5940,6 +6368,60 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/jwt-decode": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", @@ -5959,6 +6441,54 @@ "json-buffer": "3.0.1" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/lerc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", @@ -5979,6 +6509,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", @@ -6122,9 +6661,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6146,9 +6682,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6170,9 +6703,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6194,9 +6724,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6305,6 +6832,12 @@ "node": ">=20" } }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, "node_modules/listr2": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", @@ -6336,6 +6869,73 @@ "node": ">=8" } }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6350,6 +6950,18 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -6545,6 +7157,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -6554,6 +7175,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -6812,7 +7445,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -7017,7 +7649,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7408,6 +8039,12 @@ "license": "MIT", "peer": true }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7614,6 +8251,50 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -7879,6 +8560,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -8006,6 +8707,12 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8217,6 +8924,15 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -8613,6 +9329,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/term-size": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", @@ -8749,6 +9481,15 @@ "dev": true, "license": "MIT" }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8787,6 +9528,15 @@ "node": ">=20" } }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", @@ -8983,6 +9733,15 @@ "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -9002,6 +9761,60 @@ "node": ">=18" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -9059,6 +9872,16 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", @@ -9568,7 +10391,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/xml-name-validator": { @@ -9591,7 +10413,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, "license": "MIT" }, "node_modules/yallist": { @@ -9640,6 +10461,41 @@ "numcodecs": "^0.3.2" } }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/package.json b/package.json index 5fb63e49..23018d73 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,7 @@ "cwmsjs": "^2.3.0-2024.12.10", "dayjs": "^1.11.11", "deepmerge": "^4.3.1", + "exceljs": "^4.4.0", "oidc-client-ts": "^3.5.0", "ol": "^10.0.0", "plotly.js-basic-dist": "^2.33.0",