Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion web-admin/src/features/branches/deployment-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export async function maybeRedirectToEditableDeployment(
const isActiveEditableDeployment =
editableDeployment && isActiveDeployment(editableDeployment);
// Editable deployment is inactive as well, project is probably hibernating, skip redirect.
if (!isActiveEditableDeployment) return;
if (!isActiveEditableDeployment && prodDeployment) return;

// If user is already in a specific deployment do not redirect.
// This method is meant as a convenience for direct links to unpublished project.
Expand Down
48 changes: 44 additions & 4 deletions web-admin/src/features/projects/ProjectCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,31 @@
import TooltipContent from "@rilldata/web-common/components/tooltip/TooltipContent.svelte";
import { createAdminServiceGetProject } from "../../client";
import ProjectAccessControls from "./ProjectAccessControls.svelte";
import ProjectCardActions from "@rilldata/web-admin/features/projects/ProjectCardActions.svelte";
import GuardedDeleteProjectConfirmation from "@rilldata/web-admin/features/projects/settings/GuardedDeleteProjectConfirmation.svelte";
import ProjectRenameDialog from "@rilldata/web-admin/features/projects/settings/ProjectRenameDialog.svelte";
import EditBranchDialog from "@rilldata/web-admin/features/edit-session/EditBranchDialog.svelte";

export let organization: string;
export let project: string;
let { organization, project }: { organization: string; project: string } =
$props();

// Check whether project is public or private
$: proj = createAdminServiceGetProject(organization, project);
let proj = $derived(createAdminServiceGetProject(organization, project));
let primaryBranch = $derived($proj.data?.project?.primaryBranch);

let hovering = $state(false);
let actionsOpen = $state(false);
let editProjectOpen = $state(false);
let renameProjectOpen = $state(false);
let deleteProjectOpen = $state(false);

function doesProjectNameIncludeUnderscores(project: string) {
return project.includes("_");
}
</script>

{#if $proj.data}
<Card href="/{organization}/{project}">
<Card href="/{organization}/{project}" bind:hovering>
<!-- Project name -->
<h2
class="text-fg-primary font-medium text-lg text-center px-4 {doesProjectNameIncludeUnderscores(
Expand All @@ -31,6 +42,19 @@
>
{project}
</h2>
<!-- Project actions -->
{#if hovering || actionsOpen}
<div class="absolute top-2.5 right-2.5 text-fg-secondary">
<ProjectCardActions
{organization}
{project}
bind:open={actionsOpen}
onEdit={() => (editProjectOpen = true)}
onRename={() => (renameProjectOpen = true)}
onDelete={() => (deleteProjectOpen = true)}
/>
</div>
{/if}
<!-- Permissions tag -->
<Tag>
<ProjectAccessControls {organization} {project}>
Expand Down Expand Up @@ -60,3 +84,19 @@
</div>
</Card>
{/if}

<ProjectRenameDialog {organization} {project} bind:open={renameProjectOpen} />

<GuardedDeleteProjectConfirmation
{organization}
{project}
bind:open={deleteProjectOpen}
button={false}
/>

<EditBranchDialog
bind:open={editProjectOpen}
{organization}
{project}
{primaryBranch}
/>
47 changes: 47 additions & 0 deletions web-admin/src/features/projects/ProjectCardActions.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts">
import * as Dropdown from "@rilldata/web-common/components/dropdown-menu";
import ThreeDot from "@rilldata/web-common/components/icons/ThreeDot.svelte";
import FeatherEditIcon from "@rilldata/web-common/components/icons/FeatherEditIcon.svelte";
import PencilIcon from "@rilldata/web-common/components/icons/PencilIcon.svelte";
import Trash from "@rilldata/web-common/components/icons/Trash.svelte";
import { ShareIcon } from "lucide-svelte";

let {
organization,
project,
open = $bindable(false),
onEdit,
onRename,
onDelete,
}: {
organization: string;
project: string;
open?: boolean;
onEdit: () => void;
onRename: () => void;
onDelete: () => void;
} = $props();
</script>

<Dropdown.Root bind:open>
<Dropdown.Trigger>
<ThreeDot size="16px" />
</Dropdown.Trigger>
<Dropdown.Content class="w-48" align="start" side="right">
<Dropdown.Item class="text-sm" onclick={onEdit}>
<FeatherEditIcon /> Edit
</Dropdown.Item>
<Dropdown.Item class="text-sm" onclick={onRename}>
<PencilIcon /> Rename
</Dropdown.Item>
<Dropdown.Item
href="/{organization}/{project}/-/dashboards?share=true"
class="text-sm"
>
<ShareIcon size={14} /> Share
</Dropdown.Item>
<Dropdown.Item class="text-sm text-destructive" onclick={onDelete}>
<Trash /> Delete
</Dropdown.Item>
</Dropdown.Content>
</Dropdown.Root>
48 changes: 28 additions & 20 deletions web-admin/src/features/projects/ProjectCards.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,44 @@
import { Button } from "@rilldata/web-common/components/button";
import { projectWelcomeEnabled } from "@rilldata/web-admin/features/welcome/project/welcome-status.ts";

export let organization: string;
let {
organization,
createProjectsPermission,
}: { organization: string; createProjectsPermission: boolean } = $props();

$: projs = createAdminServiceListProjectsForOrganization(organization, {
pageSize: 1000,
});
let projectsQuery = $derived(
createAdminServiceListProjectsForOrganization(organization, {
pageSize: 1000,
}),
);
let projects = $derived($projectsQuery.data?.projects ?? []);

let showNewProject = $derived(
projectWelcomeEnabled && createProjectsPermission,
);
</script>

<div class="flex flex-col gap-y-4">
<span
class="flex flex-row items-center text-fg-secondary text-base font-normal leading-normal"
>
<span class="grow">Check out your projects below.</span>
{#if projectWelcomeEnabled}
<Button type="primary" href="/{organization}/-/create-project">
Create new
{#if showNewProject}
<Button type="secondary" href="/{organization}/-/create-project">
+ New project
</Button>
{/if}
</span>

{#if $projs.data && $projs.data.projects?.length === 0}
<p class="text-fg-secondary text-xs">
This organization has no projects yet.
</p>
{:else if $projs.data && $projs.data.projects?.length > 0}
<ol class="flex gap-6 flex-wrap">
{#each $projs.data.projects as proj}
<li>
<ProjectCard {organization} project={proj.name} />
</li>
{/each}
</ol>
{/if}
<ol class="flex gap-6 flex-wrap">
{#each projects as proj (proj.name)}
<li>
<ProjectCard {organization} project={proj.name} />
</li>
{:else}
<p class="text-fg-secondary text-xs">
This organization has no projects yet.
</p>
{/each}
</ol>
</div>
50 changes: 2 additions & 48 deletions web-admin/src/features/projects/settings/DeleteProject.svelte
Original file line number Diff line number Diff line change
@@ -1,62 +1,16 @@
<script lang="ts">
import { goto } from "$app/navigation";
import {
createAdminServiceDeleteProject,
getAdminServiceGetProjectQueryKey,
getAdminServiceListProjectsForOrganizationQueryKey,
} from "@rilldata/web-admin/client";
import SettingsContainer from "@rilldata/web-admin/features/organizations/settings/SettingsContainer.svelte";
import { Button } from "@rilldata/web-common/components/button";
import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus";
import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryClient";
import AlertDialogGuardedConfirmation from "@rilldata/web-common/components/alert-dialog/alert-dialog-guarded-confirmation.svelte";
import GuardedDeleteProjectConfirmation from "@rilldata/web-admin/features/projects/settings/GuardedDeleteProjectConfirmation.svelte";

let { organization, project }: { organization: string; project: string } =
$props();

const deleteProjectMutation = createAdminServiceDeleteProject();

let deleteProjectResult = $derived($deleteProjectMutation);

async function deleteProject() {
await $deleteProjectMutation.mutateAsync({
org: organization,
project,
});

// Clean up cache before navigating to ensure the new page has fresh data
queryClient.removeQueries({
queryKey: getAdminServiceGetProjectQueryKey(organization, project),
});
await queryClient.invalidateQueries({
queryKey:
getAdminServiceListProjectsForOrganizationQueryKey(organization),
});

eventBus.emit("notification", {
message: "Deleted project",
});

await goto(`/${organization}`);
}
</script>

<SettingsContainer title="Delete Project">
Permanently delete this project and all of its contents from the Rill
platform. This action is not reversible — please continue with caution.

{#snippet action()}
<AlertDialogGuardedConfirmation
title="Delete Project?"
description={`The project "${project}" will be permanently deleted along with all its dashboards, data, and settings. This action cannot be undone.`}
confirmText={`delete ${project}`}
confirmButtonText="Delete"
confirmButtonType="destructive"
loading={deleteProjectResult.isPending}
error={deleteProjectResult.error?.message}
onConfirm={deleteProject}
>
<Button type="destructive">Delete Project</Button>
</AlertDialogGuardedConfirmation>
<GuardedDeleteProjectConfirmation {organization} {project} />
{/snippet}
</SettingsContainer>
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script lang="ts">
import { Button } from "@rilldata/web-common/components/button/index.ts";
import AlertDialogGuardedConfirmation from "@rilldata/web-common/components/alert-dialog/alert-dialog-guarded-confirmation.svelte";
import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryClient.ts";
import {
createAdminServiceDeleteProject,
getAdminServiceGetProjectQueryKey,
getAdminServiceListProjectsForOrganizationQueryKey,
} from "@rilldata/web-admin/client/index.ts";
import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus.ts";
import { goto } from "$app/navigation";

let {
organization,
project,
open = $bindable(false),
button = true,
}: {
organization: string;
project: string;
open?: boolean;
button?: boolean;
} = $props();

const deleteProjectMutation = createAdminServiceDeleteProject();

let deleteProjectResult = $derived($deleteProjectMutation);

async function deleteProject() {
await $deleteProjectMutation.mutateAsync({
org: organization,
project,
});

// Clean up cache before navigating to ensure the new page has fresh data
queryClient.removeQueries({
queryKey: getAdminServiceGetProjectQueryKey(organization, project),
});
await queryClient.invalidateQueries({
queryKey:
getAdminServiceListProjectsForOrganizationQueryKey(organization),
});

eventBus.emit("notification", {
message: "Deleted project",
});

await goto(`/${organization}`);
}
</script>

<AlertDialogGuardedConfirmation
bind:open
title="Delete Project?"
description={`The project "${project}" will be permanently deleted along with all its dashboards, data, and settings. This action cannot be undone.`}
confirmText={`delete ${project}`}
confirmButtonText="Delete"
confirmButtonType="destructive"
loading={deleteProjectResult.isPending}
error={deleteProjectResult.error?.message}
onConfirm={deleteProject}
>
{#if button}
<Button type="destructive">Delete Project</Button>
{:else}
<div class="hidden"></div>
{/if}
</AlertDialogGuardedConfirmation>
Loading
Loading