Skip to content
Merged
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
41 changes: 26 additions & 15 deletions web-admin/src/routes/[organization]/[project]/-/edit/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import { InfoIcon } from "lucide-svelte";
import { overlay } from "@rilldata/web-common/layout/overlay-store";
import BlockingOverlayContainer from "@rilldata/web-common/layout/BlockingOverlayContainer.svelte";
import { fileArtifacts } from "@rilldata/web-common/features/entity-management/file-artifacts.ts";

$: organization = $page.params.organization;
$: project = $page.params.project;
Expand Down Expand Up @@ -83,8 +84,12 @@
// keeps the UI in loading state while the backend transitions STOPPED → PENDING → RUNNING.
let starting = false;

// Wait for `primaryProjectQuery` too: the cloud readonly notice is gated on
// `hasPrimaryDeployment`, and it must be registered before `<slot />` renders
// any file editor (getReadonlyNotice reads the notice non-reactively).
$: isLoading =
$projectQuery.isPending ||
$primaryProjectQuery.isPending ||
starting ||
deploymentStatus === V1DeploymentStatus.DEPLOYMENT_STATUS_PENDING;

Expand Down Expand Up @@ -119,7 +124,18 @@
});
};

setCloudReadonlyNotice(envEditDisabled);
// Only surface the env notice once the project has a primary deployment.
// Fail closed: env becomes editable only once we've positively confirmed the
// project has no primary deployment. A failed or otherwise inconclusive lookup
// keeps the notice set, so a published project never exposes editable `.env`
// files while the deployment state is unknown.
// `isLoading` blocks `<slot />` until `primaryProjectQuery` resolves, so this
// has run before any file editor reads the notice.
$: if (!$primaryProjectQuery.isPending) {
const envEditable = $primaryProjectQuery.isSuccess && !hasPrimaryDeployment;
setCloudReadonlyNotice(envEditable ? undefined : envEditDisabled);
fileArtifacts.recheckReadonlyStatus();
}

onDestroy(() => {
$editorRoutePrefix = "";
Expand Down Expand Up @@ -161,7 +177,7 @@
bind:starting
/>
{:else if isReady && deployment?.id && instanceId && runtimeHost && jwt}
{#key `${runtimeHost}::${instanceId}`}
{#key `${runtimeHost}::${instanceId}::${hasPrimaryDeployment}`}
<RuntimeProvider host={runtimeHost} {instanceId} {jwt}>
{#if !inProjectWelcomePage}
<ProjectHeader
Expand Down Expand Up @@ -229,19 +245,14 @@

{#snippet envEditDisabled()}
<div class="flex flex-row gap-2 items-center w-fit text-sm">
{#if hasPrimaryDeployment}
<InfoIcon size={14} /> Manage environment variables in
<a
href="/{organization}/{project}/-/settings/environment-variables"
target="_blank"
rel="noopener"
>
Settings →
</a>
{:else}
<InfoIcon size={14} /> You can manage environment variables from settings page
after the project has been published.
{/if}
<InfoIcon size={14} /> Manage environment variables in
<a
href="/{organization}/{project}/-/settings/environment-variables"
target="_blank"
rel="noopener"
>
Settings →
</a>
</div>
{/snippet}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { beforeAll, afterAll, describe, expect, it } from "vitest";
import type { Snippet } from "svelte";
import { setRuntimeEditEnvironment } from "../edit-environment.ts";
import { isPinned, isProtectedDirectory, isManaged } from "./protected-files";
import {
isPinned,
isProtectedDirectory,
isManaged,
setCloudReadonlyNotice,
} from "./protected-files";

// A published project registers a readonly notice; the content is irrelevant to
// these predicates, so a placeholder snippet stands in for it.
const notice = (() => {}) as unknown as Snippet;

describe("isPinned", () => {
it("matches /rill.yaml exactly", () => {
Expand Down Expand Up @@ -35,10 +45,13 @@ describe("isReadonly on local", () => {
describe("isReadonly on cloud", () => {
beforeAll(() => {
setRuntimeEditEnvironment("cloud");
// Simulate a published project, where env editing is managed from settings.
setCloudReadonlyNotice(notice);
});

afterAll(() => {
setRuntimeEditEnvironment("local");
setCloudReadonlyNotice(undefined);
});

it("locks /.env at the project root", () => {
Expand Down Expand Up @@ -70,6 +83,25 @@ describe("isReadonly on cloud", () => {
});
});

describe("isReadonly on cloud without a readonly notice", () => {
beforeAll(() => {
setRuntimeEditEnvironment("cloud");
// No notice registered: the project has no primary deployment (unpublished),
// or the deployment lookup has not yet resolved.
setCloudReadonlyNotice(undefined);
});

afterAll(() => {
setRuntimeEditEnvironment("local");
});

it("does not lock .env files", () => {
expect(isManaged("/.env")).toBe(false);
expect(isManaged("/foo/.env")).toBe(false);
expect(isManaged("/.dev.env")).toBe(false);
});
});

describe("isProtectedDirectory", () => {
it("matches /tmp exactly", () => {
expect(isProtectedDirectory("/tmp")).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function setCloudReadonlyNotice(notice: Snippet | undefined) {

export function isManaged(path: string): boolean {
if (isCloudRuntimeEditEnvironment()) {
return CLOUD_READONLY.some((m) => m(path));
return CLOUD_READONLY.some((m) => m(path)) && !!cloudReadonlyNotice;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fail closed when the primary deployment lookup is inconclusive

When the edit layout's new unscoped GetProject request completes without data (for example, a transient error while the branch-scoped project query still succeeds), it calls setCloudReadonlyNotice(undefined). Since this predicate now treats a missing notice as "not managed", a published project's .env files become editable and the rename/create guards stop blocking env paths instead of preserving the previous read-only behavior until the deployment state is known.

Useful? React with 👍 / 👎.

}
return false;
}
Expand Down
9 changes: 7 additions & 2 deletions web-common/src/features/entity-management/file-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ export class FileArtifact {
readonly autoSave: Writable<boolean>;
// Path is locked: file can't be renamed or deleted, and other files can't
// be renamed onto this path.
readonly pinned: boolean;
pinned: boolean;
// Content is managed outside of editors.
// Currently **/.*.env files are managed from project settings page on cloud editor
readonly managed: boolean;
managed: boolean;
readonly snapshot: Writable<{
scroll?: ReturnType<EditorView["scrollSnapshot"]>;
selection?: EditorSelection;
Expand Down Expand Up @@ -412,6 +412,11 @@ export class FileArtifact {
);
}

recheckReadonlyStatus() {
this.pinned = isPinned(this.path);
this.managed = isManaged(this.path);
}

private updateResourceNameIfChanged(resource: V1Resource) {
const isSubResource = !!resource.component?.spec?.definedInCanvas;
if (isSubResource) return;
Expand Down
4 changes: 4 additions & 0 deletions web-common/src/features/entity-management/file-artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ export class FileArtifacts {
const fileParseErrors = fileArtifact.fetchParserErrors(queryClient);
return fileParseErrors[0]?.message ?? null;
}

recheckReadonlyStatus() {
this.artifacts.forEach((artifact) => artifact.recheckReadonlyStatus());
}
}

export const fileArtifacts = new FileArtifacts();
Loading