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
2 changes: 1 addition & 1 deletion src/routes/(console)/project-[region]-[project]/+layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {

// Track console access for cloud projects (fire-and-forget, backend has 6-day cooldown).
// Skip if paused — user must explicitly resume via the paused project modal.
if (isCloud && browser && project.status !== 'paused') {
if (isCloud && browser && project.status !== 'paused' && scopes.includes('projects.write')) {
generateFingerprintToken()
.then((fingerprint) => {
sdk.forConsole.client.headers['X-Appwrite-Console-Fingerprint'] = fingerprint;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@
import UpdateVariables from '../updateVariables.svelte';
import { page } from '$app/state';
import UpdateLabels from './updateLabels.svelte';
import type { PageData } from './$types';
import { Alert } from '@appwrite.io/pink-svelte';

export let data;

let teamId: string = null;
let { data }: { data: PageData } = $props();

onMount(() => {
teamId ??= $project.teamId;

const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);

Expand Down Expand Up @@ -87,25 +85,31 @@

<Container>
{#if $project}
{#if $canWriteProjects}
<UpdateName />
<UpdateLabels />
<UpdateProtocols />
<UpdateServices />
<UpdateInstallations {...data.installations} limit={data.limit} offset={data.offset} />
<UpdateVariables
{sdkCreateVariable}
{sdkUpdateVariable}
{sdkDeleteVariable}
isGlobal
variableList={data.variables}
backendPagination
variablesOffset={data.variablesOffset}
variablesLimit={data.limit}
project={data.project}
analyticsSource="project_settings" />
<ChangeOrganization />
<DeleteProject />
{#if !$canWriteProjects}
<Alert.Inline status="info" title="Read-only project settings">
You can open this settings area, but editing project-level settings requires the
<code>projects.write</code> scope.
</Alert.Inline>
{/if}

<UpdateName />
<UpdateLabels />
<UpdateProtocols />
<UpdateServices />
<UpdateInstallations {...data.installations} limit={data.limit} offset={data.offset} />
<UpdateVariables
{sdkCreateVariable}
{sdkUpdateVariable}
{sdkDeleteVariable}
disabled={!$canWriteProjects}
isGlobal
variableList={data.variables}
backendPagination
variablesOffset={data.variablesOffset}
variablesLimit={data.limit}
project={data.project}
analyticsSource="project_settings" />
<ChangeOrganization />
<DeleteProject />
{/if}
</Container>
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import type { PageLoad } from './$types';

function isReadonlySettingsPermissionError(error: unknown): boolean {
const code = (error as { code?: number } | null)?.code;
return code === 401 || code === 403;
}

export const load: PageLoad = async ({ depends, url, params }) => {
depends(Dependencies.PROJECT_VARIABLES);
depends(Dependencies.PROJECT_INSTALLATIONS);

const limit = PAGE_LIMIT;
const offset = Number(url.searchParams.get('offset') ?? 0);
const variablesOffset = Number(url.searchParams.get('variablesOffset') ?? 0);
const projectSdk = sdk.forProject(params.region, params.project);
const [variables, installations] = await Promise.all([
const [variablesResult, installationsResult] = await Promise.allSettled([
projectSdk.projectApi.listVariables({
queries: [Query.limit(limit), Query.offset(variablesOffset)]
}),
Expand All @@ -19,6 +25,38 @@ export const load: PageLoad = async ({ depends, url, params }) => {
})
]);

const variables =
variablesResult.status === 'fulfilled'
? variablesResult.value
: (() => {
// Read-only users can be blocked from write-adjacent settings APIs.
// Only silence those permission errors so genuine load failures still surface.
if (!isReadonlySettingsPermissionError(variablesResult.reason)) {
throw variablesResult.reason;
}

return {
total: 0,
variables: []
};
})();

const installations =
installationsResult.status === 'fulfilled'
? installationsResult.value
: (() => {
// Read-only users can be blocked from write-adjacent settings APIs.
// Only silence those permission errors so genuine load failures still surface.
if (!isReadonlySettingsPermissionError(installationsResult.reason)) {
throw installationsResult.reason;
}

return {
total: 0,
installations: []
};
})();

return {
limit,
offset,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { organizationList } from '$lib/stores/organization';
import { project } from '../store';
import TransferProjectModal from './transferProjectModal.svelte';
import { canWriteProjects } from '$lib/stores/roles';

let teamId: string;
let showTransfer = false;
Expand All @@ -18,6 +19,7 @@
id="organization"
placeholder="Select destination"
label="Move to"
disabled={!$canWriteProjects}
bind:value={teamId}
options={$organizationList.teams
.filter((team) => team.$id !== $project.teamId)
Expand All @@ -30,7 +32,7 @@
<svelte:fragment slot="actions">
<Button
secondary
disabled={teamId === $project.teamId || !teamId}
disabled={!$canWriteProjects || teamId === $project.teamId || !teamId}
on:click={() => (showTransfer = true)}>Move</Button>
</svelte:fragment>
</CardGrid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { project, projectRegion } from '../store';
import { organization } from '$lib/stores/organization';
import { Dependencies } from '$lib/constants';
import { canWriteProjects } from '$lib/stores/roles';
let error: string;
let showDelete = false;
let name: string = null;
Expand Down Expand Up @@ -59,7 +60,9 @@
</svelte:fragment>

<svelte:fragment slot="actions">
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
<Button secondary disabled={!$canWriteProjects} on:click={() => (showDelete = true)}>
Delete
</Button>
</svelte:fragment>
</CardGrid>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import type { ComponentType } from 'svelte';
import { Link } from '$lib/elements';
import { regionalConsoleVariables, mcpTools } from '../store';
import { canWriteProjects } from '$lib/stores/roles';

export let total: number;
export let limit: number;
Expand Down Expand Up @@ -87,6 +88,7 @@
<div class="installations-action-row">
<FormButton
secondary
disabled={!$canWriteProjects}
href={configureGitHub()}
on:click={() => {
trackEvent(Click.SettingsInstallProviderClick);
Expand Down Expand Up @@ -131,6 +133,7 @@
type="button"
class="button is-text is-only-icon"
aria-label="more options"
disabled={!$canWriteProjects}
on:click={toggle}>
<span class="icon-dots-horizontal" aria-hidden="true"
></span>
Expand Down Expand Up @@ -186,7 +189,11 @@
title="No installation was added to the project yet"
description="Add an installation to connect repositories">
<svelte:fragment slot="actions">
<FormButton secondary href={configureGitHub()} external>
<FormButton
secondary
disabled={!$canWriteProjects}
href={configureGitHub()}
external>
<Icon icon={IconGithub} size="s" slot="start" />
Connect to GitHub
</FormButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { project } from '../store';
import { Tag, Input, Layout, Icon } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { canWriteProjects } from '$lib/stores/roles';

const alphaNumericRegExp = /^[a-zA-Z0-9]+$/;
let suggestedLabels = ['live', 'stage', 'internal'];
Expand Down Expand Up @@ -73,6 +74,7 @@
id="project-labels"
label="Labels"
placeholder="Select or type project labels"
disabled={!$canWriteProjects}
bind:tags={labels} />
{/key}
<Layout.Stack direction="row">
Expand All @@ -81,6 +83,8 @@
size="s"
selected={labels.includes(suggestedLabel)}
on:click={() => {
if (!$canWriteProjects) return;

if (!labels.includes(suggestedLabel)) {
labels = [...labels, suggestedLabel];
} else {
Expand All @@ -98,7 +102,7 @@
</svelte:fragment>

<svelte:fragment slot="actions">
<Button disabled={isDisabled} submit>Update</Button>
<Button disabled={!$canWriteProjects || isDisabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,21 @@
</Button>
</svelte:fragment>
</CardGrid>
{#if $canWriteProjects}
<Form onSubmit={updateName}>
<CardGrid>
<svelte:fragment slot="title">Name</svelte:fragment>
<svelte:fragment slot="aside">
<InputText
id="name"
label="Name"
bind:value={name}
required
placeholder="Enter name" />
</svelte:fragment>
<Form onSubmit={updateName}>
<CardGrid>
<svelte:fragment slot="title">Name</svelte:fragment>
<svelte:fragment slot="aside">
<InputText
id="name"
label="Name"
bind:value={name}
required
disabled={!$canWriteProjects}
placeholder="Enter name" />
</svelte:fragment>

<svelte:fragment slot="actions">
<Button disabled={name === $project.name} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
{/if}
<svelte:fragment slot="actions">
<Button disabled={!$canWriteProjects || name === $project.name} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import { SvelteSet } from 'svelte/reactivity';
import { ProtocolId } from '@appwrite.io/console';
import { get } from 'svelte/store';
import { canWriteProjects } from '$lib/stores/roles';

let isUpdatingAllProtocols = $state(false);
let showUpdateProtocolDialog = $state(false);
Expand Down Expand Up @@ -139,20 +140,26 @@
<Button
extraCompact
on:click={() => {
if (!$canWriteProjects) return;
showUpdateProtocolDialog = true;
updateProtocolsEnabledMode = true;
}}
disabled={shouldDisableEnableAllButton}>Enable all</Button>
disabled={!$canWriteProjects || shouldDisableEnableAllButton}>
Enable all
</Button>
<span style:height="20px">
<Divider vertical />
</span>
<Button
extraCompact
on:click={() => {
if (!$canWriteProjects) return;
showUpdateProtocolDialog = true;
updateProtocolsEnabledMode = false;
}}
disabled={shouldDisableDisableAllButton}>Disable all</Button>
disabled={!$canWriteProjects || shouldDisableDisableAllButton}>
Disable all
</Button>
</Layout.Stack>
</div>
<Divider />
Expand All @@ -167,7 +174,8 @@
description={protocolDescriptions[protocol.method]}
bind:value={protocol.value}
on:change={() => protocolUpdate(protocol)}
disabled={apiProtocolUpdates.has(protocol.method)} />
disabled={!$canWriteProjects ||
apiProtocolUpdates.has(protocol.method)} />

{#if apiProtocolUpdates.has(protocol.method)}
<span class="protocol-spinner">
Expand Down Expand Up @@ -196,7 +204,7 @@
<Button
secondary
submissionLoader
disabled={isUpdatingAllProtocols}
disabled={!$canWriteProjects || isUpdatingAllProtocols}
forceShowLoader={isUpdatingAllProtocols}
on:click={() => toggleAllProtocols(updateProtocolsEnabledMode)}>
{dialogDetails.actionButton}
Expand Down
Loading
Loading