Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refresh button for source rows in Status page #6175

Open
wants to merge 52 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
2a8574f
new cell to refresh source in status tab
lovincyrus Nov 26, 2024
11378f3
move refresh sources to higher order
lovincyrus Nov 26, 2024
8babbc3
wip
lovincyrus Nov 26, 2024
263604a
add refresh confirm dialog
lovincyrus Nov 27, 2024
b878b9d
wip, fixme
lovincyrus Nov 27, 2024
227fd70
refetch allResources on row reload click
lovincyrus Dec 3, 2024
93b491c
force a new query using refetchKey for clean data
lovincyrus Dec 4, 2024
90d942e
add tooltip to menu item
lovincyrus Dec 4, 2024
e4e7b96
lint
lovincyrus Dec 4, 2024
04e0364
feedback, custom polling to avoid stale data
lovincyrus Dec 4, 2024
0dad749
error handling
lovincyrus Dec 4, 2024
4228a22
tsc whitelist
lovincyrus Dec 9, 2024
b56fbe0
Revert "tsc whitelist"
lovincyrus Dec 10, 2024
893dc80
add refresh trigger to new files
lovincyrus Dec 10, 2024
48a6fb7
stop polling when reconcile error
lovincyrus Dec 10, 2024
9d638f3
only stop polling when all resources are in idle status
lovincyrus Dec 11, 2024
0ecf036
rebase fix
lovincyrus Jan 24, 2025
6ac47e1
reserved space for actions in table
lovincyrus Jan 25, 2025
ca0fe8b
color loading spinner, loading type in notification
lovincyrus Jan 25, 2025
c43f6d6
confirmation dialog for single resource
lovincyrus Jan 25, 2025
eba66df
improve notifications based on diff resource status
lovincyrus Jan 25, 2025
82c56ba
lint
lovincyrus Jan 27, 2025
8298fed
refactor onError, feedback
lovincyrus Jan 27, 2025
c5ffad5
message fix
lovincyrus Jan 27, 2025
8efd19f
customizable timeout notification, feedback
lovincyrus Jan 27, 2025
5801459
use persisted, remove x from loading notification
lovincyrus Jan 27, 2025
82e29eb
proper notification clean up with id
lovincyrus Jan 28, 2025
7b998c5
hoist timeout to constants
lovincyrus Jan 28, 2025
1199ff6
max backoff
lovincyrus Jan 28, 2025
ab02acf
use resources, fetch interval
lovincyrus Jan 28, 2025
4ba771f
remove timestamp
lovincyrus Jan 28, 2025
98e3b7a
polling for refresh all
lovincyrus Jan 28, 2025
8d02b61
lint
lovincyrus Jan 28, 2025
973f7d6
readd loading notification, type check
lovincyrus Jan 28, 2025
53c7a6d
type check
lovincyrus Jan 28, 2025
a6a5a05
use is fetching for refreshing, max poll
lovincyrus Jan 29, 2025
83fbce7
handle navigate away case
lovincyrus Jan 29, 2025
39e930e
fix
lovincyrus Jan 29, 2025
ac8e223
optional type, clean up
lovincyrus Jan 29, 2025
afe8f95
fix
lovincyrus Jan 29, 2025
1d31e1e
polish navigate away and back notification
lovincyrus Jan 29, 2025
0c85b01
add refresh trigger to user facing resource kinds
lovincyrus Jan 30, 2025
2e0c8cf
add clear-all-notifications event
lovincyrus Jan 30, 2025
c00fcb3
feedback
lovincyrus Jan 30, 2025
f660ac3
lint
lovincyrus Jan 30, 2025
8609468
reset
lovincyrus Jan 30, 2025
25527d4
make refresh call async
lovincyrus Jan 30, 2025
a73d269
just right
lovincyrus Jan 30, 2025
08406c7
backoff strat, error handling
lovincyrus Jan 30, 2025
6bbf94c
move to pure function for refetch interval
lovincyrus Jan 31, 2025
4321669
clean up, clean up, clean up
lovincyrus Jan 31, 2025
78d9b08
lint
lovincyrus Jan 31, 2025
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
66 changes: 66 additions & 0 deletions web-admin/src/features/projects/status/ActionsCell.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<script lang="ts">
import IconButton from "@rilldata/web-common/components/button/IconButton.svelte";
import * as DropdownMenu from "@rilldata/web-common/components/dropdown-menu";
import ThreeDot from "@rilldata/web-common/components/icons/ThreeDot.svelte";
import { createRuntimeServiceCreateTrigger } from "@rilldata/web-common/runtime-client";
import { RefreshCcwIcon } from "lucide-svelte";
import { runtime } from "@rilldata/web-common/runtime-client/runtime-store";
import RefreshResourceConfirmDialog from "./RefreshResourceConfirmDialog.svelte";
import { useQueryClient } from "@tanstack/svelte-query";
import { getRuntimeServiceListResourcesQueryKey } from "@rilldata/web-common/runtime-client";

export let resourceKind: string;
export let resourceName: string;
export let canRefresh: boolean;

let isConfirmDialogOpen = false;
let isDropdownOpen = false;

const createTrigger = createRuntimeServiceCreateTrigger();
const queryClient = useQueryClient();

async function refresh(resourceKind: string, resourceName: string) {
await $createTrigger.mutateAsync({
instanceId: $runtime.instanceId,
data: {
resources: [{ kind: resourceKind, name: resourceName }],
},
});

await queryClient.invalidateQueries(
getRuntimeServiceListResourcesQueryKey($runtime.instanceId, undefined),
);
}
</script>

{#if canRefresh}
<DropdownMenu.Root bind:open={isDropdownOpen}>
<DropdownMenu.Trigger class="flex-none">
<IconButton rounded active={isDropdownOpen}>
<ThreeDot size="16px" />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="start">
<DropdownMenu.Item
class="font-normal flex items-center"
on:click={() => {
isConfirmDialogOpen = true;
}}
>
<div class="flex items-center">
<RefreshCcwIcon size="12px" />
<span class="ml-2">Refresh</span>
</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
{/if}

<RefreshResourceConfirmDialog
bind:open={isConfirmDialogOpen}
name={resourceName}
onRefresh={() => {
void refresh(resourceKind, resourceName);
isConfirmDialogOpen = false;
}}
/>
114 changes: 72 additions & 42 deletions web-admin/src/features/projects/status/ProjectResources.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,92 @@
import DelayedSpinner from "@rilldata/web-common/features/entity-management/DelayedSpinner.svelte";
import {
createRuntimeServiceCreateTrigger,
createRuntimeServiceListResources,
getRuntimeServiceListResourcesQueryKey,
V1ReconcileStatus,
type V1Resource,
type V1ListResourcesResponse,
createRuntimeServiceListResources,
} from "@rilldata/web-common/runtime-client";
import { runtime } from "@rilldata/web-common/runtime-client/runtime-store";
import { useQueryClient } from "@tanstack/svelte-query";
import Button from "web-common/src/components/button/Button.svelte";
import ProjectResourcesTable from "./ProjectResourcesTable.svelte";
import RefreshConfirmDialog from "./RefreshConfirmDialog.svelte";
import RefreshAllSourcesAndModelsConfirmDialog from "./RefreshAllSourcesAndModelsConfirmDialog.svelte";
import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors";

const queryClient = useQueryClient();
const createTrigger = createRuntimeServiceCreateTrigger();

let isConfirmDialogOpen = false;
let isReconciling = false;
let isRefreshConfirmDialogOpen = false;

const INITIAL_REFETCH_INTERVAL = 500; // Start at 500ms
const MAX_REFETCH_INTERVAL = 10_000; // Cap at 10s
const BACKOFF_FACTOR = 2; // Double each time
let currentRefetchInterval = INITIAL_REFETCH_INTERVAL;

$: ({ instanceId } = $runtime);

$: resources = createRuntimeServiceListResources(
instanceId,
// All resource "kinds"
undefined,
{
query: {
select: (data) => {
// Filter out the "ProjectParser" resource
return data.resources.filter(
(resource) =>
resource.meta.name.kind !== "rill.runtime.v1.ProjectParser",
);
},
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchInterval: isReconciling ? 500 : false,
},
},
);
function isResourceErrored(resource: V1Resource) {
return !!resource.meta.reconcileError;
}

$: isAnySourceOrModelReconciling = $resources?.data?.some(
(resource) =>
function isResourceReconciling(resource: V1Resource) {
return (
resource.meta.reconcileStatus ===
V1ReconcileStatus.RECONCILE_STATUS_PENDING ||
resource.meta.reconcileStatus ===
V1ReconcileStatus.RECONCILE_STATUS_RUNNING,
V1ReconcileStatus.RECONCILE_STATUS_RUNNING
);
}

function calculateRefetchInterval(
currentInterval: number,
data: V1ListResourcesResponse | undefined,
): number | false {
if (!data?.resources) {
return INITIAL_REFETCH_INTERVAL;
}

const hasErrors = data.resources.some(isResourceErrored);
const hasReconcilingResources = data.resources.some(isResourceReconciling);

if (hasErrors || !hasReconcilingResources) {
return false;
}

return Math.min(currentInterval * BACKOFF_FACTOR, MAX_REFETCH_INTERVAL);
}

$: resources = createRuntimeServiceListResources(instanceId, undefined, {
query: {
select: (data: V1ListResourcesResponse) => ({
...data,
// Filter out project parser and refresh triggers
resources: data?.resources?.filter(
(resource: V1Resource) =>
resource.meta.name.kind !== ResourceKind.ProjectParser &&
resource.meta.name.kind !== ResourceKind.RefreshTrigger,
),
}),
refetchInterval: (data) =>
calculateRefetchInterval(
$resources?.data ? currentRefetchInterval : INITIAL_REFETCH_INTERVAL,
data,
),
},
});

$: hasReconcilingResources = $resources.data?.resources?.some(
isResourceReconciling,
);

$: isReconciling = Boolean(hasReconcilingResources);

$: isRefreshButtonDisabled = hasReconcilingResources;

function refreshAllSourcesAndModels() {
isReconciling = true;
isReconciling = false;

void $createTrigger.mutateAsync({
instanceId,
Expand All @@ -59,17 +97,9 @@
});

void queryClient.invalidateQueries(
getRuntimeServiceListResourcesQueryKey(
instanceId,
// All resource "kinds"
undefined,
),
getRuntimeServiceListResourcesQueryKey(instanceId, undefined),
);
}

$: if (!isAnySourceOrModelReconciling) {
isReconciling = false;
}
</script>

<section class="flex flex-col gap-y-4 size-full">
Expand All @@ -78,11 +108,11 @@
<Button
type="secondary"
on:click={() => {
isRefreshConfirmDialogOpen = true;
isConfirmDialogOpen = true;
}}
disabled={isReconciling}
disabled={isRefreshButtonDisabled}
>
{#if isReconciling}
{#if isRefreshButtonDisabled}
Refreshing...
{:else}
Refresh all sources and models
Expand All @@ -96,12 +126,12 @@
<div class="text-red-500">
Error loading resources: {$resources.error?.message}
</div>
{:else if $resources.isSuccess}
<ProjectResourcesTable data={$resources.data} />
{:else if $resources.data}
<ProjectResourcesTable data={$resources?.data?.resources} {isReconciling} />
{/if}
</section>

<RefreshConfirmDialog
bind:open={isRefreshConfirmDialogOpen}
<RefreshAllSourcesAndModelsConfirmDialog
bind:open={isConfirmDialogOpen}
onRefresh={refreshAllSourcesAndModels}
/>
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<script lang="ts">
import Tag from "@rilldata/web-common/components/tag/Tag.svelte";
import { prettyResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors";
import {
prettyResourceKind,
ResourceKind,
} from "@rilldata/web-common/features/entity-management/resource-selectors";
import type { V1Resource } from "@rilldata/web-common/runtime-client";
import ResourceErrorMessage from "./ResourceErrorMessage.svelte";
import { getResourceKindTagColor } from "./display-utils";
Expand All @@ -9,8 +12,10 @@
import BasicTable from "@rilldata/web-common/components/table/BasicTable.svelte";
import RefreshCell from "./RefreshCell.svelte";
import NameCell from "./NameCell.svelte";
import ActionsCell from "./ActionsCell.svelte";

export let data: V1Resource[];
export let isReconciling: boolean;

const columns: ColumnDef<V1Resource, any>[] = [
{
Expand Down Expand Up @@ -62,11 +67,30 @@
date: info.getValue() as string,
}),
},
{
accessorKey: "actions",
header: "",
cell: ({ row }) => {
if (!isReconciling) {
return flexRender(ActionsCell, {
resourceKind: row.original.meta.name.kind,
resourceName: row.original.meta.name.name,
canRefresh:
row.original.meta.name.kind === ResourceKind.Model ||
row.original.meta.name.kind === ResourceKind.Source,
});
}
},
enableSorting: false,
meta: {
widthPercent: 0,
},
},
];
</script>

<BasicTable
{data}
{columns}
columnLayout="minmax(95px, 108px) minmax(100px, 3fr) 48px minmax(80px, 2fr) minmax(100px, 2fr) "
columnLayout="minmax(95px, 108px) minmax(100px, 3fr) 48px minmax(80px, 2fr) minmax(100px, 2fr) 56px"
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script lang="ts">
import {
AlertDialog,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@rilldata/web-common/components/alert-dialog/index.js";
import { Button } from "@rilldata/web-common/components/button/index.js";

export let open = false;
export let name: string;
export let onRefresh: () => void;

function handleRefresh() {
try {
onRefresh();
open = false;
} catch (error) {
console.error("Failed to refresh resource:", error);
}
}
</script>

<AlertDialog bind:open>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Refresh {name}?</AlertDialogTitle>
<AlertDialogDescription>
<div class="mt-1">
Refreshing this resource will update all dependent resources.
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<Button
type="plain"
on:click={() => {
open = false;
}}>Cancel</Button
>
<Button type="primary" on:click={handleRefresh}>Yes, refresh</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
10 changes: 8 additions & 2 deletions web-common/src/components/icons/LoadingSpinner.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
<script>
<script lang="ts">
export let size = "1em";
export let color = "currentColor";
</script>

<svg height={size} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg
height={size}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill={color}
>
<path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
Expand Down
13 changes: 8 additions & 5 deletions web-common/src/components/notifications/Notification.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import Button from "../button/Button.svelte";
import { onMount } from "svelte";
import WarningIcon from "../icons/WarningIcon.svelte";

const NOTIFICATION_TIMEOUT = 3500;
import LoadingSpinner from "../icons/LoadingSpinner.svelte";
import { NOTIFICATION_TIMEOUT } from "./constants";

export let location: "top" | "bottom" | "middle" = "bottom";
export let justify: "left" | "right" | "center" = "center";
Expand All @@ -18,8 +18,9 @@
$: ({ message, link, type, detail, options } = notification);

onMount(() => {
if (!options?.persisted && !link) {
setTimeout(onClose, NOTIFICATION_TIMEOUT);
if (!options?.persisted && !link && type !== "loading") {
const timeout = options?.timeout ?? NOTIFICATION_TIMEOUT;
setTimeout(onClose, timeout);
}
});
</script>
Expand All @@ -33,6 +34,8 @@
<div class="message-container" class:font-medium={detail}>
{#if type === "success"}
<Check size="18px" className="text-white" />
{:else if type === "loading"}
<LoadingSpinner size="18px" />
{:else if type == "error"}
<WarningIcon />
{/if}
Expand All @@ -48,7 +51,7 @@
</div>
{/if}

{#if options?.persisted}
{#if options?.persisted && type !== "loading"}
<div class="px-2 py-2 border-l">
<Button on:click={onClose} square>
<Close size="18px" color="#fff" />
Expand Down
Loading
Loading