From 9b1f30c3795a83da342ecfd5417cf07eed98915e Mon Sep 17 00:00:00 2001 From: Charles Graham SWT Date: Mon, 24 Feb 2025 16:40:06 -0600 Subject: [PATCH 01/13] Add library base for status summary --- lib/components/data/summary/DataStatus.jsx | 75 ++++++++++++ .../data/summary/components/StatusRow.jsx | 113 ++++++++++++++++++ .../data/utilities/qualityDecoder.tsx | 44 +++++++ lib/css/alert.css | 29 +++++ lib/index.jsx | 28 +++-- 5 files changed, 276 insertions(+), 13 deletions(-) create mode 100644 lib/components/data/summary/DataStatus.jsx create mode 100644 lib/components/data/summary/components/StatusRow.jsx create mode 100644 lib/components/data/utilities/qualityDecoder.tsx create mode 100644 lib/css/alert.css diff --git a/lib/components/data/summary/DataStatus.jsx b/lib/components/data/summary/DataStatus.jsx new file mode 100644 index 0000000..1e3a9cd --- /dev/null +++ b/lib/components/data/summary/DataStatus.jsx @@ -0,0 +1,75 @@ + +import { useQuery } from "@tanstack/react-query"; +import { + UsaceBox, Badge, Text, + Table, TableBody, TableRow, TableHead, TableCell, +} from "@usace/groundwork"; + +import "../../../css/alert.css"; +import StatusRow from "./components/StatusRow"; + +function DataStatus({ office, pageSize, cdaUrl, dataStatusUrl, linkPath, tsids = [], lookBackHours = 24, dateFormat = "DD MMM HH:mm" }) { + // fetch the data status file from URL and parse it new line delimited + + const { data: fileTsids = [], isPending: fileIsPending, error: fileError } = useQuery({ + queryKey: ["dataStatus", dataStatusUrl], + queryFn: async () => { + return fetch(dataStatusUrl) + .then(response => { + if (response.ok) + return response.text() + else + return Promise.reject(response.statusText) + }) + .then(text => { + // Split by new lines and filter out lines starting with ":" (commented) + return text.split("\n").filter(line => !line.startsWith(":")) + }) + }, + refetchInterval: 60 * 1000, + refetchOnWindowFocus: false, + enabled: dataStatusUrl != null, + }) + + if (fileTsids.length > 0) { + tsids = [...tsids, ...fileTsids] + } + + if (!dataStatusUrl && !tsids) { + console.error("Error: No data status URL or tsids provided to component ") + return Error: No data status URL or tsids provided + } + + return ( + + Data Quality Flags are shown as: + Missing + Questionable + Unknown or Undefined + Passed Screening and/or Validated + + {/* Build list of dates for column headers */} + + + Gage + Quality Info + Latest Date-time + + + + {tsids.map((name, idx) => { + if (name) { + return ( + + ) + } + return null + })} + +
+
+ ) +} + +export default DataStatus; +export { DataStatus }; \ No newline at end of file diff --git a/lib/components/data/summary/components/StatusRow.jsx b/lib/components/data/summary/components/StatusRow.jsx new file mode 100644 index 0000000..a035284 --- /dev/null +++ b/lib/components/data/summary/components/StatusRow.jsx @@ -0,0 +1,113 @@ +import { Text, TextLink, TableRow, TableCell, Skeleton } from "@usace/groundwork"; +import dayjs from "dayjs"; +import { useCdaConfig } from "../../helpers/cda"; +import { TimeSeriesApi } from "cwmsjs"; +import { useQuery } from "@tanstack/react-query"; +import getQualityStr from "../../utilities/qualityDecoder"; +import { useLayoutEffect, useRef, useState } from "react"; + + +export default function StatusRow({ office, linkPath, name, pageSize, lookBackHours, cdaUrl, dateFormat }) { + // append fileTsids to tsids + const config = useCdaConfig("v2", cdaUrl); + const tsApi = new TimeSeriesApi(config); + const lookBack = dayjs().subtract(lookBackHours, "hour") + let position = 0 + const [statusDelta, setStatusDelta] = useState(null) + + const { data: tsData, error: tsError, isPending: tsPending } = useQuery({ + queryKey: ["dataStatusTS", name, dayjs().format("YYYY-MM-DDTHH:mm:ss")], + queryFn: async () => { + return tsApi.getTimeSeries({ + office, + name: name, + begin: dayjs(lookBack).format("YYYY-MM-DDTHH:mm:ssZZ"), + end: dayjs().format("YYYY-MM-DDTHH:mm:ssZZ"), + pageSize + }).then(data => { + let returnData = { name: data?.name, values: [null, [null]] } + if (data?.name && data?.values) { + + if (data.values.length > 0) { + + let quality_array = [] + let dt = new Date(0) + + // Catch missing data from lookback + const first_dt = data.values?.[0]?.[0] + if (first_dt > lookBack.valueOf()) { + const missing_count = dayjs(first_dt).diff(lookBack, "hours", true) * 4 + for (let i = 0; i < missing_count; i++) { + quality_array.push("missing") + } + } + + // Read response + data.values.map(val => { + // Latest Date-time + if (val[0] > dt && val[1] != null) { + dt = val[0] + } + // Quality + let quality = getQualityStr(val[2])?.toLowerCase() + if (val[1] === null) { quality = "missing" } + quality_array.push(quality) + }) + returnData.values = [dt, [quality_array]] + } + } + return returnData + }) + }, + refetchInterval: 60 * 1000, + refetchOnWindowFocus: true, + enabled: name != null + }) + const cellRef = useRef(null); + + useLayoutEffect(() => { + if (tsData?.values?.[1]?.[0]) + setStatusDelta(Math.round(cellRef.current?.getBoundingClientRect()?.width / tsData?.values[1][0].length)); + }, [tsPending]) + if (tsError) { + return Error: {tsError.message} + } + return ( + {tsPending ? : + <> + + {linkPath ? + {`${name.split(".")[0]} ${name.split(".")[1]}`} + : `${name.split(".")[0]} ${name.split(".")[1]}` + } + + + + {(tsData?.values[1][0]) ? + tsData?.values[1][0].map((val, idx) => { + console.log({ idx }) + return ( + + ) + }) : null} + + + + {(tsData?.values?.[0]) ? dayjs(tsData.values[0]).format(dateFormat) : "Missing"} + + + } + + ) + +} \ No newline at end of file diff --git a/lib/components/data/utilities/qualityDecoder.tsx b/lib/components/data/utilities/qualityDecoder.tsx new file mode 100644 index 0000000..05cefd2 --- /dev/null +++ b/lib/components/data/utilities/qualityDecoder.tsx @@ -0,0 +1,44 @@ +// Author: @stephenkissock + +const QUALITY_CODES: { [key: number]: string } = { + 0: "UNKNOWN", + 1: "UNKNOWN", + 3: "OKAY", + 5: "MISSING", + 9: "QUESTIONABLE", + 17: "REJECTED", + 33: "UNKNOWN", + 35: "OKAY", + 37: "MISSING", + 41: "QUESTIONABLE", + 49: "REJECTED", + 65: "UNKNOWN", + 67: "OKAY", + 69: "MISSING", + 73: "QUESTIONABLE", + 81: "REJECTED", + 97: "UNKNOWN", + 99: "OKAY", + 101: "MISSING", + 105: "QUESTIONABLE", + 113: "REJECTED", + 2433: "UNKNOWN", + 2435: "OKAY", + 2437: "MISSING", + 2441: "QUESTIONABLE", + 2449: "REJECTED", + 2465: "UNKNOWN", + 2467: "OKAY", + 2469: "MISSING", + 2473: "QUESTIONABLE", + 2481: "REJECTED", + 2497: "UNKNOWN", +} + +function getQualityStr(quality_int: number) { + return QUALITY_CODES?.[quality_int] +} + +export default getQualityStr +export { getQualityStr } +export type { QUALITY_CODES } \ No newline at end of file diff --git a/lib/css/alert.css b/lib/css/alert.css new file mode 100644 index 0000000..21dbd2d --- /dev/null +++ b/lib/css/alert.css @@ -0,0 +1,29 @@ +/* ----------------- */ +/* DCP Status Colors */ +/* ----------------- */ + +.okay { + stroke: green; + background-color: #7fbf7f; +} + +.missing { + stroke: black; + background-color: #7f7f7f; + color: white; +} + +.questionable { + stroke: yellow; + background-color: #ffff7f; +} + +.unknown { + stroke: magenta; + background-color: #ff7fff; +} + +.undefined { + stroke: magenta; + background-color: #ff7fff; +} diff --git a/lib/index.jsx b/lib/index.jsx index f843295..c4f5adb 100644 --- a/lib/index.jsx +++ b/lib/index.jsx @@ -18,21 +18,23 @@ import useCdaTimeSeries from "./components/data/hooks/useCdaTimeSeries"; import useCdaTimeSeriesGroup from "./components/data/hooks/useCdaTimeSeriesGroup"; import useNwpsGauge from "./components/data/hooks/useNwpsGauge"; import useNwpsGaugeData from "./components/data/hooks/useNwpsGaugeData"; +import DataStatus from "./components/data/summary/DataStatus"; // import { helperFunction } from './utils/helpers'; export { - TSTable, - CWMSTable, - GageMap, - CWMSPlot, - CdaLatestValueCard, - CdaUrlProvider, - useCdaCatalog, - useCdaLatestValue, - useCdaLocation, - useCdaTimeSeries, - useCdaTimeSeriesGroup, - useNwpsGauge, - useNwpsGaugeData, + TSTable, + CWMSTable, + GageMap, + CWMSPlot, + CdaLatestValueCard, + CdaUrlProvider, + DataStatus, + useCdaCatalog, + useCdaLatestValue, + useCdaLocation, + useCdaTimeSeries, + useCdaTimeSeriesGroup, + useNwpsGauge, + useNwpsGaugeData, }; // export { helperFunction }; From db84c3325674479cdea75f9e13dade6699428b82 Mon Sep 17 00:00:00 2001 From: Charles Graham SWT Date: Mon, 24 Feb 2025 16:40:39 -0600 Subject: [PATCH 02/13] Add docs and initial tests --- docs/public/data/summary/swt.datastatus | 35 ++++++++++ docs/src/bundles/route-bundle.js | 2 + docs/src/pages/docs/summary/data-status.jsx | 77 +++++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 docs/public/data/summary/swt.datastatus create mode 100644 docs/src/pages/docs/summary/data-status.jsx diff --git a/docs/public/data/summary/swt.datastatus b/docs/public/data/summary/swt.datastatus new file mode 100644 index 0000000..884a5d1 --- /dev/null +++ b/docs/public/data/summary/swt.datastatus @@ -0,0 +1,35 @@ +ATOK.Elev.Inst.30Minutes.0.Decodes-Raw +KEYS.Elev.Inst.30Minutes.0.Ccp-Rev +KEYS.Precip-Inc.Total.1Hour.1Hour.Ccp-Rev +: OOLO.Elev.Inst.30Minutes.0.Ccp-Rev +: OOLO.Precip-Inc.Total.1Hour.1Hour.Ccp-Rev +: DENI.Elev.Inst.1Hour.0.Ccp-Rev +: DENI.Precip-Inc.Total.1Hour.1Hour.Ccp-Rev +: JOHN.Elev.Inst.1Hour.0.Ccp-Rev +: JOHN.Precip-Inc.Total.1Hour.1Hour.Ccp-Rev +: INDX.Stage.Inst.1Hour.0.Ccp-Rev +: FLOR.Stage.Inst.1Hour.0.Ccp-Rev +: JOPL.Stage.Inst.1Hour.0.Ccp-Rev +: UNGE.Stage.Inst.1Hour.0.Ccp-Rev +: BART.Stage.Inst.1Hour.0.Ccp-Rev +: RALS.Stage.Inst.1Hour.0.Ccp-Rev +: DUND.Stage.Inst.1Hour.0.Ccp-Rev +: FRIT.Precip-Cuml.Inst.1Hour.0.Ccp-Rev +: AMES.Stage.Inst.1Hour.0.Ccp-Rev +: CLDY.Stage.Inst.1Hour.0.Ccp-Rev +ALTU.Precip-Inc.Total.15Minutes.15Minutes.Ccp-Rev +CLRK.Precip-Inc.Total.15Minutes.15Minutes.Ccp-Rev +: FSUP.Precip-Inc.Total.30Minutes.30Minutes.Ccp-Rev +: PENS.Precip-Inc.Total.1Hour.1Hour.Ccp-Rev +: THRA.Precip-Inc.Total.30Minutes.30Minutes.Ccp-Rev +: WOO2.Precip-Inc.Total.30Minutes.30Minutes.Ccp-Rev +: ALTA.Flow.Inst.15Minutes.0.Ccp-Rev +: KANS.Flow.Inst.15Minutes.0.Ccp-Rev +: PECK.Flow.Inst.15Minutes.0.Ccp-Rev +: STIL.Flow.Inst.15Minutes.0.Ccp-Rev +: WETU.Flow.Inst.30Minutes.0.Ccp-Rev +: ARBU.Stor.Inst.30Minutes.0.Ccp-Rev +: DENI.Stor.Inst.30Minutes.0.Ccp-Rev +: MARI.Stor.Inst.1Hour.0.Ccp-Rev +: TENK.Stor.Inst.1Hour.0.Ccp-Rev +: WDMA.Stor.Inst.1Hour.0.Ccp-Rev diff --git a/docs/src/bundles/route-bundle.js b/docs/src/bundles/route-bundle.js index 1594028..33fa919 100644 --- a/docs/src/bundles/route-bundle.js +++ b/docs/src/bundles/route-bundle.js @@ -21,6 +21,7 @@ import DataHooks from "../pages/docs/hooks"; import HelpPage from "../pages/docs/help"; import CdaUrlProviderDocs from "../pages/docs/utilities/cda-url-provider"; import UtilitiesDocs from "../pages/docs/utilities"; +import DataStatus from "../pages/docs/summary/data-status"; export default createRouteBundle( { @@ -40,6 +41,7 @@ export default createRouteBundle( "/docs/plots/cwms-plot": CWMSPlotDocs, "/docs/maps": Maps, "/docs/tables": Tables, + "/docs/summary/data-status": DataStatus, "/docs/utilities": UtilitiesDocs, "/docs/utilities/cda-url-provider": CdaUrlProviderDocs, "/docs/react-query": ReactQuery, diff --git a/docs/src/pages/docs/summary/data-status.jsx b/docs/src/pages/docs/summary/data-status.jsx new file mode 100644 index 0000000..68e7e2b --- /dev/null +++ b/docs/src/pages/docs/summary/data-status.jsx @@ -0,0 +1,77 @@ + + +import { Text } from "@usace/groundwork"; +import ParamsTable from "../../components/params-table"; +import { Code } from "@usace/groundwork"; +import DocsPage from "../_docs-wrapper"; +import Divider from "../../components/divider"; +// import { DataStatus } from "@usace-watermanagement/groundwork-water" +import DataStatus from "../../../../../lib/components/data/summary/DataStatus"; + +const returnParams = [ + { + name: "data", + type: "object?", + desc: ( + <> + Undefined until the request has resolved, then an object containing the + response (if a valid response is received). The return object will vary + depending on the request endpoint, but response definitions for each CDA + endpoint are available in the{" "} + + CDA Swagger Docs + + . + + ), + }, + { + name: "isPending", + type: "boolean", + desc: "Is true until a query response is received, then false.", + }, + { + name: "error", + type: "object?", + desc: "Is null if no error has occurred, else an object containing the error.", + }, +]; + +function DataStatusPage() { + return ( + +
+ + The Data Status component uses quality flags and values from timeseries to + return a quick overview of data in the last number of hours that you specify. + It is not meant to be a replacement of the DataStatusSummary CWMS application, + but certainly can function as a substitute! + +
+ + +
+ Return Parameters -{" "} + {`const {...} = useQuery()`} +
+ + + A full list of the return parameters for useQuery hooks can be + referenced in the{" "} + + useQuery documentation + + . + +
+ ); +} + +export { DataStatusPage }; +export default DataStatusPage; From 9310c95d607a0d14295a0bdcd6899258c9e5f6e8 Mon Sep 17 00:00:00 2001 From: "Charles Graham, SWT" Date: Mon, 24 Feb 2025 21:52:03 -0600 Subject: [PATCH 03/13] Some components return undefined for their class name. Rename conflicting class until that can be fixed. --- lib/components/data/summary/DataStatus.jsx | 162 +++++++++++++-------- lib/css/alert.css | 32 ++-- 2 files changed, 117 insertions(+), 77 deletions(-) diff --git a/lib/components/data/summary/DataStatus.jsx b/lib/components/data/summary/DataStatus.jsx index 1e3a9cd..1118755 100644 --- a/lib/components/data/summary/DataStatus.jsx +++ b/lib/components/data/summary/DataStatus.jsx @@ -1,75 +1,115 @@ - import { useQuery } from "@tanstack/react-query"; import { - UsaceBox, Badge, Text, - Table, TableBody, TableRow, TableHead, TableCell, + UsaceBox, + Badge, + Text, + Table, + TableBody, + TableRow, + TableHead, + TableCell, } from "@usace/groundwork"; import "../../../css/alert.css"; import StatusRow from "./components/StatusRow"; -function DataStatus({ office, pageSize, cdaUrl, dataStatusUrl, linkPath, tsids = [], lookBackHours = 24, dateFormat = "DD MMM HH:mm" }) { - // fetch the data status file from URL and parse it new line delimited - - const { data: fileTsids = [], isPending: fileIsPending, error: fileError } = useQuery({ - queryKey: ["dataStatus", dataStatusUrl], - queryFn: async () => { - return fetch(dataStatusUrl) - .then(response => { - if (response.ok) - return response.text() - else - return Promise.reject(response.statusText) - }) - .then(text => { - // Split by new lines and filter out lines starting with ":" (commented) - return text.split("\n").filter(line => !line.startsWith(":")) - }) - }, - refetchInterval: 60 * 1000, - refetchOnWindowFocus: false, - enabled: dataStatusUrl != null, - }) +function DataStatus({ + office, + pageSize, + cdaUrl, + dataStatusUrl, + linkPath, + tsids = [], + lookBackHours = 24, + dateFormat = "DD MMM HH:mm", +}) { + // fetch the data status file from URL and parse it new line delimited - if (fileTsids.length > 0) { - tsids = [...tsids, ...fileTsids] - } + const { + data: fileTsids = [], + isPending: fileIsPending, + error: fileError, + } = useQuery({ + queryKey: ["dataStatus", dataStatusUrl], + queryFn: async () => { + return fetch(dataStatusUrl) + .then((response) => { + if (response.ok) return response.text(); + else return Promise.reject(response.statusText); + }) + .then((text) => { + // Split by new lines and filter out lines starting with ":" (commented) + return text.split("\n").filter((line) => !line.startsWith(":")); + }); + }, + refetchInterval: 60 * 1000, + refetchOnWindowFocus: false, + enabled: dataStatusUrl != null, + }); - if (!dataStatusUrl && !tsids) { - console.error("Error: No data status URL or tsids provided to component ") - return Error: No data status URL or tsids provided - } + if (fileTsids.length > 0) { + tsids = [...tsids, ...fileTsids]; + } + if (!dataStatusUrl && !tsids) { + console.error( + "Error: No data status URL or tsids provided to component " + ); return ( - - Data Quality Flags are shown as: - Missing - Questionable - Unknown or Undefined - Passed Screening and/or Validated - - {/* Build list of dates for column headers */} - - - Gage - Quality Info - Latest Date-time - - - - {tsids.map((name, idx) => { - if (name) { - return ( - - ) - } - return null - })} - -
-
- ) + Error: No data status URL or tsids provided + ); + } + + return ( + + Data Quality Flags are shown as: + + Missing + + + Questionable + + + Unknown or Undefined + + + Passed Screening and/or Validated + + + {/* Build list of dates for column headers */} + + + Gage + Quality Info + + Latest Date-time + + + + + {tsids.map((name, idx) => { + if (name) { + return ( + + ); + } + return null; + })} + +
+
+ ); } export default DataStatus; -export { DataStatus }; \ No newline at end of file +export { DataStatus }; diff --git a/lib/css/alert.css b/lib/css/alert.css index 21dbd2d..f0f9c0e 100644 --- a/lib/css/alert.css +++ b/lib/css/alert.css @@ -2,28 +2,28 @@ /* DCP Status Colors */ /* ----------------- */ -.okay { - stroke: green; - background-color: #7fbf7f; +.alert-okay { + stroke: green; + background-color: #7fbf7f; } -.missing { - stroke: black; - background-color: #7f7f7f; - color: white; +.alert-missing { + stroke: black; + background-color: #7f7f7f; + color: white; } -.questionable { - stroke: yellow; - background-color: #ffff7f; +.alert-questionable { + stroke: yellow; + background-color: #ffff7f; } -.unknown { - stroke: magenta; - background-color: #ff7fff; +.alert-unknown { + stroke: magenta; + background-color: #ff7fff; } -.undefined { - stroke: magenta; - background-color: #ff7fff; +.alert-undefined { + stroke: magenta; + background-color: #ff7fff; } From 51b3354e2f90974cdfa52f3bc6281b9fc3050f78 Mon Sep 17 00:00:00 2001 From: "Charles Graham, SWT" Date: Mon, 24 Feb 2025 22:56:00 -0600 Subject: [PATCH 04/13] Add code example and finish updating props --- docs/src/pages/docs/summary/data-status.jsx | 187 +++++++++++++------- 1 file changed, 124 insertions(+), 63 deletions(-) diff --git a/docs/src/pages/docs/summary/data-status.jsx b/docs/src/pages/docs/summary/data-status.jsx index 68e7e2b..46bd234 100644 --- a/docs/src/pages/docs/summary/data-status.jsx +++ b/docs/src/pages/docs/summary/data-status.jsx @@ -1,76 +1,137 @@ - - -import { Text } from "@usace/groundwork"; +import { Badge, Text } from "@usace/groundwork"; import ParamsTable from "../../components/params-table"; -import { Code } from "@usace/groundwork"; import DocsPage from "../_docs-wrapper"; import Divider from "../../components/divider"; +import { Code } from "../../components/code"; // import { DataStatus } from "@usace-watermanagement/groundwork-water" import DataStatus from "../../../../../lib/components/data/summary/DataStatus"; const returnParams = [ - { - name: "data", - type: "object?", - desc: ( - <> - Undefined until the request has resolved, then an object containing the - response (if a valid response is received). The return object will vary - depending on the request endpoint, but response definitions for each CDA - endpoint are available in the{" "} - - CDA Swagger Docs - - . - - ), - }, - { - name: "isPending", - type: "boolean", - desc: "Is true until a query response is received, then false.", - }, - { - name: "error", - type: "object?", - desc: "Is null if no error has occurred, else an object containing the error.", - }, + { + name: "tsids", + type: "array", + desc: "An array of TimeSeries Identifiers to fetch data status for from CDA", + }, + { + name: "dataStatusUrl", + type: "string", + desc: "A DataStatusSummary file URL to fetch additional tsids from. Append to tsids prop. New line delimited. Comments start with ':'", + }, + { + name: "office", + type: "string", + required: true, + desc: "The office code for the data status", + }, + { + name: "pageSize", + type: "number", + desc: "The maximum number of entries to fetch in one date range", + }, + { + name: "cdaUrl", + type: "string", + desc: "The URL to the CDA service to fetch TimeSeries from. Defaults to: https://cwms-data.usace.army.mil/cwms-data", + }, + { + name: "linkPath", + type: "string", + desc: "A url path to a project or some other page. I.e. /{office}/projects/ would end up pointing to /{office}/projects/{name}", + }, + { + name: "lookBackHours", + type: "number", + desc: "Number of hours from current time to look back for data status", + }, + { + name: "dateFormat", + type: "string", + desc: ( + + A{" "} + + day.js + {" "} + format string for the date display in the table + + ), + }, + { + name: "showBadges", + type: "boolean", + desc: "A flag to determine if the badge color legend should be shown", + }, + { + name: "title", + type: "string", + desc: "The title of the Data Status component within UsaceBox", + }, ]; function DataStatusPage() { - return ( - -
- - The Data Status component uses quality flags and values from timeseries to - return a quick overview of data in the last number of hours that you specify. - It is not meant to be a replacement of the DataStatusSummary CWMS application, - but certainly can function as a substitute! - -
- - -
- Return Parameters -{" "} - {`const {...} = useQuery()`} -
- - - A full list of the return parameters for useQuery hooks can be - referenced in the{" "} - - useQuery documentation - - . - -
- ); + return ( + +
+ + The Data Status component uses quality flags and values from + timeseries to return a quick overview of data in the last number of + hours that you specify. It is not meant to be a replacement of the + DataStatusSummary CWMS application, but certainly can function as a + substitute! + +
+ + +
+ + {`import dayjs from "dayjs"; +import { useState } from "react"; + +/* swt.datastatus Contents : +: This is a comment +KEYS.Elev.Inst.1Hour.0.Ccp-Rev +ALTU.Precip-Inc.Total.15Minutes.15Minutes.Ccp-Rev +CLRK.Precip-Inc.Total.15Minutes.15Minutes.Ccp-Rev +: KEYS.Precip-Inc.Total.1Hour.1Hour.Ccp-Rev +*/ + +default export function Example() { + +} +`} + +
+
+ Data Status Parameters + {``} +
+ + + + You must specify either dataStatusUrl or tsids or the table will have no + values! + +
+ ); } export { DataStatusPage }; From 062d7f279f098de44eb0fe320c40ae70ac6358e4 Mon Sep 17 00:00:00 2001 From: "Charles Graham, SWT" Date: Mon, 24 Feb 2025 22:56:53 -0600 Subject: [PATCH 05/13] Add additional props to DataStatus --- lib/components/data/summary/DataStatus.jsx | 35 ++++++++++++---------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/lib/components/data/summary/DataStatus.jsx b/lib/components/data/summary/DataStatus.jsx index 1118755..d245640 100644 --- a/lib/components/data/summary/DataStatus.jsx +++ b/lib/components/data/summary/DataStatus.jsx @@ -22,6 +22,8 @@ function DataStatus({ tsids = [], lookBackHours = 24, dateFormat = "DD MMM HH:mm", + showBadges = true, + title = "Data Status", }) { // fetch the data status file from URL and parse it new line delimited @@ -42,7 +44,6 @@ function DataStatus({ return text.split("\n").filter((line) => !line.startsWith(":")); }); }, - refetchInterval: 60 * 1000, refetchOnWindowFocus: false, enabled: dataStatusUrl != null, }); @@ -61,20 +62,24 @@ function DataStatus({ } return ( - - Data Quality Flags are shown as: - - Missing - - - Questionable - - - Unknown or Undefined - - - Passed Screening and/or Validated - + + {showBadges && ( + <> + Data Quality Flags are shown as: + + Missing + + + Questionable + + + Unknown or Undefined + + + Passed Screening and/or Validated + + + )} {/* Build list of dates for column headers */} From f7d173c2532390023e2442f3c7f370d9bbe2da6c Mon Sep 17 00:00:00 2001 From: "Charles Graham, SWT" Date: Mon, 24 Feb 2025 23:00:45 -0600 Subject: [PATCH 06/13] Fix formatting --- .../data/summary/components/StatusRow.jsx | 244 ++++++++++-------- 1 file changed, 143 insertions(+), 101 deletions(-) diff --git a/lib/components/data/summary/components/StatusRow.jsx b/lib/components/data/summary/components/StatusRow.jsx index a035284..fd46c8e 100644 --- a/lib/components/data/summary/components/StatusRow.jsx +++ b/lib/components/data/summary/components/StatusRow.jsx @@ -1,4 +1,10 @@ -import { Text, TextLink, TableRow, TableCell, Skeleton } from "@usace/groundwork"; +import { + Text, + TextLink, + TableRow, + TableCell, + Skeleton, +} from "@usace/groundwork"; import dayjs from "dayjs"; import { useCdaConfig } from "../../helpers/cda"; import { TimeSeriesApi } from "cwmsjs"; @@ -6,108 +12,144 @@ import { useQuery } from "@tanstack/react-query"; import getQualityStr from "../../utilities/qualityDecoder"; import { useLayoutEffect, useRef, useState } from "react"; +export default function StatusRow({ + office, + linkPath, + name, + pageSize, + lookBackHours, + cdaUrl, + dateFormat, +}) { + // append fileTsids to tsids + const config = useCdaConfig("v2", cdaUrl); + const tsApi = new TimeSeriesApi(config); + const lookBack = dayjs().subtract(lookBackHours, "hour"); + let position = 0; + const [statusDelta, setStatusDelta] = useState(null); -export default function StatusRow({ office, linkPath, name, pageSize, lookBackHours, cdaUrl, dateFormat }) { - // append fileTsids to tsids - const config = useCdaConfig("v2", cdaUrl); - const tsApi = new TimeSeriesApi(config); - const lookBack = dayjs().subtract(lookBackHours, "hour") - let position = 0 - const [statusDelta, setStatusDelta] = useState(null) + const { + data: tsData, + error: tsError, + isPending: tsPending, + } = useQuery({ + queryKey: ["dataStatusTS", name, dayjs().format("YYYY-MM-DDTHH:mm:ss")], + queryFn: async () => { + return tsApi + .getTimeSeries({ + office, + name: name, + begin: dayjs(lookBack).format("YYYY-MM-DDTHH:mm:ssZZ"), + end: dayjs().format("YYYY-MM-DDTHH:mm:ssZZ"), + pageSize, + }) + .then((data) => { + let returnData = { name: data?.name, values: [null, [null]] }; + if (data?.name && data?.values) { + if (data.values.length > 0) { + let quality_array = []; + let dt = new Date(0); - const { data: tsData, error: tsError, isPending: tsPending } = useQuery({ - queryKey: ["dataStatusTS", name, dayjs().format("YYYY-MM-DDTHH:mm:ss")], - queryFn: async () => { - return tsApi.getTimeSeries({ - office, - name: name, - begin: dayjs(lookBack).format("YYYY-MM-DDTHH:mm:ssZZ"), - end: dayjs().format("YYYY-MM-DDTHH:mm:ssZZ"), - pageSize - }).then(data => { - let returnData = { name: data?.name, values: [null, [null]] } - if (data?.name && data?.values) { - - if (data.values.length > 0) { - - let quality_array = [] - let dt = new Date(0) - - // Catch missing data from lookback - const first_dt = data.values?.[0]?.[0] - if (first_dt > lookBack.valueOf()) { - const missing_count = dayjs(first_dt).diff(lookBack, "hours", true) * 4 - for (let i = 0; i < missing_count; i++) { - quality_array.push("missing") - } - } - - // Read response - data.values.map(val => { - // Latest Date-time - if (val[0] > dt && val[1] != null) { - dt = val[0] - } - // Quality - let quality = getQualityStr(val[2])?.toLowerCase() - if (val[1] === null) { quality = "missing" } - quality_array.push(quality) - }) - returnData.values = [dt, [quality_array]] - } + // Catch missing data from lookback + const first_dt = data.values?.[0]?.[0]; + if (first_dt > lookBack.valueOf()) { + const missing_count = + dayjs(first_dt).diff(lookBack, "hours", true) * 4; + for (let i = 0; i < missing_count; i++) { + quality_array.push("missing"); } - return returnData - }) - }, - refetchInterval: 60 * 1000, - refetchOnWindowFocus: true, - enabled: name != null - }) - const cellRef = useRef(null); + } - useLayoutEffect(() => { - if (tsData?.values?.[1]?.[0]) - setStatusDelta(Math.round(cellRef.current?.getBoundingClientRect()?.width / tsData?.values[1][0].length)); - }, [tsPending]) - if (tsError) { - return Error: {tsError.message} - } - return ( - {tsPending ? : - <> - - {linkPath ? - {`${name.split(".")[0]} ${name.split(".")[1]}`} - : `${name.split(".")[0]} ${name.split(".")[1]}` - } - - - - {(tsData?.values[1][0]) ? - tsData?.values[1][0].map((val, idx) => { - console.log({ idx }) - return ( - - ) - }) : null} - - - - {(tsData?.values?.[0]) ? dayjs(tsData.values[0]).format(dateFormat) : "Missing"} - - - } - - ) + // Read response + data.values.map((val) => { + // Latest Date-time + if (val[0] > dt && val[1] != null) { + dt = val[0]; + } + // Quality + let quality = getQualityStr(val[2])?.toLowerCase(); + if (val[1] === null) { + quality = "missing"; + } + quality_array.push(quality); + }); + returnData.values = [dt, [quality_array]]; + } + } + return returnData; + }); + }, + refetchInterval: 60 * 1000, + refetchOnWindowFocus: true, + enabled: name != null, + }); + const cellRef = useRef(null); -} \ No newline at end of file + useLayoutEffect(() => { + if (tsData?.values?.[1]?.[0]) + setStatusDelta( + Math.round( + cellRef.current?.getBoundingClientRect()?.width / + tsData?.values[1][0].length + ) + ); + }, [tsPending]); + if (tsError) { + return ( + + Error: {tsError.message} + + ); + } + return ( + + {tsPending ? ( + + + + ) : ( + <> + + {linkPath ? ( + + {`${name.split(".")[0]} ${name.split(".")[1]}`} + + ) : ( + `${name.split(".")[0]} ${name.split(".")[1]}` + )} + + + + {tsData?.values[1][0] + ? tsData?.values[1][0].map((val, idx) => { + console.log({ idx }); + return ( + + ); + }) + : null} + + + + {tsData?.values?.[0] + ? dayjs(tsData.values[0]).format(dateFormat) + : "Missing"} + + + )} + + ); +} From 660208c01ee88b721bddf87f6acc849e5cbc197b Mon Sep 17 00:00:00 2001 From: "Charles Graham, SWT" Date: Mon, 24 Feb 2025 23:01:00 -0600 Subject: [PATCH 07/13] Remove unused vars/imports --- .../data/summary/components/StatusRow.jsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lib/components/data/summary/components/StatusRow.jsx b/lib/components/data/summary/components/StatusRow.jsx index fd46c8e..3ed81ee 100644 --- a/lib/components/data/summary/components/StatusRow.jsx +++ b/lib/components/data/summary/components/StatusRow.jsx @@ -1,10 +1,4 @@ -import { - Text, - TextLink, - TableRow, - TableCell, - Skeleton, -} from "@usace/groundwork"; +import { TextLink, TableRow, TableCell, Skeleton } from "@usace/groundwork"; import dayjs from "dayjs"; import { useCdaConfig } from "../../helpers/cda"; import { TimeSeriesApi } from "cwmsjs"; @@ -25,7 +19,6 @@ export default function StatusRow({ const config = useCdaConfig("v2", cdaUrl); const tsApi = new TimeSeriesApi(config); const lookBack = dayjs().subtract(lookBackHours, "hour"); - let position = 0; const [statusDelta, setStatusDelta] = useState(null); const { @@ -79,8 +72,8 @@ export default function StatusRow({ return returnData; }); }, - refetchInterval: 60 * 1000, - refetchOnWindowFocus: true, + refetchOnWindowFocus: false, + retry: false, enabled: name != null, }); const cellRef = useRef(null); From bc13cd0eee4f5aabe5126a205d33d0d1aa2607de Mon Sep 17 00:00:00 2001 From: "Charles Graham, SWT" Date: Mon, 24 Feb 2025 23:08:20 -0600 Subject: [PATCH 08/13] Bump office to the top of the props since it's required --- docs/src/pages/docs/summary/data-status.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/pages/docs/summary/data-status.jsx b/docs/src/pages/docs/summary/data-status.jsx index 46bd234..a78133a 100644 --- a/docs/src/pages/docs/summary/data-status.jsx +++ b/docs/src/pages/docs/summary/data-status.jsx @@ -7,6 +7,12 @@ import { Code } from "../../components/code"; import DataStatus from "../../../../../lib/components/data/summary/DataStatus"; const returnParams = [ + { + name: "office", + type: "string", + required: true, + desc: "The office code for the data status", + }, { name: "tsids", type: "array", @@ -17,12 +23,6 @@ const returnParams = [ type: "string", desc: "A DataStatusSummary file URL to fetch additional tsids from. Append to tsids prop. New line delimited. Comments start with ':'", }, - { - name: "office", - type: "string", - required: true, - desc: "The office code for the data status", - }, { name: "pageSize", type: "number", From b02c290dd3ad8c5dccd8ac65870fd3a9d199ecb8 Mon Sep 17 00:00:00 2001 From: "Charles Graham, SWT" Date: Mon, 24 Feb 2025 23:08:39 -0600 Subject: [PATCH 09/13] Ensure the errors when CDA is down are shown in the table --- .../data/summary/components/StatusRow.jsx | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/lib/components/data/summary/components/StatusRow.jsx b/lib/components/data/summary/components/StatusRow.jsx index 3ed81ee..548329a 100644 --- a/lib/components/data/summary/components/StatusRow.jsx +++ b/lib/components/data/summary/components/StatusRow.jsx @@ -1,4 +1,10 @@ -import { TextLink, TableRow, TableCell, Skeleton } from "@usace/groundwork"; +import { + Badge, + TextLink, + TableRow, + TableCell, + Skeleton, +} from "@usace/groundwork"; import dayjs from "dayjs"; import { useCdaConfig } from "../../helpers/cda"; import { TimeSeriesApi } from "cwmsjs"; @@ -28,6 +34,7 @@ export default function StatusRow({ } = useQuery({ queryKey: ["dataStatusTS", name, dayjs().format("YYYY-MM-DDTHH:mm:ss")], queryFn: async () => { + let returnData = { name: null, values: [null, [null]] }; return tsApi .getTimeSeries({ office, @@ -37,7 +44,7 @@ export default function StatusRow({ pageSize, }) .then((data) => { - let returnData = { name: data?.name, values: [null, [null]] }; + returnData = { name: data?.name, values: [null, [null]] }; if (data?.name && data?.values) { if (data.values.length > 0) { let quality_array = []; @@ -70,11 +77,14 @@ export default function StatusRow({ } } return returnData; + }) + .catch((e) => { + return { ...returnData, response: e.response }; }); }, refetchOnWindowFocus: false, retry: false, - enabled: name != null, + enabled: name != null && office != null, }); const cellRef = useRef(null); @@ -87,10 +97,30 @@ export default function StatusRow({ ) ); }, [tsPending]); - if (tsError) { + if (!office) { return ( - Error: {tsError.message} + + Error + + No office provided + + ); + } + if (tsError || tsData?.response) { + const STATUS_CODE = tsData?.response?.status; + return ( + + + = 500 ? "red" : "yellow"}> + Error {STATUS_CODE} + + + +
+ {tsError?.message || tsData?.message || "CWMS Data API Unreachable"} +
+
); } From 419d4cbd271b0eb86e10464638aa4f6ae1b1cc71 Mon Sep 17 00:00:00 2001 From: Charles Graham SWT Date: Tue, 25 Feb 2025 16:28:42 -0600 Subject: [PATCH 10/13] Split the data status file fetch into a custom hook --- .../data/hooks/useDataStatusFile.ts | 31 +++++++++++++++++++ lib/components/data/summary/DataStatus.jsx | 29 ++--------------- lib/index.jsx | 2 ++ 3 files changed, 35 insertions(+), 27 deletions(-) create mode 100644 lib/components/data/hooks/useDataStatusFile.ts diff --git a/lib/components/data/hooks/useDataStatusFile.ts b/lib/components/data/hooks/useDataStatusFile.ts new file mode 100644 index 0000000..7d0424e --- /dev/null +++ b/lib/components/data/hooks/useDataStatusFile.ts @@ -0,0 +1,31 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + + +interface useDataStatusFileParams { + fileUrl: string; + queryOptions?: Partial>; +} + +const useDataStatusFile = ({ + fileUrl, + queryOptions, +}: useDataStatusFileParams) => { + + return useQuery({ + queryKey: ["dataStatus", fileUrl], + queryFn: async () => { + return fetch(fileUrl) + .then((response) => response.text()) + .then((text) => { + // Split by new lines and filter out lines starting with ":" (commented) + return text.split("\n").filter((line) => !line.startsWith(":")); + }); + }, + refetchOnWindowFocus: false, + ...queryOptions, + }); + +}; + +export { useDataStatusFile }; +export default useDataStatusFile; diff --git a/lib/components/data/summary/DataStatus.jsx b/lib/components/data/summary/DataStatus.jsx index d245640..9f8242d 100644 --- a/lib/components/data/summary/DataStatus.jsx +++ b/lib/components/data/summary/DataStatus.jsx @@ -1,4 +1,4 @@ -import { useQuery } from "@tanstack/react-query"; + import { UsaceBox, Badge, @@ -17,7 +17,6 @@ function DataStatus({ office, pageSize, cdaUrl, - dataStatusUrl, linkPath, tsids = [], lookBackHours = 24, @@ -27,32 +26,8 @@ function DataStatus({ }) { // fetch the data status file from URL and parse it new line delimited - const { - data: fileTsids = [], - isPending: fileIsPending, - error: fileError, - } = useQuery({ - queryKey: ["dataStatus", dataStatusUrl], - queryFn: async () => { - return fetch(dataStatusUrl) - .then((response) => { - if (response.ok) return response.text(); - else return Promise.reject(response.statusText); - }) - .then((text) => { - // Split by new lines and filter out lines starting with ":" (commented) - return text.split("\n").filter((line) => !line.startsWith(":")); - }); - }, - refetchOnWindowFocus: false, - enabled: dataStatusUrl != null, - }); - - if (fileTsids.length > 0) { - tsids = [...tsids, ...fileTsids]; - } - if (!dataStatusUrl && !tsids) { + if (!tsids) { console.error( "Error: No data status URL or tsids provided to component " ); diff --git a/lib/index.jsx b/lib/index.jsx index c4f5adb..f7dbf39 100644 --- a/lib/index.jsx +++ b/lib/index.jsx @@ -19,6 +19,7 @@ import useCdaTimeSeriesGroup from "./components/data/hooks/useCdaTimeSeriesGroup import useNwpsGauge from "./components/data/hooks/useNwpsGauge"; import useNwpsGaugeData from "./components/data/hooks/useNwpsGaugeData"; import DataStatus from "./components/data/summary/DataStatus"; +import useDataStatusFile from "./components/data/hooks/useDataStatusFile"; // import { helperFunction } from './utils/helpers'; export { @@ -29,6 +30,7 @@ export { CdaLatestValueCard, CdaUrlProvider, DataStatus, + useDataStatusFile, useCdaCatalog, useCdaLatestValue, useCdaLocation, From fe2596896ea87f7c3501bc7bc676da57c5eda983 Mon Sep 17 00:00:00 2001 From: Charles Graham SWT Date: Tue, 25 Feb 2025 16:29:37 -0600 Subject: [PATCH 11/13] Update the mapped quality decoder to handle null/missing values and unknown --- lib/components/data/utilities/qualityDecoder.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/components/data/utilities/qualityDecoder.tsx b/lib/components/data/utilities/qualityDecoder.tsx index 05cefd2..7adf743 100644 --- a/lib/components/data/utilities/qualityDecoder.tsx +++ b/lib/components/data/utilities/qualityDecoder.tsx @@ -35,8 +35,9 @@ const QUALITY_CODES: { [key: number]: string } = { 2497: "UNKNOWN", } -function getQualityStr(quality_int: number) { - return QUALITY_CODES?.[quality_int] +function getQualityStr(ts_value: (number | null)[]) { + if (ts_value?.[1] === null) return QUALITY_CODES[37] + return QUALITY_CODES?.[ts_value?.[2] ?? 0] } export default getQualityStr From b6c28be1f78a9236f5cee1855f2e0801e7fccd59 Mon Sep 17 00:00:00 2001 From: Charles Graham SWT Date: Tue, 25 Feb 2025 16:30:19 -0600 Subject: [PATCH 12/13] Remove the lookback code and data map in favor of using the quality codes directly/at the edge just before render --- .../data/summary/components/StatusRow.jsx | 60 ++++++++----------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/lib/components/data/summary/components/StatusRow.jsx b/lib/components/data/summary/components/StatusRow.jsx index 548329a..cdeafc0 100644 --- a/lib/components/data/summary/components/StatusRow.jsx +++ b/lib/components/data/summary/components/StatusRow.jsx @@ -47,35 +47,10 @@ export default function StatusRow({ returnData = { name: data?.name, values: [null, [null]] }; if (data?.name && data?.values) { if (data.values.length > 0) { - let quality_array = []; - let dt = new Date(0); - - // Catch missing data from lookback - const first_dt = data.values?.[0]?.[0]; - if (first_dt > lookBack.valueOf()) { - const missing_count = - dayjs(first_dt).diff(lookBack, "hours", true) * 4; - for (let i = 0; i < missing_count; i++) { - quality_array.push("missing"); - } - } - - // Read response - data.values.map((val) => { - // Latest Date-time - if (val[0] > dt && val[1] != null) { - dt = val[0]; - } - // Quality - let quality = getQualityStr(val[2])?.toLowerCase(); - if (val[1] === null) { - quality = "missing"; - } - quality_array.push(quality); - }); - returnData.values = [dt, [quality_array]]; + returnData.values = data?.values } } + returnData.values = data?.values; return returnData; }) .catch((e) => { @@ -89,11 +64,11 @@ export default function StatusRow({ const cellRef = useRef(null); useLayoutEffect(() => { - if (tsData?.values?.[1]?.[0]) + if (tsData?.values) setStatusDelta( Math.round( cellRef.current?.getBoundingClientRect()?.width / - tsData?.values[1][0].length + tsData?.values.length ) ); }, [tsPending]); @@ -145,21 +120,36 @@ export default function StatusRow({ - {tsData?.values[1][0] - ? tsData?.values[1][0].map((val, idx) => { - console.log({ idx }); + {tsData?.values + ? tsData?.values.map((val, idx) => { return ( { + // Setup a tooltip to show date - value when a user hovers with a visible line to show which value is highlighted + const tooltip = document.createElement("div"); + tooltip.className = "tooltip"; + tooltip.innerText = e.target.dataset.tooltip; + tooltip.style.position = "absolute"; + tooltip.style.top = `${e.clientY + 4}px`; + tooltip.style.left = `${e.clientX}px`; + document.body.appendChild(tooltip); + e.target.style.transform = "translateY(-4px)"; + }} + onMouseLeave={(e) => { + document.querySelector(".tooltip")?.remove(); + e.target.style.transform = "translateY(0)"; + }} key={`line-${name}-${idx}`} x1={Math.round(statusDelta * idx)} y1="0" x2={Math.round(statusDelta * idx)} y2="10" strokeWidth={statusDelta + "px"} - className={val} + className={`alert-${getQualityStr(val)?.toLowerCase()}`} + data-tooltip={dayjs(val[0]).format(dateFormat) + " " + val[1]?.toFixed(2)} /> ); }) @@ -168,7 +158,7 @@ export default function StatusRow({ {tsData?.values?.[0] - ? dayjs(tsData.values[0]).format(dateFormat) + ? dayjs(tsData.values[tsData.values.length - 1][0]).format(dateFormat) : "Missing"} From 295af244189b7f6f76b0c57803ee58af2312985f Mon Sep 17 00:00:00 2001 From: Charles Graham SWT Date: Tue, 25 Feb 2025 16:31:11 -0600 Subject: [PATCH 13/13] Format and update the docs to show the new useDataStatusFile hook. --- docs/src/pages/docs/summary/data-status.jsx | 223 ++++++++++---------- 1 file changed, 117 insertions(+), 106 deletions(-) diff --git a/docs/src/pages/docs/summary/data-status.jsx b/docs/src/pages/docs/summary/data-status.jsx index a78133a..ff903b4 100644 --- a/docs/src/pages/docs/summary/data-status.jsx +++ b/docs/src/pages/docs/summary/data-status.jsx @@ -5,96 +5,96 @@ import Divider from "../../components/divider"; import { Code } from "../../components/code"; // import { DataStatus } from "@usace-watermanagement/groundwork-water" import DataStatus from "../../../../../lib/components/data/summary/DataStatus"; +import { useDataStatusFile } from "@usace-watermanagement/groundwork-water"; const returnParams = [ - { - name: "office", - type: "string", - required: true, - desc: "The office code for the data status", - }, - { - name: "tsids", - type: "array", - desc: "An array of TimeSeries Identifiers to fetch data status for from CDA", - }, - { - name: "dataStatusUrl", - type: "string", - desc: "A DataStatusSummary file URL to fetch additional tsids from. Append to tsids prop. New line delimited. Comments start with ':'", - }, - { - name: "pageSize", - type: "number", - desc: "The maximum number of entries to fetch in one date range", - }, - { - name: "cdaUrl", - type: "string", - desc: "The URL to the CDA service to fetch TimeSeries from. Defaults to: https://cwms-data.usace.army.mil/cwms-data", - }, - { - name: "linkPath", - type: "string", - desc: "A url path to a project or some other page. I.e. /{office}/projects/ would end up pointing to /{office}/projects/{name}", - }, - { - name: "lookBackHours", - type: "number", - desc: "Number of hours from current time to look back for data status", - }, - { - name: "dateFormat", - type: "string", - desc: ( - - A{" "} - - day.js - {" "} - format string for the date display in the table - - ), - }, - { - name: "showBadges", - type: "boolean", - desc: "A flag to determine if the badge color legend should be shown", - }, - { - name: "title", - type: "string", - desc: "The title of the Data Status component within UsaceBox", - }, + { + name: "office", + type: "string", + required: true, + desc: "The office code for the data status", + }, + { + name: "tsids", + type: "array", + desc: "An array of TimeSeries Identifiers to fetch data status for from CDA", + }, + { + name: "pageSize", + type: "number", + desc: "The maximum number of entries to fetch in one date range", + }, + { + name: "cdaUrl", + type: "string", + desc: "The URL to the CDA service to fetch TimeSeries from. Defaults to: https://cwms-data.usace.army.mil/cwms-data", + }, + { + name: "linkPath", + type: "string", + desc: "A url path to a project or some other page. I.e. /{office}/projects/ would end up pointing to /{office}/projects/{name}", + }, + { + name: "lookBackHours", + type: "number", + desc: "Number of hours from current time to look back for data status", + }, + { + name: "dateFormat", + type: "string", + desc: ( + + A{" "} + + day.js + {" "} + format string for the date display in the table + + ), + }, + { + name: "showBadges", + type: "boolean", + desc: "A flag to determine if the badge color legend should be shown", + }, + { + name: "title", + type: "string", + desc: "The title of the Data Status component within UsaceBox", + }, ]; function DataStatusPage() { - return ( - -
- - The Data Status component uses quality flags and values from - timeseries to return a quick overview of data in the last number of - hours that you specify. It is not meant to be a replacement of the - DataStatusSummary CWMS application, but certainly can function as a - substitute! - -
- - -
- - {`import dayjs from "dayjs"; + const { data: fileTsids = [], error: fileError, isPending: filePending } = useDataStatusFile({ + fileUrl: "/data/summary/swt.datastatus", + }) + return ( + +
+ + The Data Status component uses quality flags and values from + timeseries to return a quick overview of data in the last number of + hours that you specify. It is not meant to be a replacement of the + DataStatusSummary CWMS application, but certainly can function as a + substitute! + +
+ {!filePending && fileError ?
Failed to load TSID data status file! {fileError?.message}
: } + +
+ + {`import dayjs from "dayjs"; import { useState } from "react"; +import { useDataStatusFile } from "@usace-watermanagement/groundwork-water"; /* swt.datastatus Contents : : This is a comment @@ -105,33 +105,44 @@ CLRK.Precip-Inc.Total.15Minutes.15Minutes.Ccp-Rev */ default export function Example() { - + } + if (fileError) { + return
Failed to load TSID data status file! {fileError?.message}
+ } + /* You could use a timeseries group to load tsids as well! */ + // Or use a static array below : + const tsids = ["KEYS.Elev.Inst.1Hour.0.Ccp-Rev"] + return } `} -
-
-
- Data Status Parameters - {``} -
- - - - You must specify either dataStatusUrl or tsids or the table will have no - values! - -
- ); +
+
+
+ Data Status Parameters + {``} +
+ + + + You must specify either dataStatusUrl or tsids or the table will have no + values! + +
+ ); } export { DataStatusPage };