Skip to content

Commit

Permalink
Public URLs Table UI (#5366)
Browse files Browse the repository at this point in the history
* rename CreateShareableURLForm to CreatePublicURLForm

* create public urls route, wip

* add settings project route to admin tabs

* reorg routes, only settings

* remove unused

* list all magic auth tokens in table

* lint

* file rename

* style left sidebar and right content

* padding tweak

* include option to go to public urls after creation

* use name in created by col

* ui for row deletion

* wire up magic auth token deletion from trash icon

* eslint, add no public url cta

* refetch queries after actions

* reorg project settings layout

* comments

* mutable table data

* lint

* add V1MagicAuthToken to runtime swagger, orval gen

* goto public url on click

* update copy

* remove general from proj settings

* clean up

* qol and style changes

* orval gen with url

* remove unused

* update copy

* orval gen

* wip

* good stop for migrating svelte table

* onDelete is back, wip

* actions row, add copy url

* clean up

* Add title to MagicAuthToken, db migration

* orval gen runtime

* proto gen

* use title, then fallback to metrics view

* lint

* implement forward cursor-based pagination

* dashboard link clickable

* use all tokens, placeholder public url name col

* forgot to save

* action icon

* ability to edit public url name

* clean up, set dashboard title as the default value in the form

* fix name input layout

* fix copy to clipboard

* hide copy button when there is no url in previous urls

* clean up

* confirm deletion, fix data reactivity

* clean up

* hover bg button

* title -> name in magic auth token

* remap dashboardTitle to tokens, switch back to next

* polish

* update names

* lint

* fix column def ts error

* bind isOpen to share dashboard button selected

* fix copy nits

* use vaildSpec's title

* url label

* modify actions component

* polish with janet

* offline feedback

* name -> title

* add desctrutive type to menu item

* delete confirm dialog

* make it scrollable

* apply classname prop

* lint

* wip

* truly infinite scrolling

* clean up

* wip

* finally

* clean up

* sort by createdOn client side

* fix conditionals

* polish

* fixme

* pr feedback

* lint
  • Loading branch information
lovincyrus authored Sep 19, 2024
1 parent f26fa4c commit ca3fc8b
Show file tree
Hide file tree
Showing 30 changed files with 3,141 additions and 2,369 deletions.
2 changes: 2 additions & 0 deletions admin/auth_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ type IssueMagicAuthTokenOptions struct {
MetricsViewFilterJSON string
MetricsViewFields []string
State string
Title string
}

// IssueMagicAuthToken generates and persists a new magic auth token for a project.
Expand All @@ -199,6 +200,7 @@ func (s *Service) IssueMagicAuthToken(ctx context.Context, opts *IssueMagicAuthT
MetricsViewFilterJSON: opts.MetricsViewFilterJSON,
MetricsViewFields: opts.MetricsViewFields,
State: opts.State,
Title: opts.Title,
})
if err != nil {
return nil, err
Expand Down
2 changes: 2 additions & 0 deletions admin/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@ type MagicAuthToken struct {
MetricsViewFilterJSON string `db:"metrics_view_filter_json"`
MetricsViewFields []string `db:"metrics_view_fields"`
State string `db:"state"`
Title string `db:"title"`
}

// MagicAuthTokenWithUser is a MagicAuthToken with additional information about the user who created it.
Expand All @@ -647,6 +648,7 @@ type InsertMagicAuthTokenOptions struct {
MetricsViewFilterJSON string
MetricsViewFields []string
State string
Title string
}

// AuthClient is a client that requests and consumes auth tokens.
Expand Down
1 change: 1 addition & 0 deletions admin/database/postgres/migrations/0045.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ CREATE TABLE billing_issues (
FOREIGN KEY (org_id) REFERENCES orgs (id) ON DELETE CASCADE,
CONSTRAINT billing_issues_org_id_type_unique UNIQUE (org_id, type)
);

1 change: 1 addition & 0 deletions admin/database/postgres/migrations/0046.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE magic_auth_tokens ADD COLUMN title TEXT NOT NULL DEFAULT '';
6 changes: 3 additions & 3 deletions admin/database/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -1150,9 +1150,9 @@ func (c *connection) InsertMagicAuthToken(ctx context.Context, opts *database.In

res := &magicAuthTokenDTO{}
err = c.getDB(ctx).QueryRowxContext(ctx, `
INSERT INTO magic_auth_tokens (id, secret_hash, secret, secret_encryption_key_id, project_id, expires_on, created_by_user_id, attributes, metrics_view, metrics_view_filter_json, metrics_view_fields, state)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`,
opts.ID, opts.SecretHash, encSecret, encKeyID, opts.ProjectID, opts.ExpiresOn, opts.CreatedByUserID, opts.Attributes, opts.MetricsView, opts.MetricsViewFilterJSON, opts.MetricsViewFields, opts.State,
INSERT INTO magic_auth_tokens (id, secret_hash, secret, secret_encryption_key_id, project_id, expires_on, created_by_user_id, attributes, metrics_view, metrics_view_filter_json, metrics_view_fields, state, title)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *`,
opts.ID, opts.SecretHash, encSecret, encKeyID, opts.ProjectID, opts.ExpiresOn, opts.CreatedByUserID, opts.Attributes, opts.MetricsView, opts.MetricsViewFilterJSON, opts.MetricsViewFields, opts.State, opts.Title,
).StructScan(res)
if err != nil {
return nil, parseErr("magic auth token", err)
Expand Down
2 changes: 2 additions & 0 deletions admin/server/magic_tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func (s *Server) IssueMagicAuthToken(ctx context.Context, req *adminv1.IssueMagi
MetricsView: req.MetricsView,
MetricsViewFields: req.MetricsViewFields,
State: req.State,
Title: req.Title,
}

if req.TtlMinutes != 0 {
Expand Down Expand Up @@ -284,6 +285,7 @@ func (s *Server) magicAuthTokenToPB(tkn *database.MagicAuthTokenWithUser, org *d
MetricsViewFilter: metricsViewFilter,
MetricsViewFields: tkn.MetricsViewFields,
State: tkn.State,
Title: tkn.Title,
}
if tkn.ExpiresOn != nil {
res.ExpiresOn = timestamppb.New(*tkn.ExpiresOn)
Expand Down
5 changes: 5 additions & 0 deletions proto/gen/rill/admin/v1/admin.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,9 @@ paths:
state:
type: string
description: Optional state to store with the token. Can be fetched with GetCurrentMagicAuthToken.
title:
type: string
description: Optional public url title to store with the token.
tags:
- AdminService
/v1/organizations/{organization}/projects/{project}/upload-assets:
Expand Down Expand Up @@ -4330,6 +4333,8 @@ definitions:
type: string
state:
type: string
title:
type: string
v1MemberUser:
type: object
properties:
Expand Down
4,724 changes: 2,372 additions & 2,352 deletions proto/gen/rill/admin/v1/api.pb.go

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions proto/gen/rill/admin/v1/api.pb.validate.go

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

3 changes: 3 additions & 0 deletions proto/rill/admin/v1/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1639,6 +1639,8 @@ message IssueMagicAuthTokenRequest {
repeated string metrics_view_fields = 6;
// Optional state to store with the token. Can be fetched with GetCurrentMagicAuthToken.
string state = 7;
// Optional public url title to store with the token.
string title = 8;
}

message IssueMagicAuthTokenResponse {
Expand Down Expand Up @@ -2241,6 +2243,7 @@ message MagicAuthToken {
rill.runtime.v1.Expression metrics_view_filter = 10;
repeated string metrics_view_fields = 11;
string state = 12;
string title = 15;
}

message VirtualFile {
Expand Down
3 changes: 3 additions & 0 deletions web-admin/src/client/gen/index.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ If empty, all dimensions and measures are accessible. */
metricsViewFields?: string[];
/** Optional state to store with the token. Can be fetched with GetCurrentMagicAuthToken. */
state?: string;
/** Optional public url title to store with the token. */
title?: string;
};

export type AdminServiceListMagicAuthTokensParams = {
Expand Down Expand Up @@ -827,6 +829,7 @@ export interface V1MagicAuthToken {
metricsViewFilter?: V1Expression;
metricsViewFields?: string[];
state?: string;
title?: string;
}

export interface V1ListWhitelistedDomainsResponse {
Expand Down
4 changes: 2 additions & 2 deletions web-admin/src/components/nav/LeftNavItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
</script>

<a class:selected href={link}>
{label}
<span class="text-slate-900">{label}</span>
</a>

<style lang="postcss">
a {
@apply p-2 flex gap-x-1 items-center;
@apply rounded-sm text-slate-900;
@apply rounded-sm;
@apply text-sm font-medium;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
import { copyToClipboard } from "@rilldata/web-common/lib/actions/copy-to-clipboard";
export let createMagicAuthTokens: boolean;
let isOpen = false;
</script>

<Popover>
<Popover bind:open={isOpen}>
<PopoverTrigger asChild let:builder>
<Button type="secondary" builders={[builder]}>Share</Button>
<Button type="secondary" builders={[builder]} selected={isOpen}
>Share</Button
>
</PopoverTrigger>
<PopoverContent align="end" class="w-[402px] p-0">
<Tabs>
Expand Down
2 changes: 2 additions & 0 deletions web-admin/src/features/navigation/nav-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export function isProjectPage(page: Page): boolean {
page.route.id === "/[organization]/[project]/-/reports" ||
page.route.id === "/[organization]/[project]/-/alerts" ||
page.route.id === "/[organization]/[project]/-/status" ||
page.route.id === "/[organization]/[project]/-/settings" ||
page.route.id === "/[organization]/[project]/-/settings/public-urls" ||
!!page.route?.id?.startsWith("/[organization]/[project]/-/request-access")
);
}
Expand Down
20 changes: 19 additions & 1 deletion web-admin/src/features/projects/ProjectTabs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
route: `/${organization}/${project}/-/status`,
label: "Status",
},
{
// TODO: Change this back to `/${organization}/${project}/-/settings`
// Once project settings are implemented
route: `/${organization}/${project}/-/settings/public-urls`,
label: "Settings",
},
];
if (data.projectPermissions?.manageProject) {
Expand All @@ -50,12 +56,24 @@
);
$: tabs = $tabsQuery.data;
function isSelected(tabRoute: string, currentPathname: string) {
if (tabRoute.endsWith(`/${organization}/${project}`)) {
// For the dashboard (root) route, only exact match
return currentPathname === tabRoute;
}
const isExactMatch = currentPathname === tabRoute;
const isSubpage = currentPathname.startsWith(tabRoute + "/");
return isExactMatch || isSubpage;
}
</script>

{#if tabs}
<nav>
{#each tabs as tab (tab.route)}
<a href={tab.route} class:selected={pathname === tab.route}>
<a href={tab.route} class:selected={isSelected(tab.route, pathname)}>
{tab.label}
{#if tab.label === "Status"}
<ProjectGlobalStatusIndicator {organization} {project} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
<div class="border rounded-md">
<Table.Root {...$tableAttrs}>
<Table.Header>
{#each $headerRows as headerRow}
{#each $headerRows as headerRow (headerRow.id)}
<Subscribe rowAttrs={headerRow.attrs()}>
<Table.Row>
{#each headerRow.cells as cell (cell.id)}
Expand Down
64 changes: 59 additions & 5 deletions web-admin/src/features/public-urls/CreatePublicURLForm.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<script lang="ts">
import { page } from "$app/stores";
import { createAdminServiceIssueMagicAuthToken } from "@rilldata/web-admin/client";
import {
createAdminServiceIssueMagicAuthToken,
getAdminServiceListMagicAuthTokensQueryKey,
} from "@rilldata/web-admin/client";
import { Button } from "@rilldata/web-common/components/button";
import Label from "@rilldata/web-common/components/forms/Label.svelte";
import Switch from "@rilldata/web-common/components/forms/Switch.svelte";
Expand All @@ -11,13 +14,15 @@
import { defaults, superForm } from "sveltekit-superforms";
import { yup } from "sveltekit-superforms/adapters";
import { object, string } from "yup";
import { useQueryClient } from "@tanstack/svelte-query";
import {
convertDateToMinutes,
getMetricsViewFields,
getSanitizedDashboardStateParam,
hasDashboardWhereFilter,
} from "./form-utils";
const queryClient = useQueryClient();
const {
dashboardStore,
metricsViewName,
Expand All @@ -29,6 +34,8 @@
$: ({ organization, project } = $page.params);
$: isTitleEmpty = $form.title.trim() === "";
$: metricsViewFields = getMetricsViewFields(
$dashboardStore,
$visibleDimensions,
Expand All @@ -44,14 +51,16 @@
let setExpiration = false;
let apiError: string;
const formId = "create-shareable-url-form";
const formId = "create-public-url-form";
const initialValues = {
expiresAt: null,
title: "",
};
const validationSchema = object({
expiresAt: string().nullable(),
title: string().required("Title is required"),
});
const issueMagicAuthToken = createAdminServiceIssueMagicAuthToken();
Expand All @@ -78,6 +87,7 @@
? convertDateToMinutes(values.expiresAt).toString()
: undefined,
state: sanitizedState ? sanitizedState : undefined,
title: values.title,
},
});
token = _token;
Expand All @@ -86,6 +96,10 @@
`${window.location.origin}/${organization}/${project}/-/share/${token}`,
"URL copied to clipboard",
);
void queryClient.invalidateQueries(
getAdminServiceListMagicAuthTokensQueryKey(organization, project),
);
} catch (error) {
const typedError = error as HTTPError;
apiError = typedError.response?.data?.message ?? typedError.message;
Expand All @@ -111,12 +125,23 @@
{#if !token}
<form id={formId} on:submit|preventDefault={submit} use:enhance>
<div class="information-container">
<h3>Create a public URL that you can send to anyone.</h3>
<h3>Create a shareable public URL for this view</h3>
<ul>
<li>Measures and dimensions will be limited to current visible set.</li>
<li>Filters will be locked and hidden.</li>
</ul>

<div class="name-input-container">
<Label for="name-input" class="text-xs">URL label</Label>
<input
id="name-input"
type="text"
bind:value={$form.title}
placeholder="Name this URL"
class="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>

<!-- Filters -->
{#if hasWhereFilter}
<div>
Expand All @@ -139,7 +164,7 @@
</div>
{#if setExpiration}
<div class="expires-at-container">
<label for="expires-at" class="expires-at-label">
<label for="expires-at" class="expires-at-label w-1/3">
Access expires
</label>
<!-- TODO: use a Rill date picker, once we have one that can select a single day -->
Expand All @@ -151,12 +176,18 @@
min={new Date(Date.now() + 24 * 60 * 60 * 1000)
.toISOString()
.slice(0, 10)}
class="w-2/3"
/>
</div>
{/if}
</div>

<Button type="primary" disabled={$submitting} form={formId} submitForm>
<Button
type="primary"
disabled={$submitting || isTitleEmpty}
form={formId}
submitForm
>
Create and copy URL
</Button>

Expand Down Expand Up @@ -192,6 +223,10 @@
@apply list-disc list-inside;
}
.name-input-container {
@apply flex flex-col gap-y-1;
}
.has-expiration-container {
@apply flex items-center gap-x-2;
}
Expand All @@ -204,4 +239,23 @@
.expires-at-label {
@apply text-slate-500 font-medium;
}
input {
@apply size-full outline-none border-0;
}
#name-input {
@apply flex justify-center items-center overflow-hidden;
@apply h-8 pl-2 w-full;
@apply border border-gray-300 rounded-sm;
@apply text-xs;
}
#name-input:focus-within {
@apply border-primary-500;
}
#name-input::placeholder {
@apply text-xs;
}
</style>
Loading

1 comment on commit ca3fc8b

@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.