= T extends new (...angs: any) => {
2 | $props: infer P
3 | }
4 | ? NonNullable
5 | : T extends (props: infer P, ...args: any) => any
6 | ? P
7 | : {}
8 |
9 | export type DeepRequired = Required<{
10 | [K in keyof T]: T[K] extends Required ? T[K] : DeepRequired
11 | }>
12 |
13 | export const arrayOfAll =
14 | () =>
15 | (
16 | array: U & ([T] extends [U[number]] ? unknown : 'Invalid') & { 0: T },
17 | ) =>
18 | array
19 |
--------------------------------------------------------------------------------
/src/components/analysis/AnalysisUnsupportedChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Unsupported chart type: {{ chart.type }}
5 |
6 |
7 |
8 |
9 |
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | .env
26 | /test-results/
27 | /playwright-report/
28 | /blob-report/
29 | /playwright/.cache/
30 | /test-results/
31 | /playwright-report/
32 | /blob-report/
33 | /playwright/.cache/
34 |
--------------------------------------------------------------------------------
/src/components/general/HighlightMatch.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ match.before }}
3 | {{ match.match }}
4 | {{ match.after }}
5 |
6 |
7 |
20 |
--------------------------------------------------------------------------------
/src/components/wms/panel/OverlayInformationPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mdi-layers-outline
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
1 | # .github/release.yml
2 |
3 | changelog:
4 | exclude:
5 | labels:
6 | - 'rel: ignore'
7 | categories:
8 | - title: New Features
9 | labels:
10 | - 'rel: new feature'
11 |
12 | - title: Improvements
13 | labels:
14 | - 'rel: improvement'
15 |
16 | - title: Fixes
17 | labels:
18 | - 'rel: fix'
19 |
20 | - title: Beta Features
21 | labels:
22 | - 'rel: beta'
23 |
24 | - title: Other Changes
25 | labels:
26 | - '*'
27 |
--------------------------------------------------------------------------------
/src/lib/products/utils.ts:
--------------------------------------------------------------------------------
1 | export function getFileExtension(url: string): string {
2 | const urlParts = url.toLowerCase().split('.')
3 | return urlParts[urlParts.length - 1]
4 | }
5 |
6 | export type ViewMode = 'html' | 'iframe' | 'img' | 'pdf'
7 | export function getViewMode(extension: string): ViewMode {
8 | switch (extension) {
9 | case 'html':
10 | return 'html'
11 | case 'png':
12 | case 'jpg':
13 | case 'jpeg':
14 | return 'img'
15 | case 'pdf':
16 | return 'pdf'
17 | default:
18 | return 'iframe'
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/views/DataDownloadDisplayView.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
21 |
--------------------------------------------------------------------------------
/src/lib/workflows/form.ts:
--------------------------------------------------------------------------------
1 | import { ScenarioData } from '@/lib/whatif'
2 |
3 | export function isBoundingBoxInFormData(data: ScenarioData): boolean {
4 | const properties = Object.keys(data)
5 | return (
6 | properties.includes('xMin') &&
7 | properties.includes('yMin') &&
8 | properties.includes('xMax') &&
9 | properties.includes('yMax')
10 | )
11 | }
12 |
13 | export function isCoordinateInFormData(data: ScenarioData): boolean {
14 | const properties = Object.keys(data)
15 | return properties.includes('latitude') && properties.includes('longitude')
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/webdisplay/WebDisplay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
24 |
--------------------------------------------------------------------------------
/src/components/analysis/AnalysisAddButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
27 |
--------------------------------------------------------------------------------
/src/stores/globalSearch.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 |
3 | export interface GlobalSearchItem {
4 | id: string
5 | title: string
6 | children?: GlobalSearchItem[]
7 | }
8 |
9 | interface GlobalSearchState {
10 | active: boolean
11 | type: 'locations' | 'parameters' | 'nodes'
12 | items: GlobalSearchItem[]
13 | selectedItems: string[]
14 | }
15 |
16 | const useGlobalSearchState = defineStore('globalSearchState', {
17 | state: (): GlobalSearchState => ({
18 | active: false,
19 | type: 'locations',
20 | items: [],
21 | selectedItems: [],
22 | }),
23 | })
24 |
25 | export { useGlobalSearchState }
26 |
--------------------------------------------------------------------------------
/src/views/auth/Logout.vue:
--------------------------------------------------------------------------------
1 | Redirecting after logout
2 |
3 |
19 |
--------------------------------------------------------------------------------
/docs/public/deployments/delftfews-sa/Modules/weboc/WEB-INF/web.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 | Delft-FEWS Web OC
8 |
9 |
10 | 404
11 | /index.html
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/assets/DefaultBaseMaps.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "automatic",
4 | "name": "Automatic",
5 | "icon": "mdi-theme-light-dark",
6 | "style": "automatic"
7 | },
8 | {
9 | "id": "light",
10 | "name": "CartoDB (light)",
11 | "icon": "mdi-weather-sunny",
12 | "style": "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
13 | "beforeId": "boundary_country_outline"
14 | },
15 | {
16 | "id": "dark",
17 | "name": "CartoDB (dark)",
18 | "icon": "mdi-weather-night",
19 | "style": "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
20 | "beforeId": "boundary_country_outline"
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/src/components/reports/tiptap/data.css:
--------------------------------------------------------------------------------
1 | .ProseMirror data {
2 | background-position: right;
3 | padding-right: 1.5em;
4 | outline: 2px solid #68cef8;
5 | outline-offset: 2px;
6 | border-radius: 2px;
7 | background-image: url('data:image/svg+xml,');
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/wms/ColourLegendTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | |
5 |
6 | |
7 | {{ item.label }} |
8 |
9 |
10 |
11 |
12 |
21 |
22 |
33 |
--------------------------------------------------------------------------------
/src/services/application-config/ApplicationConfig.ts:
--------------------------------------------------------------------------------
1 | import { VBtn } from 'vuetify/components'
2 |
3 | export enum RequestHeaderAuthorization {
4 | BEARER = 'Bearer',
5 | OFF = 'Off',
6 | }
7 |
8 | export type ApplicationConfig = {
9 | VITE_APP_NAME: string
10 | VITE_AUTH_AUTHORITY: string
11 | VITE_AUTH_ID: string
12 | VITE_AUTH_METADATA_URL: string
13 | VITE_AUTH_SCOPE: string
14 | VITE_FEWS_ARCHIVE_WEBSERVICES_URL: string
15 | VITE_FEWS_WEBSERVICES_URL: string
16 | VITE_LOGIN_BUTTON_PROPS: string | VBtn['$props']
17 | VITE_LOGIN_STYLESHEET_URL: string
18 | VITE_REQUEST_HEADER_AUTHORIZATION: RequestHeaderAuthorization
19 | VITE_I18N_LOCALE: string
20 | }
21 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # The Delft-FEWS Web OC Developers Documentation
2 |
3 | ## Definition Of Done
4 |
5 | Before merging a feature into the main branch, the following requirements have to be met:
6 |
7 | - All feature branches have to be reviewed and merged by another developer than the feature developer.
8 | - All unit tests and e2e tests have to succeed: npm run test:unit and test:e2e.
9 | - No lint errors are allowed: npm run lint
10 | - Sonarqube (https://sonarqube.deltares.nl/projects/) reports all A's.
11 | - Code coverage of libraries at least 80%.
12 | - Typescript types have to be used from the libraries.
13 | - Reusable vue components to web components.
14 | - Documentation up to date
15 |
--------------------------------------------------------------------------------
/src/components/reports/tiptap/CustomImage.ts:
--------------------------------------------------------------------------------
1 | import Image from '@tiptap/extension-image'
2 |
3 | export const CustomImage = Image.extend({
4 | addAttributes() {
5 | return {
6 | ...this.parent?.(),
7 |
8 | alt: {
9 | parseHTML: (element) => element.getAttribute('alt'),
10 | },
11 |
12 | src: {
13 | parseHTML: (element) => element.getAttribute('src'),
14 | },
15 |
16 | width: {
17 | parseHTML: (element) => element.getAttribute('width'),
18 | },
19 |
20 | height: {
21 | parseHTML: (element) => element.getAttribute('height'),
22 | },
23 | }
24 | },
25 | })
26 |
27 | export default CustomImage
28 |
--------------------------------------------------------------------------------
/src/lib/topology/componentSettings/mapSettings.ts:
--------------------------------------------------------------------------------
1 | import type { DeepRequired } from '@/lib/utils/types'
2 | import type { MapSettings as PiMapSettings } from '@deltares/fews-pi-requests'
3 |
4 | export type MapSettings = DeepRequired
5 |
6 | export const defaultMapSettings: MapSettings = {
7 | wmsLayer: {
8 | show: true, // TODO: Implement
9 | autoPlay: false, // TODO: Implement
10 | animateVectors: true, // TODO: Implement
11 | doubleClickAction: true,
12 | },
13 | locationsLayer: {
14 | show: true,
15 | locationNames: true, // TODO: Implement
16 | singleClickAction: true,
17 | locationSearchEnabled: true,
18 | },
19 | overlays: [],
20 | }
21 |
--------------------------------------------------------------------------------
/src/lib/timeseries/types/SeriesData.ts:
--------------------------------------------------------------------------------
1 | import type { TimeSeriesEvent } from '@deltares/fews-pi-requests'
2 |
3 | export interface SeriesData
4 | extends Pick {
5 | x: Date | number | null
6 | y: number | null
7 | }
8 |
9 | export interface SeriesArrayData
10 | extends Pick {
11 | x: Date | number | null
12 | y: (number | null)[]
13 | }
14 |
15 | export interface TimeSeriesData extends SeriesData {
16 | x: Date
17 | }
18 |
19 | export function isSeriesArrayData(
20 | data: SeriesData | SeriesArrayData,
21 | ): data is SeriesArrayData {
22 | return Array.isArray(data.y)
23 | }
24 |
--------------------------------------------------------------------------------
/src/lib/display/DisplayConfig.ts:
--------------------------------------------------------------------------------
1 | import { ChartConfig } from '../charts/types/ChartConfig.js'
2 | import { ActionPeriod, ActionRequest } from '@deltares/fews-pi-requests'
3 |
4 | export enum DisplayType {
5 | TimeSeriesChart = 'TimeSeriesChart',
6 | TimeSeriesTable = 'TimeSeriesTable',
7 | ElevationChart = 'ElevationChart',
8 | Information = 'Information',
9 | }
10 |
11 | export interface DisplayConfig {
12 | id: string
13 | nodeId: string | undefined
14 | plotId: string | undefined
15 | index: number | undefined
16 | title: string
17 | forecastLegend: string | undefined
18 | class: string
19 | requests: ActionRequest[]
20 | period: ActionPeriod | undefined
21 | subplots: ChartConfig[]
22 | }
23 |
--------------------------------------------------------------------------------
/src/plugins/vuetify.ts:
--------------------------------------------------------------------------------
1 | // Vuetify
2 | import { toHumanReadableDate } from '@/lib/date'
3 | import '@/styles/main.scss'
4 | import '@mdi/font/css/materialdesignicons.css'
5 | import { createVuetify } from 'vuetify'
6 | import { createVueI18nAdapter } from 'vuetify/locale/adapters/vue-i18n'
7 | import { i18n } from './i18n'
8 | import { useI18n } from 'vue-i18n'
9 |
10 | const vuetify = createVuetify({
11 | locale: {
12 | adapter: createVueI18nAdapter({ i18n, useI18n }),
13 | },
14 | defaults: {
15 | VBtn: {
16 | variant: 'text',
17 | },
18 | VDateInput: {
19 | displayFormat: toHumanReadableDate,
20 | placeholder: 'dd/mm/yyyy',
21 | },
22 | },
23 | })
24 |
25 | export default vuetify
26 |
--------------------------------------------------------------------------------
/src/lib/requests/transformRequest.ts:
--------------------------------------------------------------------------------
1 | import { authenticationManager } from '@/services/authentication/AuthenticationManager.ts'
2 |
3 | export function createTransformRequestFn(controller?: AbortController) {
4 | return async (request: Request): Promise => {
5 | return Promise.resolve(
6 | authenticationManager.transformRequestAuth(request, controller?.signal),
7 | )
8 | }
9 | }
10 |
11 | export function mergeHeaders(headers1: Headers, headers2: Headers): Headers {
12 | const mergedHeaders = new Headers()
13 | headers1.forEach((value, key) => {
14 | mergedHeaders.set(key, value)
15 | })
16 | headers2.forEach((value, key) => {
17 | mergedHeaders.set(key, value)
18 | })
19 | return mergedHeaders
20 | }
21 |
--------------------------------------------------------------------------------
/src/views/InformationDisplayView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | No information document configured
5 |
6 |
7 |
8 |
26 |
--------------------------------------------------------------------------------
/src/assets/JsonFormsConfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "vuetify": {
3 | "v-text-field": {
4 | "density": "compact",
5 | "variant": "outlined",
6 | "hide-details": "auto",
7 | "persistent-hint": true
8 | },
9 | "v-number-input": {
10 | "precision": 2,
11 | "density": "compact",
12 | "variant": "outlined",
13 | "hide-details": "auto",
14 | "persistent-hint": true
15 | },
16 | "v-container": {
17 | "class": "pa-0",
18 | "density": "compact"
19 | },
20 | "v-select": {
21 | "density": "compact",
22 | "variant": "outlined",
23 | "hide-details": "auto"
24 | },
25 | "v-col": {
26 | "class": "pa-2"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/lib/taskruns/utils.ts:
--------------------------------------------------------------------------------
1 | import { TaskRun } from './types'
2 |
3 | export function sortTasks(a: TaskRun, b: TaskRun): number {
4 | const hasDispatchTimeA = a.dispatchTimestamp !== null
5 | const hasDispatchTimeB = b.dispatchTimestamp !== null
6 | if (!hasDispatchTimeA && !hasDispatchTimeB) {
7 | // If both tasks are pending, sort by workflowId.
8 | return a.workflowId.localeCompare(b.workflowId)
9 | } else if (!hasDispatchTimeA) {
10 | // If A is pending and B is not, return A.
11 | return -1
12 | } else if (!hasDispatchTimeB) {
13 | // If B is pending and A is not, return B.
14 | return 1
15 | } else {
16 | // Otherwise, sort by timestamp.
17 | return b.dispatchTimestamp! - a.dispatchTimestamp!
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/wms/locations/LocationsCircleLayer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
28 |
--------------------------------------------------------------------------------
/src/lib/charts/getUniqueSeriesIds.ts:
--------------------------------------------------------------------------------
1 | import { uniq, uniqBy } from 'lodash-es'
2 | import type { ChartSeries } from './types/ChartSeries.js'
3 |
4 | export function getUniqueSeriesIds(series: ChartSeries[] | undefined) {
5 | // Some ChartSeries appear twice in the ChartConfig; once for a line and once for a marker.
6 | // Only one of these has to be included in the table.
7 | if (series === undefined) return []
8 | return uniq(
9 | series.filter((series) => series.visibleInTable).map((series) => series.id),
10 | )
11 | }
12 |
13 | export function getUniqueSeries(series: ChartSeries[] | undefined) {
14 | if (series === undefined) return []
15 | return uniqBy(
16 | series.filter((series) => series.visibleInTable),
17 | 'id',
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/date/convertFewsPiDateTimeToJsDate.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 |
3 | import { convertFewsPiDateTimeToJsDate } from '.'
4 |
5 | test('parses a FEWS PI date/time object to a Date object without timezone offset', () => {
6 | const fewsDatetime = { date: '2021-07-01', time: '12:00:00' }
7 | const result = convertFewsPiDateTimeToJsDate(fewsDatetime, 'Z')
8 | expect(result).toEqual(new Date('2021-07-01T12:00:00Z'))
9 | })
10 |
11 | test('parses a FEWS PI date/time object to a Date object with a timezone offset', () => {
12 | const fewsDatetime = { date: '2021-07-01', time: '12:00:00' }
13 | const result = convertFewsPiDateTimeToJsDate(fewsDatetime, '+02:00')
14 | expect(result).toEqual(new Date('2021-07-01T12:00:00+02:00'))
15 | })
16 |
--------------------------------------------------------------------------------
/src/stores/alerts.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 |
3 | export type AlertType = 'success' | 'error' | 'warning' | 'info'
4 |
5 | export interface Alert {
6 | id: string
7 | type: AlertType
8 | message: string
9 | }
10 |
11 | interface AlertState {
12 | alerts: Alert[]
13 | }
14 |
15 | const useAlertsStore = defineStore('alerts', {
16 | state: (): AlertState => ({
17 | alerts: [],
18 | }),
19 |
20 | actions: {
21 | addAlert(alert: Alert) {
22 | this.alerts.push(alert)
23 | },
24 | removeAlert(id: string) {
25 | this.alerts = this.alerts.filter((alert) => alert.id !== id)
26 | },
27 | },
28 |
29 | getters: {
30 | hasAlerts: (state) => state.alerts.length > 0,
31 | },
32 | })
33 |
34 | export { useAlertsStore }
35 |
--------------------------------------------------------------------------------
/src/components/thresholds/ThresholdSummaryChip.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 | {{ count }}
12 |
13 |
14 |
15 |
28 |
29 |
35 |
--------------------------------------------------------------------------------
/src/stores/locationNames.ts:
--------------------------------------------------------------------------------
1 | import { shallowRef } from 'vue'
2 | import { defineStore } from 'pinia'
3 | import { Location } from '@deltares/fews-pi-requests'
4 |
5 | export const useLocationNamesStore = defineStore('locationNames', () => {
6 | const locationNames = shallowRef