Skip to content

Commit

Permalink
Cloud UI: create page for a project's Scheduled Reports (#3241)
Browse files Browse the repository at this point in the history
* Display a project's dashboards in a table

* Refactor `Tag` component to accept a `color` prop

* Change `Search` component to white color

* Add `ContentContainer` helper layout component

* Edit copy

* Create `Tab` components using `svelte-headlessui`

* Use tabs to navigate between a project's subroutes

* Display scheduled reports in a table

* Move file

* Rename component

* Add icon

* Add file

* Fix import

* Remove project hero

* Add `ContentContainer`

* Fix issues with `dashboards/listing` directory

* Add status badge to "Logs" tab

* Hide "Reports" tab for now

* Fix import

* Fix CSS selector

* Fix import

* Match Figma mocks

* Only show Logs tab to admins

* Add fallback/localized error for dashboards table

* Add URL slug to dashboard table row

* Better sort copy

* Restore the `ProjectHero` while no project tabs

* Better "last refresh" date

* Fix up sorting behavior

* Move type, add comment

* Make the click target bigger

* Add tooltip for last refresh date

* Create dedicated slot for an empty table

* Nit

* Make whole cell clickable

* Fix svelte-check

* Better typing

* Decompose object

* Move `ProjectTabs` into runtime context

* Fix refresh of logs page

* Remove old padding

* Center the dashboards table

* Add "No logs" view

* Bugfix

* Use `$table` not `get(table)`

* Reset table filter when changing projects

* Hide sort button for now

* Show reports tab

* Fix row padding

* Rename "InfoCell" -> "CompositeCell"

* Center the table

* Match dashboards table

* Fix padding on other Search instances

* Use `luxon`

* Add report icon; make entire row clickable

* Use `CheckCircleOutline` icon

* Clean up header

* Add table empty state

* Adjust left padding in table header

* Creating `listing` subdirectory

* Remove redirect from project to dashboard

* Fetch reports from runtime

* Add error state

* Add 0 reports state

* Move code

* Remove unused props

* Move data fetching to `selectors.ts`

* Format report `owner`

* Account for reports not-yet-run

* Use `cronstrue` to translate cron expressions to natural language

* Add owner's name; fix status badge

* Fix lint
  • Loading branch information
ericpgreen2 authored Nov 8, 2023
1 parent 4279acb commit 03f9bdf
Show file tree
Hide file tree
Showing 19 changed files with 441 additions and 9 deletions.
10 changes: 10 additions & 0 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion web-admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@
"@fontsource/fira-mono": "^4.5.0",
"@playwright/test": "^1.25.0",
"@rgossiaux/svelte-headlessui": "^2.0.0",
"@rilldata/svelte-query": "^4.29.20-0.0.1",
"@sveltejs/adapter-static": "^1.0.0",
"@sveltejs/kit": "^1.5.0",
"@rilldata/svelte-query": "^4.29.20-0.0.1",
"@tanstack/svelte-query": "npm:@rilldata/[email protected]",
"@types/cookie": "^0.5.1",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"autoprefixer": "^10.4.13",
"axios": "^0.27.2",
"cookie": "^0.4.1",
"cronstrue": "^2.41.0",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^4.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
class="pl-2 pr-4 py-2 max-w-[800px] flex items-center gap-x-2 bg-slate-100"
>
<!-- Search bar -->
<div class="px-4 grow">
<div class="px-2 grow">
<Search placeholder="Search" autofocus={false} bind:value={filter} />
</div>

Expand Down
9 changes: 4 additions & 5 deletions web-admin/src/features/projects/ProjectTabs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@
route: `/${organization}/${project}`,
label: "Dashboards",
},
// Hide until releasing Scheduled Reports
// {
// route: `/${organization}/${project}/-/reports`,
// label: "Reports",
// },
{
route: `/${organization}/${project}/-/reports`,
label: "Reports",
},
];
const adminTabs = [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts">
import ReportIcon from "@rilldata/web-common/components/icons/ReportIcon.svelte";
</script>

<div
class="m-auto mt-20 pb-10 flex-col justify-center items-center gap-y-4 inline-flex"
>
<div class="flex flex-col justify-center items-center">
<div class="relative">
<ReportIcon className="text-slate-300 w-12 h-12" />
</div>
</div>
<div
class="flex flex-col gap-y-2 justify-center items-center text-center text-sm"
>
<div class="text-gray-600 font-semibold">
You don't have any reports yet
</div>
<div>
<span style="text-gray-500 font-normal"
>Learn how to create a report in our
</span><a href="https://docs.rilldata.com/" target="_blank">docs</a>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script lang="ts">
</script>

<div
class="m-auto mt-20 pb-10 flex-col justify-center items-center gap-y-4 inline-flex"
>
<div
class="flex flex-col gap-y-2 justify-center items-center text-center text-sm"
>
<div class="text-gray-600 font-semibold">Error loading reports</div>
<div class="text-gray-500 font-normal">
If this error persists, please contact support.
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<script lang="ts">
import Spinner from "@rilldata/web-common/features/entity-management/Spinner.svelte";
import { EntityStatus } from "@rilldata/web-common/features/entity-management/types";
import type { V1Resource } from "@rilldata/web-common/runtime-client";
import { runtime } from "@rilldata/web-common/runtime-client/runtime-store";
import { ColumnDef, flexRender } from "@tanstack/svelte-table";
import Table from "../../../components/table/Table.svelte";
import { useReports } from "../selectors";
import NoReportsCTA from "./NoReportsCTA.svelte";
import ReportsError from "./ReportsError.svelte";
import ReportsTableCompositeCell from "./ReportsTableCompositeCell.svelte";
import ReportsTableEmpty from "./ReportsTableEmpty.svelte";
import ReportsTableHeader from "./ReportsTableHeader.svelte";
export let organization: string;
export let project: string;
$: reports = useReports($runtime.instanceId);
/**
* Table column definitions.
* - "composite": Renders all dashboard data in a single cell.
* - Others: Used for sorting and filtering but not displayed.
*
* Note: TypeScript error prevents using `ColumnDef<DashboardResource, string>[]`.
* Relevant issues:
* - https://github.com/TanStack/table/issues/4241
* - https://github.com/TanStack/table/issues/4302
*/
const columns: ColumnDef<V1Resource, string>[] = [
{
id: "composite",
cell: (info) =>
flexRender(ReportsTableCompositeCell, {
organization: organization,
project: project,
id: info.row.original.meta.name.name,
title: info.row.original.report.spec.title,
lastRun:
info.row.original.report.state.executionHistory[0]?.reportTime,
frequency: info.row.original.report.spec.refreshSchedule.cron,
ownerId:
info.row.original.report.spec.annotations["admin_owner_user_id"],
lastRunErrorMessage:
info.row.original.report.state.executionHistory[0]?.errorMessage,
}),
},
{
id: "name",
accessorFn: (row) => row.meta.name.name,
},
{
id: "lastRun",
accessorFn: (row) => row.report.state.currentExecution.reportTime,
},
// {
// id: "nextRun",
// accessorFn: (row) => row.nextRun,
// },
// {
// id: "actions",
// cell: ({ row }) =>
// flexRender(ReportsTableActionCell, {
// title: row.original.name,
// }),
// },
];
const columnVisibility = {
name: false,
lastRun: false,
};
</script>

{#if $reports.isLoading}
<div class="m-auto mt-20">
<Spinner status={EntityStatus.Running} size="24px" />
</div>
{:else if $reports.isError}
<ReportsError />
{:else if $reports.isSuccess}
{#if $reports.data.resources.length === 0}
<NoReportsCTA />
{:else}
<Table {columns} data={$reports?.data?.resources} {columnVisibility}>
<ReportsTableHeader slot="header" />
<ReportsTableEmpty slot="empty" />
</Table>
{/if}
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import ThreeDot from "@rilldata/web-common/components/icons/ThreeDot.svelte";
export let reportName: string;
</script>

<button
class="text-gray-500 hover:text-blue-500 hover:bg-slate-200 grid place-items-center rounded-sm"
on:click={() => console.log(`open actions for ${reportName}`)}
>
<ThreeDot size="16px" />
</button>

<!-- TODO: Menu for admins -->

<!-- TODO: Menu for viewers -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script lang="ts">
import CancelCircleInverse from "@rilldata/web-common/components/icons/CancelCircleInverse.svelte";
import CheckCircleOutline from "@rilldata/web-common/components/icons/CheckCircleOutline.svelte";
import ReportIcon from "@rilldata/web-common/components/icons/ReportIcon.svelte";
import cronstrue from "cronstrue";
import { createAdminServiceListProjectMembers } from "../../../client";
import { formatDateToCustomString } from "../tableUtils";
export let organization: string;
export let project: string;
export let id: string;
export let title: string;
export let lastRun: string | undefined;
export let frequency: string;
export let ownerId: string;
export let lastRunErrorMessage: string | undefined;
const humanReadableFrequency = cronstrue.toString(frequency);
const membersQuery = createAdminServiceListProjectMembers(
organization,
project
);
$: owner = $membersQuery.data?.members.find(
(member) => member.userId === ownerId
);
</script>

<a href={`reports/${id}`} class="flex flex-col gap-y-0.5 group px-4 py-[5px]">
<div class="flex gap-x-2 items-center">
<ReportIcon size={"14px"} className="text-slate-500" />
<div class="text-gray-700 text-sm font-semibold group-hover:text-blue-600">
{title}
</div>
{#if lastRun}
{#if lastRunErrorMessage}
<CancelCircleInverse className="text-red-500" />
{:else}
<CheckCircleOutline className="text-blue-500" />
{/if}
{/if}
</div>
<div class="flex gap-x-1 text-gray-500 text-xs font-normal">
{#if !lastRun}
<span>Hasn't run yet</span>
{:else}
<span>Last run {formatDateToCustomString(new Date(lastRun))}</span>
{/if}
<span>•</span>
<span>{humanReadableFrequency}</span>
<span>•</span>
<span>Created by {owner?.userName || "a project admin"}</span>
</div>
</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<span class="text-gray-500"> No reports found. </span>
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<script lang="ts">
import { Search } from "@rilldata/web-common/components/search";
import type { Table } from "@tanstack/table-core/src/types";
import { getContext } from "svelte";
import type { Readable } from "svelte/store";
const table = getContext("table") as Readable<Table<unknown>>;
// Search
let filter = "";
$: filterTable(filter);
function filterTable(filter: string) {
$table.setGlobalFilter(filter);
}
// Number of reports
$: numReports = $table.getRowModel().rows.length;
// Sort
// function sortAlphabetically() {
// $table.setSorting([{ id: "monocolumn", desc: false }]);
// }
// function sortByMostRecentlyRun() {
// $table.setSorting([{ id: "lastRun", desc: true }]);
// }
// function sortByNextToRun() {
// $table.setSorting([{ id: "monocolumn", desc: false }]);
// }
// let openSortMenu = false;
// function closeSortMenu() {
// openSortMenu = false;
// }
</script>

<thead>
<tr>
<td
class="pl-2 pr-4 py-2 max-w-[800px] flex items-center gap-x-2 bg-slate-100"
>
<!-- Search bar -->
<div class="px-2 grow">
<Search placeholder="Search" autofocus={false} bind:value={filter} />
</div>

<!-- Spacer -->
<div class="grow" />

<!-- filter menu button (future work) -->
<!-- <Button on:click={() => console.log("open filter menu")} type="secondary">
<span>Filter</span>
<CaretDownIcon />
</Button> -->

<!-- Number of reports -->
<span class="shrink-0"
>{numReports} report{numReports !== 1 ? "s" : ""}</span
>

<!-- Sort button -->
<!-- <WithTogglableFloatingElement active={openSortMenu}>
<Button on:click={() => (openSortMenu = true)} type="secondary">
<span>Sort</span>
<CaretDownIcon />
</Button>
<Menu
slot="floating-element"
minWidth="0px"
on:item-select={closeSortMenu}
on:click-outside={closeSortMenu}
on:escape={closeSortMenu}
>
<MenuItem on:select={sortAlphabetically}>Alphabetical</MenuItem>
<MenuItem on:select={sortByMostRecentlyRun}
>Most recently run</MenuItem
>
<MenuItem on:select={sortByNextToRun} disabled>Next to run</MenuItem>
</Menu>
</WithTogglableFloatingElement> -->
</td>
</tr>
</thead>

<!--
Rounded table corners are tricky:
- `border-radius` does not apply to table elements when `border-collapse` is `collapse`.
- You can only apply `border-radius` to <td>, not <tr> or <table>.
-->
<style lang="postcss">
thead tr td {
@apply border-y;
}
thead tr td:first-child {
@apply border-l rounded-tl-sm;
}
thead tr td:last-child {
@apply border-r rounded-tr-sm;
}
</style>
18 changes: 18 additions & 0 deletions web-admin/src/features/scheduled-reports/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors";
import {
createRuntimeServiceGetResource,
createRuntimeServiceListResources,
} from "@rilldata/web-common/runtime-client";

export function useReports(instanceId: string) {
return createRuntimeServiceListResources(instanceId, {
kind: ResourceKind.Report,
});
}

export function useReport(instanceId: string, name: string) {
return createRuntimeServiceGetResource(instanceId, {
"name.name": name,
"name.kind": ResourceKind.Report,
});
}
Loading

1 comment on commit 03f9bdf

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.