diff --git a/web-admin/src/features/alerts/CreateAlert.svelte b/web-admin/src/features/alerts/CreateAlert.svelte index 300e914ad6ac..d38d89344fa9 100644 --- a/web-admin/src/features/alerts/CreateAlert.svelte +++ b/web-admin/src/features/alerts/CreateAlert.svelte @@ -1,4 +1,6 @@ {#if hasTimeDimension && $dashboardStore} diff --git a/web-admin/src/features/alerts/history/AlertHistoryTable.svelte b/web-admin/src/features/alerts/history/AlertHistoryTable.svelte index b571d69f5777..735d197f3616 100644 --- a/web-admin/src/features/alerts/history/AlertHistoryTable.svelte +++ b/web-admin/src/features/alerts/history/AlertHistoryTable.svelte @@ -3,7 +3,7 @@ import NoAlertRunsYet from "@rilldata/web-admin/features/alerts/history/NoAlertRunsYet.svelte"; import { useAlert } from "@rilldata/web-admin/features/alerts/selectors"; import ResourceList from "@rilldata/web-common/features/resources/ResourceList.svelte"; - import type { V1AlertExecution } from "@rilldata/web-common/runtime-client/gen/index.schemas"; + import { type V1AlertExecution } from "@rilldata/web-common/runtime-client"; import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import type { ColumnDef } from "tanstack-table-8-svelte-5"; import { renderComponent } from "tanstack-table-8-svelte-5"; @@ -14,10 +14,11 @@ $: alertQuery = useAlert(runtimeClient, alert); + $: history = $alertQuery.data?.resource?.alert?.state?.executionHistory ?? []; + /** * Table column definitions. - * - "composite": Renders all dashboard data in a single cell. - * - Others: Used for sorting and filtering but not displayed. + * - "composite": Renders all execution data in a single cell. */ const columns: ColumnDef[] = [ { @@ -46,13 +47,13 @@ {:else if $alertQuery.isLoading}
Loading...
- {:else if !$alertQuery.data?.resource.alert.state.executionHistory.length} + {:else if !history.length} {:else} diff --git a/web-admin/src/features/alerts/listing/AlertActionsCell.svelte b/web-admin/src/features/alerts/listing/AlertActionsCell.svelte new file mode 100644 index 000000000000..d11150a62f86 --- /dev/null +++ b/web-admin/src/features/alerts/listing/AlertActionsCell.svelte @@ -0,0 +1,94 @@ + + +{#if !isCreatedByCode} +
+ + + + + + + + + + Edit + + { + isDeleteConfirmOpen = true; + }} + > + + Delete + + + +
+ + + The alert "{title}" will be permanently deleted and will no + longer trigger notifications. + +{/if} diff --git a/web-admin/src/features/alerts/listing/AlertOwnerBullet.svelte b/web-admin/src/features/alerts/listing/AlertOwnerBullet.svelte deleted file mode 100644 index 5b35c1647e67..000000000000 --- a/web-admin/src/features/alerts/listing/AlertOwnerBullet.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - -{#if $ownerName.isSuccess} - - {$ownerName.data ? `Created by ${$ownerName.data}` : "Created through code"} - -{/if} diff --git a/web-admin/src/features/alerts/listing/AlertOwnerCell.svelte b/web-admin/src/features/alerts/listing/AlertOwnerCell.svelte new file mode 100644 index 000000000000..d811dc2f941e --- /dev/null +++ b/web-admin/src/features/alerts/listing/AlertOwnerCell.svelte @@ -0,0 +1,22 @@ + + + + + {#if $ownerName.isSuccess} + + {$ownerName.data ?? "Code-defined"} + + {:else} + + {/if} + + diff --git a/web-admin/src/features/alerts/listing/AlertStatusCell.svelte b/web-admin/src/features/alerts/listing/AlertStatusCell.svelte new file mode 100644 index 000000000000..e8f4809b998a --- /dev/null +++ b/web-admin/src/features/alerts/listing/AlertStatusCell.svelte @@ -0,0 +1,31 @@ + + + diff --git a/web-admin/src/features/alerts/listing/AlertsTable.svelte b/web-admin/src/features/alerts/listing/AlertsTable.svelte index 034a1e66ca48..80ff6b20ffba 100644 --- a/web-admin/src/features/alerts/listing/AlertsTable.svelte +++ b/web-admin/src/features/alerts/listing/AlertsTable.svelte @@ -1,70 +1,184 @@ - + 0} + {getRowHref} +> + . - + diff --git a/web-admin/src/features/alerts/listing/AlertsTableCompositeCell.svelte b/web-admin/src/features/alerts/listing/AlertsTableCompositeCell.svelte deleted file mode 100644 index 55ae597b7e14..000000000000 --- a/web-admin/src/features/alerts/listing/AlertsTableCompositeCell.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - - -
- - - {title} - - {#if lastTrigger} - {#if lastTriggerErrorMessage} - - {:else} - - {/if} - {/if} -
-
- {#if !lastTrigger} - Hasn't been checked yet - {:else} - Last checked {timeAgo(new Date(lastTrigger))} - {/if} - - - - - - -
-
diff --git a/web-admin/src/features/alerts/metadata/AlertMetadata.svelte b/web-admin/src/features/alerts/metadata/AlertMetadata.svelte index 2ea8d7101174..600eac4ad95e 100644 --- a/web-admin/src/features/alerts/metadata/AlertMetadata.svelte +++ b/web-admin/src/features/alerts/metadata/AlertMetadata.svelte @@ -21,6 +21,8 @@ import { formatRefreshSchedule } from "@rilldata/web-admin/features/scheduled-reports/metadata/utils.ts"; import { IconButton } from "@rilldata/web-common/components/button"; import * as DropdownMenu from "@rilldata/web-common/components/dropdown-menu"; + import DeleteConfirmDialog from "@rilldata/web-common/features/resources/DeleteConfirmDialog.svelte"; + import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus"; import CancelCircle from "@rilldata/web-common/components/icons/CancelCircle.svelte"; import ThreeDot from "@rilldata/web-common/components/icons/ThreeDot.svelte"; import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte"; @@ -103,19 +105,27 @@ const queryClient = useQueryClient(); const deleteAlert = createAdminServiceDeleteAlert(); + let isDeleteConfirmOpen = false; + async function handleDeleteAlert() { - await $deleteAlert.mutateAsync({ - org: organization, - project, - name: $alertQuery.data.resource.meta.name.name, - }); - await queryClient.invalidateQueries({ - queryKey: getRuntimeServiceListResourcesQueryKey( - runtimeClient.instanceId, - ), - }); - // goto only after invalidate is complete - goto(`/${organization}/${project}/-/alerts`); + try { + await $deleteAlert.mutateAsync({ + org: organization, + project, + name: $alertQuery.data.resource.meta.name.name, + }); + await queryClient.invalidateQueries({ + queryKey: getRuntimeServiceListResourcesQueryKey( + runtimeClient.instanceId, + ), + }); + goto(`/${organization}/${project}/-/alerts`); + } catch { + eventBus.emit("notification", { + message: "Failed to delete alert", + type: "error", + }); + } } @@ -151,7 +161,12 @@ - + { + isDeleteConfirmOpen = true; + }} + > Delete Alert @@ -245,3 +260,12 @@ {/if} {/if} + + + The alert "{alertSpec?.displayName ?? ""}" will be + permanently deleted and will no longer trigger notifications. + diff --git a/web-admin/src/features/dashboards/listing/DashboardsTable.svelte b/web-admin/src/features/dashboards/listing/DashboardsTable.svelte index 53b0c877b4d6..28262d8db384 100644 --- a/web-admin/src/features/dashboards/listing/DashboardsTable.svelte +++ b/web-admin/src/features/dashboards/listing/DashboardsTable.svelte @@ -1,14 +1,7 @@ -{#if isLoading || isBuilding} -
- -
-{:else if isError} - -{:else if isSuccess} -
- - - - - Create a dashboard to get started - - - - {#if hasMoreDashboards} - - {/if} -
-{/if} + diff --git a/web-admin/src/features/dashboards/listing/DashboardsTableCompositeCell.svelte b/web-admin/src/features/dashboards/listing/DashboardsTableCompositeCell.svelte deleted file mode 100644 index 41b378e308c3..000000000000 --- a/web-admin/src/features/dashboards/listing/DashboardsTableCompositeCell.svelte +++ /dev/null @@ -1,62 +0,0 @@ - - - -
- - - {title !== "" ? title : name} - - {#if error !== ""} - Error - {/if} -
-
- {name} - {#if lastRefreshedDate} - - - Last refreshed {timeAgo(lastRefreshedDate)} - - {lastRefreshedDate.toLocaleString()} - - - {/if} - {#if description} - - {description} - {/if} -
-
diff --git a/web-admin/src/features/dashboards/share/ShareDashboardPopover.svelte b/web-admin/src/features/dashboards/share/ShareDashboardPopover.svelte index 520d5ccb97b8..8c9b2d3ce424 100644 --- a/web-admin/src/features/dashboards/share/ShareDashboardPopover.svelte +++ b/web-admin/src/features/dashboards/share/ShareDashboardPopover.svelte @@ -1,4 +1,6 @@ - - - - {filterSelection === "all" ? "All users" : filterSelection} - {#if isDropdownOpen} - - {:else} - - {/if} - - - { - filterSelection = "all"; - }} - > - All - - {#if showMembers} - { - filterSelection = "members"; - }} - > - Members - - {/if} - { - filterSelection = "pending"; - }} - > - Pending invites - - - - -{#if showRoleFilter} - - - {roleFilterLabel} - {#if isRoleDropdownOpen} - - {:else} - - {/if} - - - {#each roleOptions as option} - { - roleFilter = option.value; - }} - > - {option.label} - - {/each} - - -{/if} diff --git a/web-admin/src/features/projects/ProjectPage.svelte b/web-admin/src/features/projects/ProjectPage.svelte index 7aea6e2c57ea..81d465ae97b3 100644 --- a/web-admin/src/features/projects/ProjectPage.svelte +++ b/web-admin/src/features/projects/ProjectPage.svelte @@ -11,8 +11,11 @@ $: ({ isLoading, isError, isSuccess, error } = $query); - -
+ +
{#if isLoading}
diff --git a/web-admin/src/features/projects/environment-variables/DeleteDialog.svelte b/web-admin/src/features/projects/environment-variables/DeleteDialog.svelte index 7812e528b7b6..4ae0471ee0e8 100644 --- a/web-admin/src/features/projects/environment-variables/DeleteDialog.svelte +++ b/web-admin/src/features/projects/environment-variables/DeleteDialog.svelte @@ -4,16 +4,7 @@ createAdminServiceUpdateProjectVariables, getAdminServiceGetProjectVariablesQueryKey, } from "@rilldata/web-admin/client"; - import { - AlertDialog, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, - } from "@rilldata/web-common/components/alert-dialog/index.js"; - import { Button } from "@rilldata/web-common/components/button/index.js"; + import DeleteConfirmDialog from "@rilldata/web-common/features/resources/DeleteConfirmDialog.svelte"; import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus"; import { useQueryClient } from "@tanstack/svelte-query"; @@ -27,14 +18,14 @@ const queryClient = useQueryClient(); const updateProjectVariables = createAdminServiceUpdateProjectVariables(); - async function onDelete(deletedName: string) { + async function handleDelete() { try { await $updateProjectVariables.mutateAsync({ org: organization, project, data: { environment, - unsetVariables: [deletedName], + unsetVariables: [name], }, }); @@ -59,50 +50,14 @@ }); } } - - async function handleDelete() { - try { - onDelete(name); - open = false; - } catch (error) { - console.error("Failed to delete environment variable:", error); - } - } - - - {#snippet child({ props })} - - {/snippet} - - - - Delete this environment variable? - -
- The environment variable {name} will no longer be available for this project. -
-
-
- - - - -
-
- - + + The environment variable {name} will no longer be available for this project. + diff --git a/web-admin/src/features/projects/status/logs/ProjectLogsPage.svelte b/web-admin/src/features/projects/status/logs/ProjectLogsPage.svelte index 1c7ed4877d16..e8a3d3fdc520 100644 --- a/web-admin/src/features/projects/status/logs/ProjectLogsPage.svelte +++ b/web-admin/src/features/projects/status/logs/ProjectLogsPage.svelte @@ -10,10 +10,8 @@ V1LogLevel, type V1WatchLogsResponse, } from "@rilldata/web-common/runtime-client"; - import Search from "@rilldata/web-common/components/search/Search.svelte"; - import * as DropdownMenu from "@rilldata/web-common/components/dropdown-menu"; - import CaretDownIcon from "@rilldata/web-common/components/icons/CaretDownIcon.svelte"; - import CaretUpIcon from "@rilldata/web-common/components/icons/CaretUpIcon.svelte"; + import { TableToolbar } from "@rilldata/web-common/components/table-toolbar"; + import type { FilterGroup } from "@rilldata/web-common/components/table-toolbar/types"; import { createUrlFilterSync, parseArrayParam, @@ -38,7 +36,6 @@ let logsVersion = 0; let logsContainer: HTMLDivElement; let connectionError: string | null = null; - let filterDropdownOpen = false; let searchText = parseStringParam($page.url.searchParams.get("q")); let selectedLevels = parseArrayParam($page.url.searchParams.get("level")); let mounted = false; @@ -103,19 +100,26 @@ totalLogs = logStore.getAll().length; } - $: selectedLevelLabel = (() => { - if (selectedLevels.length === 0) return "All levels"; - if (selectedLevels.length === 1) { - return ( - filterableLevels.find((l) => l.value === selectedLevels[0])?.label ?? - "1 level" - ); - } - const first = filterableLevels.find( - (l) => l.value === selectedLevels[0], - )?.label; - return `${first}, +${selectedLevels.length - 1} other${selectedLevels.length > 2 ? "s" : ""}`; - })(); + $: filterGroups = [ + { + label: "Levels", + key: "level", + options: filterableLevels, + selected: selectedLevels, + defaultValue: [], + multiSelect: true, + }, + ] satisfies FilterGroup[]; + + function handleFilterChange(key: string, selected: string | string[]) { + if (key !== "level") return; + selectedLevels = Array.isArray(selected) ? selected : [selected]; + } + + function clearFilters() { + selectedLevels = []; + searchText = ""; + } let unsubs: (() => void)[] = []; @@ -224,19 +228,6 @@ return ""; } } - - function toggleLevel(level: string) { - if (selectedLevels.includes(level)) { - selectedLevels = selectedLevels.filter((l) => l !== level); - } else { - selectedLevels = [...selectedLevels, level]; - } - } - - function clearFilters() { - selectedLevels = []; - searchText = ""; - }
@@ -263,55 +254,13 @@
-
-
- -
- - - - - {selectedLevelLabel} - - {#if filterDropdownOpen} - - {:else} - - {/if} - - - {#each filterableLevels as level} - toggleLevel(level.value)} - > - {level.label} - - {/each} - - - - {#if selectedLevels.length > 0 || searchText} - - {/if} -
+
{#if hasConnectionError} diff --git a/web-admin/src/features/public-urls/PublicURLsActionsRow.svelte b/web-admin/src/features/public-urls/PublicURLsActionsRow.svelte index 5a26d5d7ac73..1182f4071fb2 100644 --- a/web-admin/src/features/public-urls/PublicURLsActionsRow.svelte +++ b/web-admin/src/features/public-urls/PublicURLsActionsRow.svelte @@ -4,12 +4,16 @@ import ThreeDot from "@rilldata/web-common/components/icons/ThreeDot.svelte"; import { copyToClipboard } from "@rilldata/web-common/lib/actions/copy-to-clipboard"; import { Trash2Icon, CopyIcon } from "lucide-svelte"; - import DeletePublicURLConfirmDialog from "./DeletePublicURLConfirmDialog.svelte"; + import DeleteConfirmDialog from "@rilldata/web-common/features/resources/DeleteConfirmDialog.svelte"; export let id: string; export let url: string; export let onDelete: (deletedTokenId: string) => void; + async function handleDelete() { + onDelete(id); + } + function handleCopy() { copyToClipboard(url, "Public URL copied to clipboard"); } @@ -47,4 +51,10 @@ - + + Recipients of this URL will no longer be able to access it. + diff --git a/web-admin/src/features/resources/ResourceList.svelte b/web-admin/src/features/resources/ResourceList.svelte deleted file mode 100644 index 0bf3635154dd..000000000000 --- a/web-admin/src/features/resources/ResourceList.svelte +++ /dev/null @@ -1,158 +0,0 @@ - - -
- {#if toolbar} - - - - {/if} - -
- -
    - {#each $table.getRowModel().rows as row (row.id)} -
  • - {#each row.getVisibleCells() as cell (cell.id)} - - {/each} -
  • - {:else} -
  • -
    - {#if isFiltered} - -
    -
    - No {kind}s match your search -
    -
    - Try adjusting your search terms -
    -
    - {:else} - - -
    - You don't have any {kind}s yet -
    -
    - {/if} -
    -
  • - {/each} -
-
-
- - diff --git a/web-admin/src/features/scheduled-reports/history/ReportHistoryTable.svelte b/web-admin/src/features/scheduled-reports/history/ReportHistoryTable.svelte index f40936211e39..49457452cf6a 100644 --- a/web-admin/src/features/scheduled-reports/history/ReportHistoryTable.svelte +++ b/web-admin/src/features/scheduled-reports/history/ReportHistoryTable.svelte @@ -14,10 +14,12 @@ $: reportQuery = useReport(runtimeClient, report); + $: history = + $reportQuery.data?.resource?.report?.state?.executionHistory ?? []; + /** * Table column definitions. - * - "composite": Renders all dashboard data in a single cell. - * - Others: Used for sorting and filtering but not displayed. + * - "composite": Renders all execution data in a single cell. */ const columns: ColumnDef[] = [ { @@ -45,13 +47,13 @@
{:else if $reportQuery.isLoading}
Loading...
- {:else if !$reportQuery.data?.resource.report.state.executionHistory.length} + {:else if !history.length} {:else} diff --git a/web-admin/src/features/scheduled-reports/listing/ReportActionsCell.svelte b/web-admin/src/features/scheduled-reports/listing/ReportActionsCell.svelte new file mode 100644 index 000000000000..37d0a1ae03c4 --- /dev/null +++ b/web-admin/src/features/scheduled-reports/listing/ReportActionsCell.svelte @@ -0,0 +1,130 @@ + + +{#if !isCreatedByCode} +
+ + + + + + + + + + Run + + + + Edit + + { + isDeleteConfirmOpen = true; + }} + > + + Delete + + + +
+ + + The report "{title}" will be permanently deleted and will + no longer send scheduled emails. + +{/if} diff --git a/web-admin/src/features/scheduled-reports/listing/ReportFrequencyCell.svelte b/web-admin/src/features/scheduled-reports/listing/ReportFrequencyCell.svelte new file mode 100644 index 000000000000..daf081589a33 --- /dev/null +++ b/web-admin/src/features/scheduled-reports/listing/ReportFrequencyCell.svelte @@ -0,0 +1,15 @@ + + +{#if frequency} + + {frequency} + {frequency} + +{:else} + +{/if} diff --git a/web-admin/src/features/scheduled-reports/listing/ReportOwnerBullet.svelte b/web-admin/src/features/scheduled-reports/listing/ReportOwnerBullet.svelte deleted file mode 100644 index 8b0d94d19fee..000000000000 --- a/web-admin/src/features/scheduled-reports/listing/ReportOwnerBullet.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - -{#if $ownerName.isSuccess} - - {$ownerName.data ? `Created by ${$ownerName.data}` : "Created through code"} - -{/if} diff --git a/web-admin/src/features/scheduled-reports/listing/ReportOwnerCell.svelte b/web-admin/src/features/scheduled-reports/listing/ReportOwnerCell.svelte new file mode 100644 index 000000000000..d1c6c237a428 --- /dev/null +++ b/web-admin/src/features/scheduled-reports/listing/ReportOwnerCell.svelte @@ -0,0 +1,22 @@ + + + + + {#if $ownerName.isSuccess} + + {$ownerName.data ?? "Code-defined"} + + {:else} + + {/if} + + diff --git a/web-admin/src/features/scheduled-reports/listing/ReportStatusCell.svelte b/web-admin/src/features/scheduled-reports/listing/ReportStatusCell.svelte new file mode 100644 index 000000000000..7d253d5700ac --- /dev/null +++ b/web-admin/src/features/scheduled-reports/listing/ReportStatusCell.svelte @@ -0,0 +1,19 @@ + + + diff --git a/web-admin/src/features/scheduled-reports/listing/ReportsTable.svelte b/web-admin/src/features/scheduled-reports/listing/ReportsTable.svelte index b36870730788..cc31abeb832f 100644 --- a/web-admin/src/features/scheduled-reports/listing/ReportsTable.svelte +++ b/web-admin/src/features/scheduled-reports/listing/ReportsTable.svelte @@ -1,72 +1,201 @@ - + 0} + {getRowHref} +> + from any dashboard - + diff --git a/web-admin/src/features/scheduled-reports/listing/ReportsTableCompositeCell.svelte b/web-admin/src/features/scheduled-reports/listing/ReportsTableCompositeCell.svelte deleted file mode 100644 index e84b9a4dbcc8..000000000000 --- a/web-admin/src/features/scheduled-reports/listing/ReportsTableCompositeCell.svelte +++ /dev/null @@ -1,59 +0,0 @@ - - - -
- - - {title} - - {#if lastRun} - {#if lastRunErrorMessage} - - {:else} - - {/if} - {/if} -
-
- {#if !lastRun} - Hasn't run yet - {:else} - Last run {formatRunDate(lastRun, timeZone)} - {/if} - - {humanReadableFrequency} - - - - - - -
-
diff --git a/web-admin/src/features/scheduled-reports/metadata/ReportMetadata.svelte b/web-admin/src/features/scheduled-reports/metadata/ReportMetadata.svelte index 8218f680bc44..8cedbab391fc 100644 --- a/web-admin/src/features/scheduled-reports/metadata/ReportMetadata.svelte +++ b/web-admin/src/features/scheduled-reports/metadata/ReportMetadata.svelte @@ -5,6 +5,8 @@ import { extractNotifier } from "@rilldata/web-admin/features/scheduled-reports/metadata/notifiers-utils"; import IconButton from "@rilldata/web-common/components/button/IconButton.svelte"; import * as DropdownMenu from "@rilldata/web-common/components/dropdown-menu"; + import DeleteConfirmDialog from "@rilldata/web-common/features/resources/DeleteConfirmDialog.svelte"; + import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus"; import CancelCircle from "@rilldata/web-common/components/icons/CancelCircle.svelte"; import ThreeDot from "@rilldata/web-common/components/icons/ThreeDot.svelte"; import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte"; @@ -96,22 +98,31 @@ const deleteReport = createAdminServiceDeleteReport(); let showEditReportDialog = false; + let isDeleteConfirmOpen = false; + function handleEditReport() { showEditReportDialog = true; } async function handleDeleteReport() { - await $deleteReport.mutateAsync({ - org: organization, - project, - name: $reportQuery.data.resource.meta.name.name, - }); - queryClient.invalidateQueries({ - queryKey: getRuntimeServiceListResourcesQueryKey( - runtimeClient.instanceId, - ), - }); - goto(`/${organization}/${project}/-/reports`); + try { + await $deleteReport.mutateAsync({ + org: organization, + project, + name: $reportQuery.data.resource.meta.name.name, + }); + await queryClient.invalidateQueries({ + queryKey: getRuntimeServiceListResourcesQueryKey( + runtimeClient.instanceId, + ), + }); + goto(`/${organization}/${project}/-/reports`); + } catch { + eventBus.emit("notification", { + message: "Failed to delete report", + type: "error", + }); + } } @@ -161,7 +172,12 @@ > Edit report - + { + isDeleteConfirmOpen = true; + }} + > Delete report @@ -244,3 +260,12 @@ }} /> {/if} + + + The report "{reportSpec?.displayName ?? ""}" will be + permanently deleted and will no longer send scheduled emails. + diff --git a/web-admin/src/routes/[organization]/-/users/+page.svelte b/web-admin/src/routes/[organization]/-/users/+page.svelte index f66c41b5a8de..4caa1c7b9638 100644 --- a/web-admin/src/routes/[organization]/-/users/+page.svelte +++ b/web-admin/src/routes/[organization]/-/users/+page.svelte @@ -7,7 +7,6 @@ import AddUsersDialog from "@rilldata/web-admin/features/organizations/user-management/dialogs/AddUsersDialog.svelte"; import ChangingBillingContactRoleDialog from "@rilldata/web-admin/features/organizations/user-management/dialogs/ChangingBillingContactRoleDialog.svelte"; import EditUserGroupDialog from "@rilldata/web-admin/features/organizations/user-management/dialogs/EditUserGroupDialog.svelte"; - import OrgUsersFilters from "@rilldata/web-admin/features/organizations/user-management/OrgUsersFilters.svelte"; import OrgUsersTable from "@rilldata/web-admin/features/organizations/user-management/table/users/OrgUsersTable.svelte"; import RemovingBillingContactDialog from "@rilldata/web-admin/features/organizations/user-management/dialogs/RemovingBillingContactDialog.svelte"; import { @@ -15,7 +14,8 @@ getOrgUserMembers, } from "@rilldata/web-admin/features/organizations/user-management/selectors.ts"; import Button from "@rilldata/web-common/components/button/Button.svelte"; - import { Search } from "@rilldata/web-common/components/search"; + import { TableToolbar } from "@rilldata/web-common/components/table-toolbar"; + import type { FilterGroup } from "@rilldata/web-common/components/table-toolbar/types"; import DelayedSpinner from "@rilldata/web-common/features/entity-management/DelayedSpinner.svelte"; import { OrgUserRoles } from "@rilldata/web-common/features/users/roles.ts"; import { Plus } from "lucide-svelte"; @@ -122,6 +122,46 @@ const currentUser = createAdminServiceGetCurrentUser(); $: billingContactUser = getOrganizationBillingContactUser(organization); + + $: filterGroups = [ + { + label: "User type", + key: "type", + options: [ + { value: "all", label: "All" }, + { value: "members", label: "Members" }, + { value: "pending", label: "Pending" }, + ], + selected: filterSelection, + defaultValue: "all", + multiSelect: false, + }, + { + label: "Role", + key: "role", + options: [ + { value: "all", label: "All Roles" }, + { value: "admin", label: "Admins" }, + { value: "editor", label: "Editors" }, + { value: "viewer", label: "Viewers" }, + ], + selected: roleFilter, + defaultValue: "all", + multiSelect: false, + }, + ] satisfies FilterGroup[]; + + function handleFilterChange(key: string, selected: string | string[]) { + if (Array.isArray(selected)) return; + if (key === "type") filterSelection = selected as typeof filterSelection; + if (key === "role") roleFilter = selected as typeof roleFilter; + } + + function clearFilters() { + filterSelection = "all"; + roleFilter = "all"; + searchText = ""; + }
@@ -138,24 +178,18 @@
{:else if $orgMemberUsersInfiniteQuery.isSuccess && $orgInvitesInfiniteQuery.isSuccess}
-
- - - -
+
{:else if $listOrganizationMemberUsergroups.isSuccess}
-
- + -
+
@@ -123,28 +147,18 @@
{:else if $orgMemberUsersInfiniteQuery.isSuccess && $orgInvitesInfiniteQuery.isSuccess}
-
- - - -
+
{project} overview - Rill - -
+ +
diff --git a/web-admin/tests/alerts.spec.ts b/web-admin/tests/alerts.spec.ts index 3aab5e95a7c7..2f5f882eefcc 100644 --- a/web-admin/tests/alerts.spec.ts +++ b/web-admin/tests/alerts.spec.ts @@ -136,6 +136,11 @@ test.describe.serial("Alerts", () => { await adminPage.goto(link); + // Move the cursor away from the leaderboard so the row's hover-only + // "previous → current" inline preview doesn't render and the assertion + // can match the canonical row text. + await adminPage.mouse.move(0, 0); + // Assert that the data is as expected await expect( adminPage @@ -277,6 +282,7 @@ test.describe.serial("Alerts", () => { // Delete the alert await adminPage.getByLabel("Alert context menu").click(); await adminPage.getByRole("menuitem", { name: "Delete Alert" }).click(); + await adminPage.getByRole("button", { name: "Yes, delete" }).click(); // Back to listing page without any alerts await expect( @@ -381,20 +387,18 @@ test.describe.serial("Alerts", () => { ); }); - test("Should delete alert with schedule", async ({ adminPage }) => { + test("Should delete alert from the listing row menu", async ({ + adminPage, + }) => { await adminPage.goto("/e2e/openrtb/-/alerts"); - await adminPage - .getByRole("link", { - name: "Requests value alert", - }) - .click(); + // Open the row's actions menu and click Delete (covers the new listing-page action wired up in the row). + await adminPage.getByLabel("Actions for Requests value alert").click(); + await adminPage.getByRole("menuitem", { name: "Delete" }).click(); + await adminPage.getByRole("button", { name: "Yes, delete" }).click(); - // Delete the alert - await adminPage.getByLabel("Alert context menu").click(); - await adminPage.getByRole("menuitem", { name: "Delete Alert" }).click(); - - // Back to listing page without any alerts + // Stay on the listing page; no alerts remain. + await expect(adminPage).toHaveURL(/\/-\/alerts$/); await expect( adminPage.getByText("You don't have any alerts yet"), ).toBeVisible(); diff --git a/web-admin/tests/reports.spec.ts b/web-admin/tests/reports.spec.ts index 22ee7181262d..2b3a3a88a959 100644 --- a/web-admin/tests/reports.spec.ts +++ b/web-admin/tests/reports.spec.ts @@ -116,18 +116,16 @@ test.describe.serial("Reports", () => { ); }); - test("Should run a report and receive an email", async ({ adminPage }) => { - // Open the report + test("Should run a report from the listing row menu and receive an email", async ({ + adminPage, + }) => { await adminPage.goto("/e2e/openrtb/-/reports"); - await adminPage - .getByRole("link", { - name: "Report for last 14 days", - }) - .click(); - // Store time before clicking run to ensure latest email is fetched. + // Trigger the run from the listing-row actions menu (the new wired-up action). const time = new Date(); - await adminPage.getByRole("button", { name: "Run now" }).click(); + await adminPage.getByLabel("Actions for Report for last 14 days").click(); + await adminPage.getByRole("menuitem", { name: "Run" }).click(); + // Notification is shown await expect(adminPage.getByLabel("Notification")).toHaveText( "Triggered an ad-hoc run of this report.", @@ -233,20 +231,18 @@ test.describe.serial("Reports", () => { ); }); - test("Should delete report", async ({ adminPage }) => { + test("Should delete report from the listing row menu", async ({ + adminPage, + }) => { await adminPage.goto("/e2e/openrtb/-/reports"); - await adminPage - .getByRole("link", { - name: "Report for last 14 days", - }) - .click(); - - // Delete the report - await adminPage.getByLabel("Report context menu").click(); - await adminPage.getByRole("menuitem", { name: "Delete Report" }).click(); + // Open the row's actions menu and click Delete (covers the new listing-page action wired up in the row). + await adminPage.getByLabel("Actions for Report for last 14 days").click(); + await adminPage.getByRole("menuitem", { name: "Delete" }).click(); + await adminPage.getByRole("button", { name: "Yes, delete" }).click(); - // Back to listing page without any reports + // Stay on the listing page; no reports remain. + await expect(adminPage).toHaveURL(/\/-\/reports$/); await expect( adminPage.getByText("You don't have any reports yet"), ).toBeVisible(); diff --git a/web-admin/tests/setup/setup.ts b/web-admin/tests/setup/setup.ts index 2064ecb76a4f..31a3f10dfb77 100644 --- a/web-admin/tests/setup/setup.ts +++ b/web-admin/tests/setup/setup.ts @@ -239,7 +239,7 @@ setup.describe("global setup", () => { // Wait for the project to be ready await expect(adminPage.getByLabel("Container title")).toHaveText( - "Project dashboards", + "Dashboards", ); // Check that the dashboards are listed @@ -250,18 +250,16 @@ setup.describe("global setup", () => { adminPage.getByRole("link", { name: "Programmatic Ads Bids" }), ).toBeVisible(); - // Wait for the first dashboard to be ready + // Wait for the first dashboard to be ready. In the column-based layout, + // each row is a link whose accessible name aggregates the cell text; + // the Status cell renders "Ready" once reconcile completes. await expect( - adminPage.getByRole("link", { - name: "Programmatic Ads Auction auction_explore", - }), - ).toContainText("Last refreshed", { timeout: 15_000 }); + adminPage.getByRole("link", { name: "Programmatic Ads Auction" }).first(), + ).toContainText("Ready", { timeout: 15_000 }); await expect( - adminPage.getByRole("link", { - name: "Programmatic Ads Bids bids_explore", - }), - ).toContainText("Last refreshed", { timeout: 15_000 }); + adminPage.getByRole("link", { name: "Programmatic Ads Bids" }), + ).toContainText("Ready", { timeout: 15_000 }); }); setup("should deploy the AdBids project", async ({ adminPage }) => { @@ -316,7 +314,7 @@ setup.describe("global setup", () => { // Wait for the project to be ready await expect(adminPage.getByLabel("Container title")).toHaveText( - "Project dashboards", + "Dashboards", ); // Check that the dashboards are listed diff --git a/web-common/src/components/table-toolbar/TableToolbar.svelte b/web-common/src/components/table-toolbar/TableToolbar.svelte index d0c11c7926cd..8735c03f3701 100644 --- a/web-common/src/components/table-toolbar/TableToolbar.svelte +++ b/web-common/src/components/table-toolbar/TableToolbar.svelte @@ -10,6 +10,7 @@ let { searchText = $bindable(""), searchDisabled = false, + showSearch = true, filterGroups = [], onFilterChange, onClearAllFilters, @@ -17,10 +18,12 @@ showSort = true, showViewToggle = false, viewMode = $bindable("list"), + disabled = false, children, }: { searchText?: string; searchDisabled?: boolean; + showSearch?: boolean; filterGroups?: FilterGroup[]; onFilterChange?: (key: string, selected: string | string[]) => void; onClearAllFilters?: () => void; @@ -28,6 +31,8 @@ showSort?: boolean; showViewToggle?: boolean; viewMode?: ViewMode; + /** Disables search, filter, and sort. Useful when the underlying data is empty. */ + disabled?: boolean; children?: Snippet; } = $props(); @@ -35,14 +40,19 @@
- +
- + {#if showSearch} + + {/if} {#if showSort} - + {/if} {#if showViewToggle} diff --git a/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte b/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte index de3ab74b418b..1ff57fb209b9 100644 --- a/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte @@ -6,9 +6,11 @@ let { filterGroups = [], onFilterChange, + disabled = false, }: { filterGroups: FilterGroup[]; onFilterChange?: (key: string, selected: string | string[]) => void; + disabled?: boolean; } = $props(); function handleClick(group: FilterGroup, value: string) { @@ -27,8 +29,9 @@ {#if filterGroups.length > 0} Filter diff --git a/web-common/src/components/table-toolbar/TableToolbarSort.svelte b/web-common/src/components/table-toolbar/TableToolbarSort.svelte index d02d1401663b..816f8648d613 100644 --- a/web-common/src/components/table-toolbar/TableToolbarSort.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarSort.svelte @@ -4,8 +4,10 @@ let { sortDirection = $bindable("newest"), + disabled = false, }: { sortDirection: SortDirection; + disabled?: boolean; } = $props(); const sortLabel = $derived(sortDirection === "newest" ? "Newest" : "Oldest"); @@ -17,7 +19,8 @@ + Cancel + diff --git a/web-common/src/features/resources/ResourceList.svelte b/web-common/src/features/resources/ResourceList.svelte index 52c1fd26bd65..1a1680b39f41 100644 --- a/web-common/src/features/resources/ResourceList.svelte +++ b/web-common/src/features/resources/ResourceList.svelte @@ -14,7 +14,6 @@ } from "tanstack-table-8-svelte-5"; import { setContext } from "svelte"; import { writable } from "svelte/store"; - import ResourceListToolbar from "./ResourceListToolbar.svelte"; export let data: unknown[] = []; export let columns: ColumnDef[] = []; @@ -23,6 +22,13 @@ export let toolbar: boolean = true; export let fixedRowHeight: boolean = true; export let initialSorting: SortingState = []; + /** + * Whether the caller has applied search/filters to `data` before passing it in. + * When true and `data` is empty, the "No {kind}s match your search" empty state + * is shown. When false (or undefined), falls back to the table's own globalFilter + * state for backwards compatibility. + */ + export let isFiltered: boolean | undefined = undefined; let sorting: SortingState = initialSorting; function setSorting(updater: Updater) { @@ -72,15 +78,16 @@ // Whenever the input data changes, rerender the table $: data && rerender(); - // Check if we're in a filtered state (search is active) - $: isFiltered = $table.getState().globalFilter?.length > 0; + // Check if we're in a filtered state. Prefer the caller-provided value (since + // most callers now pre-filter `data` externally); otherwise fall back to the + // table's own globalFilter state. + $: isFilteredEffective = + isFiltered ?? ($table.getState().globalFilter?.length ?? 0) > 0;
{#if toolbar} - - - + {/if}
@@ -97,7 +104,7 @@ {:else}
  • - {#if isFiltered} + {#if isFilteredEffective}
    @@ -127,33 +134,19 @@ @apply list-none p-0 m-0 w-full; } - .resource-list-item, - .resource-list-item-empty { - @apply block w-full border bg-surface-background; - } - - .resource-list-item.fixed-height { - @apply h-[60px]; + .resource-list-item { + @apply block w-full border-b border-x-0 border-t-0; } - /* Remove top border on non-first items to avoid double borders */ - .resource-list-item + .resource-list-item { - @apply border-t-0; + .resource-list-item:first-child { + @apply border-t; } - /* Rounded corners on first and last items */ - .resource-list-item:first-child, - .resource-list-item-empty:first-child { - @apply rounded-t-lg; - } - - .resource-list-item:last-child, - .resource-list-item-empty:last-child { - @apply rounded-b-lg; + .resource-list-item-empty { + @apply block w-full; } - /* Hover effect on list items */ - .resource-list-item:hover { - @apply bg-surface-hover; + .resource-list-item.fixed-height { + @apply h-[60px]; } diff --git a/web-common/src/features/resources/ResourceListRow.svelte b/web-common/src/features/resources/ResourceListRow.svelte new file mode 100644 index 000000000000..6d9ddee0ed1d --- /dev/null +++ b/web-common/src/features/resources/ResourceListRow.svelte @@ -0,0 +1,61 @@ + + + diff --git a/web-common/src/features/resources/ResourceListToolbar.svelte b/web-common/src/features/resources/ResourceListToolbar.svelte deleted file mode 100644 index bd4ebda983c4..000000000000 --- a/web-common/src/features/resources/ResourceListToolbar.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - - diff --git a/web-common/src/features/resources/ResourceTable.svelte b/web-common/src/features/resources/ResourceTable.svelte new file mode 100644 index 000000000000..576930da7133 --- /dev/null +++ b/web-common/src/features/resources/ResourceTable.svelte @@ -0,0 +1,227 @@ + + + + +
    + {#if toolbar} + + {/if} + + + + {#each visibleColumns as column (column.id)} + + {/each} + + + {#each headerGroups as group (group.id)} + + {#each group.headers as header (header.id)} + {@const canSort = header.column.getCanSort()} + {@const sortDir = header.column.getIsSorted()} + {@const alignRight = + header.column.columnDef.meta?.align === "right"} + + {/each} + + {/each} + + + {#if rows.length} + {#each rows as row (row.id)} + {@const href = getRowHref?.(row.original)} + handleRowClick(e, href)} + on:keydown={(e) => handleRowKeydown(e, href)} + > + {#each row.getVisibleCells() as cell (cell.id)} + + {/each} + + {/each} + {:else} + + + + {/if} + +
    + {#if !header.isPlaceholder} + {#if canSort} + + {:else} + + {/if} + {/if} +
    + +
    +
    + {#if isFilteredEffective} +
    +
    + No {kind}s match your search +
    +
    + Try adjusting your search terms +
    +
    + {:else} + +
    + You don't have any {kind}s yet +
    +
    + {/if} +
    +
    +
    diff --git a/web-common/src/features/resources/cells/NameCell.svelte b/web-common/src/features/resources/cells/NameCell.svelte new file mode 100644 index 000000000000..ac5b8b710169 --- /dev/null +++ b/web-common/src/features/resources/cells/NameCell.svelte @@ -0,0 +1,5 @@ + + +{name} diff --git a/web-common/src/features/resources/cells/RelativeTimeCell.svelte b/web-common/src/features/resources/cells/RelativeTimeCell.svelte new file mode 100644 index 000000000000..b1da94f07f40 --- /dev/null +++ b/web-common/src/features/resources/cells/RelativeTimeCell.svelte @@ -0,0 +1,21 @@ + + +{#if date} + + {timeAgo(date)} + + {date.toLocaleString()} + + +{:else} + {fallback} +{/if} diff --git a/web-common/src/features/resources/cells/StatusTextCell.svelte b/web-common/src/features/resources/cells/StatusTextCell.svelte new file mode 100644 index 000000000000..00f6ee597e27 --- /dev/null +++ b/web-common/src/features/resources/cells/StatusTextCell.svelte @@ -0,0 +1,15 @@ + + + + {label} +