Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
54002f5
Resolves hydroserver2/hydroserver#371
daniel-slaugh Mar 24, 2026
73679ae
Mount app immediately then async load API data
daniel-slaugh Mar 25, 2026
eca7682
Merge pull request #383 from hydroserver2/375-async-load
daniel-slaugh Mar 25, 2026
6aaeb02
Merge pull request #382 from hydroserver2/371-site-edit-cancel
daniel-slaugh Mar 25, 2026
2241333
Merge branch 'main' into develop
Apr 30, 2026
a848a1a
feat: add QC session editing and history integration
Maurier Jun 22, 2026
cb16a23
fix: return values from router guards instead of next()
Maurier Jun 22, 2026
f2ef3ea
feat: show the data-point toggle in the select view
Maurier Jun 23, 2026
871b1c5
feat: rework QC editing around a managed-datastream chooser
Maurier Jun 23, 2026
fe28efd
feat: base new sessions on the latest committed state and track unsav…
Maurier Aug 3, 2026
a9030e2
fix: create managed datastreams with expand_related so the catalog ke…
Maurier Aug 3, 2026
3df35db
chore: update package lock
Maurier Aug 3, 2026
e7a7116
Merge remote-tracking branch 'origin/main' into 436-integrate-history…
Maurier Aug 3, 2026
f2f8e0f
feat: add session history navigation with per-operation notes and att…
Maurier Aug 3, 2026
3fc52bb
feat: add history snapshot comparison
Maurier Aug 5, 2026
c27a9bf
style: give session rows a container treatment
Maurier Aug 5, 2026
2931eed
docs: document history snapshot comparison
Maurier Aug 5, 2026
72a8028
feat: reload to a session's baseline from the Data loaded row
Maurier Aug 5, 2026
4507dc6
Merge branch 'release/v1.12.0' for test deployment.
Aug 12, 2026
f96cdb8
Update version
Aug 13, 2026
d4a69ca
feat: move session actions into the edit history panel
Maurier Aug 17, 2026
4608188
Merge remote-tracking branch 'origin/436-integrate-history-endpoints'…
Maurier Aug 17, 2026
cbf7787
Merge remote-tracking branch 'origin/main' into 436-integrate-history…
Maurier Aug 17, 2026
426150d
test: scope the unsaved-edits discard selector to the dialog
Maurier Aug 17, 2026
cee0974
Merge remote-tracking branch 'origin/release/v1.12.0' into 436-integr…
Maurier Aug 19, 2026
f46732f
fix: keep comment and attribution when replaying history
Maurier Aug 19, 2026
20d13c6
test: assert session reads send expand_related
Maurier Aug 19, 2026
29a4d2d
feat: delete a session together with the sessions built on it
Maurier Aug 19, 2026
caccfb4
chore: remove planning docs folder
Maurier Aug 31, 2026
20f260a
fix: date picker dialog collapsing to a thin strip
Maurier Sep 1, 2026
ecd6116
style: remove redundant comment on date picker dialog width
Maurier Sep 1, 2026
934084d
Update version
Sep 1, 2026
e6a1533
Update version
Sep 1, 2026
a8e6c00
feat: choose which QC datastreams to plot from a source row
Maurier Sep 14, 2026
8502b55
refactor: add time range preset resolver
Maurier Sep 14, 2026
a136457
fix: anchor time range presets to the plotted data
Maurier Sep 14, 2026
1475d35
fix: refresh the managed datastream after a commit
Maurier Sep 14, 2026
0ca1329
fix: keep a commit successful when the datastream refresh fails
Maurier Sep 14, 2026
19a112c
fix: keep the time range preset when a date field is left unchanged
Maurier Sep 14, 2026
0e56c95
feat: continue with the current workspace from the picker
Maurier Sep 14, 2026
4965803
test: cover plotting old data with the default preset
Maurier Sep 14, 2026
22d81cf
Merge remote-tracking branch 'origin/develop' into 436-integrate-hist…
Maurier Sep 14, 2026
fc9aa60
fix: freeze the time range while editing and restore only real presets
Maurier Sep 14, 2026
2a1ebc0
fix: stop nesting history row controls inside a button
Maurier Sep 14, 2026
f04141c
test: cover QC session save and commit in the submit spec
Maurier Sep 14, 2026
d7ce519
fix: edit a copy of the base data instead of the cached record
Maurier Sep 14, 2026
6d738ee
feat: add a working copy store for managed datastreams
Maurier Sep 14, 2026
c145dfb
fix: replay committed sessions onto a copy and keep the resume test g…
Maurier Sep 14, 2026
a65996f
test: drain undo timer and auto-unmount in EditHistory spec
Maurier Sep 14, 2026
b552f13
fix: guard working copy builds against stale and concurrent loads
Maurier Sep 14, 2026
fcc8c04
test: assert the resume test checks the raw record identity
Maurier Sep 14, 2026
6edbb27
fix: return only the cached working copy from a superseded rebuild
Maurier Sep 14, 2026
6d624d1
test: capture the commit dialog and commit a session in the golden path
Maurier Sep 14, 2026
49b4ec2
fix: resize the editor plot when it is drawn before its container set…
Maurier Sep 14, 2026
edd13e0
fix: save the session draft from the nav rail exit guard
Maurier Sep 14, 2026
745e6eb
feat: plot the session working copy for managed datastreams
Maurier Sep 14, 2026
8c9cf53
refactor: share the session working copy between the plot and the editor
Maurier Sep 14, 2026
e2bec2a
test: cover the managed datastream working copy preview
Maurier Sep 14, 2026
0afd483
fix: keep the editor consistent when resuming loses its working copy
Maurier Sep 14, 2026
47308f7
test: pin raw and working copy rows by name in the preview spec
Maurier Sep 15, 2026
85e8215
fix: never resume over saved drafts without replaying them and clear …
Maurier Sep 15, 2026
2542fbc
feat: edit datastreams from a row button with the source as context
Maurier Sep 18, 2026
15185eb
feat: anchor editor context presets around the session window
Maurier Sep 18, 2026
6d35837
style: replace em-dashes with plain punctuation
Maurier Sep 18, 2026
de2655f
fix: show the multiplication sign in gap interval labels
Maurier Sep 18, 2026
3dce0ac
fix: serialize plot loads and draw once the plot element exists
Maurier Sep 18, 2026
91cded6
feat: default new sessions to the full source range
Maurier Sep 18, 2026
b47501e
feat: set description, status and method when creating a managed data…
Maurier Sep 18, 2026
653e1eb
feat: keep the edit session open in the Select view
Maurier Sep 18, 2026
0364eea
feat: ask before leaving an edit session
Maurier Sep 18, 2026
b0ebb86
test: warm the dev server before the e2e suite
Maurier Sep 18, 2026
cd12318
fix: ask before creating a datastream and scope share link zoom to it…
Maurier Sep 18, 2026
c38adb6
feat: only allow deleting the newest session and drop the in-progress…
Maurier Sep 18, 2026
6a55c0a
feat: reserve a plot slot for the edit target and label the row Edit …
Maurier Sep 18, 2026
093e474
feat: add presets and one-click fixes to the session window dialog
Maurier Sep 18, 2026
69813a3
feat: draw source context around the edit window, preview before edit…
Maurier Sep 21, 2026
0755bc8
Merge remote-tracking branch 'origin/436-integrate-history-endpoints'…
Maurier Sep 22, 2026
9ab68df
feat: move the time range control to the plot toolbar
Maurier Sep 22, 2026
0f2e9a3
fix: show the overview strip only in the Edit view
Maurier Sep 22, 2026
0f8be1b
Merge changes from main
Sep 22, 2026
b783d21
fix: block edits while a committed session is on screen
Maurier Sep 29, 2026
9a05672
fix: refit the y axis when a series gets new data
Maurier Sep 29, 2026
7e3d5a2
feat: show the local time zone on date and time inputs
Maurier Sep 29, 2026
b75f720
feat: show the period each history step touched
Maurier Sep 29, 2026
244be06
feat: start new sessions where the last commit ended
Maurier Sep 29, 2026
ea63c36
Updated version
Sep 29, 2026
dcdb343
feat: replace observations over an explicit time range
Maurier Oct 1, 2026
40af26a
feat: read the checksum of an observation range
Maurier Oct 1, 2026
b254033
fix: keep step comments through replays and skip missing delete indices
Maurier Oct 1, 2026
a201aae
fix: QC session review fixes
Maurier Oct 1, 2026
d69103f
feat: choose the time zone dates are shown in
Maurier Oct 1, 2026
174b9be
feat: shift months and years on a saved time zone calendar
Maurier Oct 1, 2026
488946b
chore: leave comment-only punctuation edits out of the diff
Maurier Oct 1, 2026
1bddad0
fix: install qc-utils from the repository
Maurier Oct 1, 2026
0443952
test: fix the store mock and slow imports in plotting specs
Maurier Oct 1, 2026
6f22a6c
fix: keep a plotted datastream as committed context once it is edited
Maurier Oct 1, 2026
396b270
Resolve merge conflicts
daniel-slaugh Oct 1, 2026
ff33cfc
Merge branch '436-integrate-history-endpoints' of https://github.com/…
daniel-slaugh Oct 1, 2026
334d2d1
fix: drop the plot reference when a plot unmounts
Maurier Oct 1, 2026
35c8f61
test: enable WebGL in the Firefox e2e browser
Maurier Oct 1, 2026
8665014
Merge remote-tracking branch 'origin/436-integrate-history-endpoints'…
Maurier Oct 1, 2026
cbb9f46
queue plot draws and dispose the plot on unmount; let date fields set…
daniel-slaugh Oct 1, 2026
07a3ce9
For now, skip WebGL check for Firefox on CI
daniel-slaugh Oct 1, 2026
3802896
Fix Data Management App e2e tests
daniel-slaugh Oct 1, 2026
808a23f
Increase branch coverage to 80%
daniel-slaugh Oct 1, 2026
4f2774f
Resolve branch conflict
daniel-slaugh Oct 1, 2026
608e22f
Merge remote-tracking branch 'origin/release/v1.12.0' into 436-integr…
daniel-slaugh Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,6 @@ jobs:
working-directory: apps/qc-app
run: npm ci --workspaces=false

- name: Install local QC utils package
working-directory: apps/qc-app
run: npm install ../../packages/qc-utils --no-save --package-lock=false --workspaces=false

- name: Run QC app typecheck
working-directory: apps/qc-app
run: npm run typecheck
Expand Down Expand Up @@ -395,10 +391,6 @@ jobs:
working-directory: apps/qc-app
run: npm ci --workspaces=false

- name: Install local QC utils package
working-directory: apps/qc-app
run: npm install ../../packages/qc-utils --no-save --package-lock=false --workspaces=false

- name: Install Playwright browsers
working-directory: apps/qc-app
run: npx playwright install --with-deps chromium firefox
Expand Down
19 changes: 15 additions & 4 deletions apps/data-management/e2e/specs/orchestration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { expect, test } from '@playwright/test'
import { expect, test, type Page } from '@playwright/test'

import { authenticateSession } from '../support/auth'
import { fixtures, users } from '../support/fixtures'
import { chooseOverlayOption, selectWorkspace } from '../support/ui'

test.describe('orchestration', () => {
async function selectDataConnection(page: Page) {
await page
.locator('.sidebar-item--connection')
.filter({ hasText: fixtures.orchestration.dataConnectionName })
.click()
}

test('orchestration page loads seeded workspace orchestration data', async ({
page,
}) => {
await authenticateSession(page, users.owner.email, users.owner.password)

await page.goto('/orchestration')
await page.goto('/orchestration/ingestion')
await selectWorkspace(page, fixtures.workspaces.private.name)
await selectDataConnection(page)

await expect(page.getByText('Job orchestration', { exact: true })).toBeVisible()
await expect(
Expand All @@ -29,8 +37,9 @@ test.describe('orchestration', () => {
}) => {
await authenticateSession(page, users.owner.email, users.owner.password)

await page.goto('/orchestration')
await page.goto('/orchestration/ingestion')
await selectWorkspace(page, fixtures.workspaces.private.name)
await selectDataConnection(page)

await expect(page.getByText('Job orchestration', { exact: true })).toBeVisible()

Expand All @@ -55,8 +64,9 @@ test.describe('orchestration', () => {
page,
}) => {
await authenticateSession(page, users.owner.email, users.owner.password)
await page.goto('/orchestration')
await page.goto('/orchestration/ingestion')
await selectWorkspace(page, fixtures.workspaces.private.name)
await selectDataConnection(page)

await expect(
page.getByRole('heading', {
Expand All @@ -68,6 +78,7 @@ test.describe('orchestration', () => {
await expect(page.getByText(fixtures.orchestration.dataConnectionName)).toHaveCount(0)

await selectWorkspace(page, fixtures.workspaces.private.name)
await selectDataConnection(page)
await expect(
page.getByRole('heading', {
name: fixtures.orchestration.dataConnectionName,
Expand Down
35 changes: 35 additions & 0 deletions apps/data-management/e2e/specs/sites.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,41 @@ test.describe('sites and workspaces', () => {
await expect(page).toHaveURL(/\/sites$/)
})

test('canceling or dismissing site edits discards unsaved changes', async ({
page,
}) => {
const stamp = Date.now()
const unsavedProvince = `Unsaved Province ${stamp}`
const unsavedCounty = `Unsaved County ${stamp}`

await authenticateSession(page, users.owner.email, users.owner.password)
await page.goto(`/sites/${fixtures.things.mutablePublic.id}`)

await page.getByTestId('edit-site-button').click()
await page.getByLabel('State/Province/Region').fill(unsavedProvince)
await page.getByRole('button', { name: 'Cancel' }).click()

await expect(page.getByText(unsavedProvince, { exact: true })).toHaveCount(0)

await page.getByTestId('edit-site-button').click()
await expect(page.getByLabel('State/Province/Region')).not.toHaveValue(
unsavedProvince
)
await page.getByRole('button', { name: 'Cancel' }).click()

await page.getByTestId('edit-site-button').click()
await page.getByLabel('County/District').fill(unsavedCounty)
await page.keyboard.press('Escape')

await expect(page.getByText(unsavedCounty, { exact: true })).toHaveCount(0)

await page.getByTestId('edit-site-button').click()
await expect(page.getByLabel('County/District')).not.toHaveValue(
unsavedCounty
)
await page.getByRole('button', { name: 'Cancel' }).click()
})

test('owner can edit, toggle privacy for, and delete a site with datastream CRUD', async ({
page,
browser,
Expand Down
33 changes: 20 additions & 13 deletions apps/data-management/e2e/specs/transfers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ async function deleteWorkspace(page: Page, name: string) {
await expect(page.getByRole('cell', { name, exact: true })).toHaveCount(0)
}

function pendingTransferRow(page: Page, name: string) {
return page
.getByRole('row', { name: new RegExp(name) })
.filter({
has: page.getByRole('button', { name: 'Accept transfer' }),
})
}

test.describe('workspace transfers', () => {
test.use({ storageState: { cookies: [], origins: [] } })

Expand All @@ -55,14 +63,18 @@ test.describe('workspace transfers', () => {

await page.getByRole('button', { name: 'Pending workspace transfer' }).click()

const transferRow = pendingTransferRow(page, fixtures.workspaces.transfer.name)
await expect(
page.getByRole('cell', { name: fixtures.workspaces.transfer.name, exact: true })
transferRow.getByRole('cell', {
name: fixtures.workspaces.transfer.name,
exact: true,
})
).toBeVisible()
await expect(
page.getByRole('button', { name: 'Accept transfer' })
transferRow.getByRole('button', { name: 'Accept transfer' })
).toBeVisible()
await expect(
page.getByRole('button', { name: 'Cancel transfer' })
transferRow.getByRole('button', { name: 'Cancel transfer' })
).toBeVisible()
})

Expand All @@ -88,15 +100,12 @@ test.describe('workspace transfers', () => {
await targetPage.goto('/orchestration')
await targetPage.getByRole('button', { name: 'Pending workspace transfer' }).click()

const pendingRow = targetPage.getByRole('row', {
name: new RegExp(workspaceName),
})
const pendingRow = pendingTransferRow(targetPage, workspaceName)
await expect(pendingRow).toBeVisible()
await pendingRow.getByRole('button', { name: 'Cancel transfer' }).click()
await expect(pendingRow).toHaveCount(0)

await page.reload()
await page.getByRole('button', { name: 'Workspaces', exact: true }).click()
await page.goto('/orchestration/workspaces')
await deleteWorkspace(page, workspaceName)

await targetContext.close()
Expand Down Expand Up @@ -124,9 +133,7 @@ test.describe('workspace transfers', () => {
await targetPage.goto('/orchestration')
await targetPage.getByRole('button', { name: 'Pending workspace transfer' }).click()

const pendingRow = targetPage.getByRole('row', {
name: new RegExp(workspaceName),
})
const pendingRow = pendingTransferRow(targetPage, workspaceName)
await expect(pendingRow).toBeVisible()
await pendingRow.getByRole('button', { name: 'Accept transfer' }).click()
await expect(pendingRow).toHaveCount(0)
Expand All @@ -136,8 +143,8 @@ test.describe('workspace transfers', () => {
await expect(ownedRow).toBeVisible()
await expect(ownedRow).toContainText('Owner')

await page.reload()
await page.getByRole('button', { name: 'Workspaces', exact: true }).click()
await page.goto('/orchestration/workspaces')
await expect(page.getByRole('button', { name: 'Add workspace' })).toBeVisible()
await expect(page.getByRole('cell', { name: workspaceName, exact: true })).toHaveCount(0)

await deleteWorkspace(targetPage, workspaceName)
Expand Down
15 changes: 14 additions & 1 deletion apps/data-management/e2e/specs/visualization.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,22 @@ test.describe("visualization", () => {
const workspaceFilter = page
.getByRole("combobox", { name: "Workspaces" })
.first();
const viewport = page.viewportSize();
const box = await workspaceFilter.boundingBox();
const isWithinViewport =
!!box &&
!!viewport &&
box.width > 0 &&
box.height > 0 &&
box.x + box.width > 0 &&
box.y + box.height > 0 &&
box.x < viewport.width &&
box.y < viewport.height;

if (
(await workspaceFilter.count()) > 0 &&
(await workspaceFilter.isVisible())
(await workspaceFilter.isVisible()) &&
isWithinViewport
) {
return;
}
Expand Down
15 changes: 13 additions & 2 deletions apps/data-management/e2e/support/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,19 @@ export async function chooseAutocompleteOption(
optionText: string
) {
const field = page.getByRole('combobox', { name: label }).first()
await field.click()
await field.fill(optionText)
const container = field.locator('xpath=ancestor::*[@role="combobox"][1]')
await expect(field).toBeVisible()
await container.scrollIntoViewIfNeeded()
const toggle = container.getByRole('button').last()
if ((await toggle.count()) > 0) {
await toggle.click({ force: true })
}
await field.evaluate((element) => {
;(element as HTMLInputElement).focus()
})
await page.keyboard.press('Control+A')
await page.keyboard.press('Backspace')
await page.keyboard.type(optionText)
await chooseOverlayOption(page, optionText)
}

Expand Down
4 changes: 3 additions & 1 deletion apps/data-management/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 32 additions & 12 deletions apps/data-management/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
<template>
<v-app>
<Navbar v-if="!route.meta.hideNavBar" />
<router-view v-slot="{ Component }">
<v-app>
<Navbar
v-if="Component && isHydroServerReady && !route.meta.hideNavBar"
/>

<v-main>
<router-view />
</v-main>
<v-main>
<component :is="Component" v-if="Component" />
<v-container v-else-if="initializationError" class="fill-height">
<v-row justify="center" align="center">
<v-col cols="12" md="6" class="text-center">
<h5 class="text-h5">Unable to load HydroServer</h5>
<p class="mt-3 text-body-1">
Check your connection and refresh the page.
</p>
</v-col>
</v-row>
</v-container>
<FullScreenLoader v-else loading-text="Loading HydroServer..." />
</v-main>

<Footer v-if="!route.meta.hideFooter" class="flex-grow-0" />
<Notifications />
<link
href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"
rel="stylesheet"
/>
</v-app>
<Footer v-if="Component && !route.meta.hideFooter" class="flex-grow-0" />
<Notifications />
<link
href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"
rel="stylesheet"
/>
</v-app>
</router-view>
</template>

<script setup lang="ts">
import {
initializationError,
isHydroServerReady,
} from '@/bootstrap/appInitialization'
import Navbar from '@/components/base/Navbar.vue'
import Footer from '@/components/base/Footer.vue'
import Notifications from '@/components/base/Notifications.vue'
import FullScreenLoader from '@/components/base/FullScreenLoader.vue'
import { useRoute } from 'vue-router'

const route = useRoute()
Expand Down
Loading
Loading