Merge branch 'img-poc' into mgn-dv

# Conflicts:
#	src/routes/(console)/+layout.svelte
#	src/routes/(console)/organization-[organization]/+page.svelte
This commit is contained in:
ernstmul
2025-04-02 11:36:26 +02:00
28 changed files with 630 additions and 125 deletions
+3 -1
View File
@@ -24,12 +24,14 @@ ARG PUBLIC_APPWRITE_ENDPOINT
ARG PUBLIC_GROWTH_ENDPOINT
ARG PUBLIC_STRIPE_KEY
ARG SENTRY_AUTH_TOKEN
ARG PUBLIC_PROJECT_PROFILE
ENV PUBLIC_APPWRITE_ENDPOINT=$PUBLIC_APPWRITE_ENDPOINT
ENV PUBLIC_GROWTH_ENDPOINT=$PUBLIC_GROWTH_ENDPOINT
ENV PUBLIC_CONSOLE_MODE=$PUBLIC_CONSOLE_MODE
ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
ENV PUBLIC_PROJECT_PROFILE=$PUBLIC_PROJECT_PROFILE
ENV NODE_OPTIONS=--max_old_space_size=8192
RUN pnpm run build
@@ -38,5 +40,5 @@ FROM nginx:1.25-alpine
EXPOSE 80
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY docker/nginx.$PUBLIC_PROJECT_PROFILE.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/build /usr/share/nginx/html/console
+39
View File
@@ -0,0 +1,39 @@
map $sent_http_content_type $expires {
# cache everything for 1 year
default 1y;
# html files shouldn't be cached for single-page applications
text/html off;
}
server {
listen 80;
server_name localhost;
# serve compressed file if filename.gz exists
gzip_static on;
location /studio {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri /studio/index.html;
# Add cache headers
expires $expires;
add_header Pragma public;
add_header Cache-Control "public";
# Deny IE browsers from going into quirks mode
add_header X-UA-Compatible "IE=Edge";
# X-Frame-Options is to prevent from clickJacking attack
add_header X-Frame-Options SAMEORIGIN;
# This header enables the Cross-site scripting (XSS) filter
add_header X-XSS-Protection "1; mode=block;";
# disable content-type sniffing on some browsers.
add_header X-Content-Type-Options nosniff;
}
location / {
absolute_redirect off;
return 301 /studio;
}
}
+2
View File
@@ -34,6 +34,7 @@
"cron-parser": "^4.9.0",
"dayjs": "^1.11.13",
"deep-equal": "^2.2.3",
"dotenv": "^16.4.7",
"echarts": "^5.6.0",
"envfile": "^7.1.0",
"ignore": "^6.0.2",
@@ -48,6 +49,7 @@
},
"devDependencies": {
"@eslint/compat": "^1.2.7",
"@eslint/js": "^9.23.0",
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.86.5",
"@playwright/test": "^1.51.1",
+6
View File
@@ -47,6 +47,9 @@ importers:
deep-equal:
specifier: ^2.2.3
version: 2.2.3
dotenv:
specifier: ^16.4.7
version: 16.4.7
echarts:
specifier: ^5.6.0
version: 5.6.0
@@ -84,6 +87,9 @@ importers:
'@eslint/compat':
specifier: ^1.2.7
version: 1.2.7(eslint@9.23.0)
'@eslint/js':
specifier: ^9.23.0
version: 9.23.0
'@melt-ui/pp':
specifier: ^0.3.2
version: 0.3.2(@melt-ui/svelte@0.86.5(svelte@5.25.3))(svelte@5.25.3)
+3 -3
View File
@@ -5,9 +5,9 @@
<meta
name="description"
content="Appwrite is an open-source platform for building applications at any scale, using your preferred programming languages and tools." />
<link rel="icon" type="image/svg+xml" href="/console/logos/appwrite-icon.svg" />
<link rel="mask-icon" type="image/png" href="/console/logos/appwrite-icon.png" />
<link rel="stylesheet" href="/console/css/loading.css" />
<link rel="icon" type="image/svg+xml" href="/{project_profile}/logos/appwrite-icon.svg" />
<link rel="mask-icon" type="image/png" href="/{project_profile}/logos/appwrite-icon.png" />
<link rel="stylesheet" href="/{project_profile}/css/loading.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
+22 -1
View File
@@ -2,6 +2,7 @@ import { sequence } from '@sveltejs/kit/hooks';
import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit';
import * as Sentry from '@sentry/sveltekit';
import { isCloud, isProd } from '$lib/system';
import type { Handle } from '@sveltejs/kit';
Sentry.init({
enabled: isCloud && isProd,
@@ -9,6 +10,26 @@ Sentry.init({
tracesSampleRate: 1.0
});
export const handle = sequence(sentryHandle());
const projectProfile = process.env.PUBLIC_PROJECT_PROFILE || 'console';
const dynamicHtmlTransform: Handle = async ({ event, resolve }) => {
let response = await resolve(event);
if (response.headers.get('content-type')?.includes('text/html')) {
let html = await response.text();
// Replace '/console/' dynamically with the project profile
html = html.replace(/\/{project_profile}\//g, `/${projectProfile}/`);
response = new Response(html, {
status: response.status,
headers: response.headers
});
}
return response;
};
export const handle = sequence(sentryHandle(), dynamicHtmlTransform);
export const handleError = handleErrorWithSentry();
+16 -14
View File
@@ -9,7 +9,7 @@
} from '@appwrite.io/pink-icons-svelte';
import { BottomSheet } from '$lib/components';
import { isSmallViewport } from '$lib/stores/viewport';
import { isCloud } from '$lib/system';
import { isCloud, isStudio } from '$lib/system';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { newOrgModal } from '$lib/stores/organization';
@@ -80,7 +80,7 @@
title: 'Switch Organization',
items: organizations.map((organization) => ({
name: organization.name,
href: `/console/organization-${organization?.$id}`
href: `${base}/organization-${organization?.$id}`
}))
},
bottom: {
@@ -101,7 +101,7 @@
items: [
{
name: 'Organization overview',
href: `/console/organization-${selectedOrg?.$id}`
href: `${base}/organization-${selectedOrg?.$id}`
}
]
},
@@ -139,12 +139,12 @@
if (index < 4) {
return {
name: project.name,
href: `/console/project-${project.$id}/overview`
href: `${base}/project-${project.$id}/overview`
};
} else if (index === 4) {
return {
name: 'All projects',
href: `/console/organization-${selectedOrg?.$id}`
href: `${base}/organization-${selectedOrg?.$id}`
};
}
return null;
@@ -156,7 +156,7 @@
{
name: 'Create project',
trailingIcon: IconPlus,
href: `/console/organization-${selectedOrg?.$id}?create-project`
href: `${base}/organization-${selectedOrg?.$id}?create-project`
}
]
},
@@ -167,7 +167,7 @@
{
name: 'Create project',
trailingIcon: IconPlus,
href: `/console/organization-${selectedOrg?.$id}?create-project`
href: `${base}/organization-${selectedOrg?.$id}?create-project`
}
]
}
@@ -185,7 +185,9 @@
<svelte:window on:resize={onResize} />
<div use:melt={$menubar}>
{#if !$isSmallViewport}
<span class="breadcrumb-separator">/</span>
{#if !isStudio}
<span class="breadcrumb-separator">/</span>
{/if}
<button
type="button"
class="trigger"
@@ -219,7 +221,7 @@
{#if selectedOrg}
<div use:melt={$itemOrganizations}>
<ActionMenu.Root>
<ActionMenu.Item.Anchor href={`/console/organization-${selectedOrg?.$id}`}
<ActionMenu.Item.Anchor href={`${base}/organization-${selectedOrg?.$id}`}
>Organization overview</ActionMenu.Item.Anchor
></ActionMenu.Root>
</div>
@@ -239,7 +241,7 @@
<div use:melt={$itemOrganizations}>
<ActionMenu.Root>
<ActionMenu.Item.Anchor
href={`/console/organization-${organization?.$id}`}
href={`${base}/organization-${organization?.$id}`}
>{organization.name}</ActionMenu.Item.Anchor>
</ActionMenu.Root>
</div>
@@ -270,7 +272,7 @@
<div use:melt={$itemOrganizations}>
<ActionMenu.Root>
<ActionMenu.Item.Anchor
href={`/console/organization-${organization?.$id}`}
href={`${base}/organization-${organization?.$id}`}
>{organization.name}</ActionMenu.Item.Anchor
></ActionMenu.Root>
</div>
@@ -315,7 +317,7 @@
{#if index < 4}
<div use:melt={$itemProjects}>
<ActionMenu.Root>
<ActionMenu.Item.Anchor href={`/console/project-${project.$id}`}
<ActionMenu.Item.Anchor href={`${base}/project-${project.$id}`}
>{project.name}</ActionMenu.Item.Anchor
></ActionMenu.Root>
</div>
@@ -323,7 +325,7 @@
<div use:melt={$itemProjects}>
<ActionMenu.Root>
<ActionMenu.Item.Anchor
href={`/console/organization-${selectedOrg.$id}`}
href={`${base}/organization-${selectedOrg.$id}`}
>All projects</ActionMenu.Item.Anchor
></ActionMenu.Root>
</div>
@@ -335,7 +337,7 @@
<ActionMenu.Root>
<ActionMenu.Item.Anchor
leadingIcon={IconPlusSm}
href={`/console/organization-${selectedOrg?.$id}?create-project`}
href={`${base}/organization-${selectedOrg?.$id}?create-project`}
>Create project</ActionMenu.Item.Anchor
></ActionMenu.Root>
</div>
@@ -0,0 +1,37 @@
<script lang="ts">
import { ActionMenu, Layout, Typography } from '@appwrite.io/pink-svelte';
import { base } from '$app/paths';
import type { Organization } from '$lib/stores/organization';
export let organization: Organization;
</script>
<nav>
<Layout.Stack>
<Typography.Text color="--fgcolor-neutral-tertiary">{organization.name}</Typography.Text>
<Layout.Stack gap="xs">
<ActionMenu.Item.Anchor href={`${base}/organization-${organization.$id}`}
>Projects</ActionMenu.Item.Anchor>
<ActionMenu.Item.Anchor href={`${base}/organization-${organization.$id}/members`}
>Members</ActionMenu.Item.Anchor>
<ActionMenu.Item.Anchor href={`${base}/organization-${organization.$id}/usage`}
>Usage</ActionMenu.Item.Anchor>
<ActionMenu.Item.Anchor href={`${base}/organization-${organization.$id}/billing`}
>Billing</ActionMenu.Item.Anchor>
<ActionMenu.Item.Anchor href={`${base}/organization-${organization.$id}/settings`}
>Settings</ActionMenu.Item.Anchor>
</Layout.Stack>
</Layout.Stack>
</nav>
<style>
nav {
position: fixed;
left: 0;
top: 48px;
width: 200px;
height: calc(100vh - 48px);
padding: var(--space-4) var(--space-8) var(--space-8) var(--space-8);
}
</style>
@@ -0,0 +1,35 @@
<script lang="ts">
import { Layout, ActionMenu, Icon, Divider } from '@appwrite.io/pink-svelte';
import {
IconAnnotation,
IconCog,
IconDatabase,
IconFolder,
IconTemplate,
IconUserGroup
} from '@appwrite.io/pink-icons-svelte';
</script>
<nav style:--icon-fill="var(--fgcolor-neutral-tertiary)">
<Layout.Stack gap="xs">
<ActionMenu.Item.Anchor href="" leadingIcon={IconTemplate}></ActionMenu.Item.Anchor>
<Divider />
<ActionMenu.Item.Anchor href="" leadingIcon={IconUserGroup}></ActionMenu.Item.Anchor>
<ActionMenu.Item.Anchor href="" leadingIcon={IconDatabase}></ActionMenu.Item.Anchor>
<ActionMenu.Item.Anchor href="" leadingIcon={IconAnnotation}></ActionMenu.Item.Anchor>
<ActionMenu.Item.Anchor href="" leadingIcon={IconFolder}></ActionMenu.Item.Anchor>
<Divider />
<ActionMenu.Item.Anchor href="" leadingIcon={IconCog}></ActionMenu.Item.Anchor>
</Layout.Stack>
</nav>
<style>
nav {
position: fixed;
left: 0;
top: 48px;
height: calc(100vh - 48px);
padding: var(--space-4);
width: 52px;
}
</style>
+11 -6
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { Layout } from '@appwrite.io/pink-svelte';
import { isStudio } from '$lib/system';
export let overlapCover = false;
export let size: 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' = null;
@@ -8,13 +9,17 @@
: '';
</script>
<div style:container-type="inline-size" class:overlap-cover={overlapCover}>
<div class="console-container" {style}>
<Layout.Stack gap="l">
<slot />
</Layout.Stack>
{#if isStudio}
<slot />
{:else}
<div style:container-type="inline-size" class:overlap-cover={overlapCover}>
<div class="console-container" {style}>
<Layout.Stack gap="l">
<slot />
</Layout.Stack>
</div>
</div>
</div>
{/if}
<style>
.overlap-cover {
+75
View File
@@ -0,0 +1,75 @@
<script lang="ts">
import { AvatarInitials, Breadcrumbs } from '$lib/components/index.js';
import { Avatar, Layout } from '@appwrite.io/pink-svelte';
import { page } from '$app/stores';
import { organization, organizationList } from '$lib/stores/organization';
import { user } from '$lib/stores/user';
import SidebarProject from '$lib/components/studio/sidebarProject.svelte';
import SidebarOrganization from '$lib/components/studio/sidebarOrganization.svelte';
import { BillingPlan } from '$lib/constants.js';
import { isCloud } from '$lib/system.js';
import { tierToPlan } from '$lib/stores/billing.js';
import type { NavbarProject } from '$lib/components/navbar.svelte';
$: hasProjectSidebar = $page.url.pathname.startsWith('/studio/proj');
export let loadedProjects: Array<NavbarProject> = [];
$: organizations = $organizationList.teams.map((org) => {
const billingPlan = org['billingPlan'];
return {
name: org.name,
$id: org.$id,
showUpgrade: billingPlan === BillingPlan.FREE,
tierName: isCloud ? (tierToPlan(billingPlan)?.name ?? '') : null,
isSelected: $page.data.organization?.$id === org.$id,
projects: loadedProjects
};
});
$: console.log(organizations);
</script>
<main>
<Layout.Stack>
<header>
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Breadcrumbs {organizations} />
<AvatarInitials name={$user?.name ?? ''} size="s" />
</Layout.Stack>
</header>
<div class="studio-content" class:project-sidebar={hasProjectSidebar}><slot /></div>
{#if hasProjectSidebar}
<SidebarProject />
{:else}
<SidebarOrganization organization={$page.data.organization} />
{/if}
</Layout.Stack>
</main>
<style lang="scss">
main {
min-height: 100vh;
background-color: var(--bgcolor-neutral-default);
}
header {
position: fixed;
top: 0;
width: 100%;
padding: var(--space-4);
}
.studio-content {
min-height: calc(100vh - 48px);
margin-top: 48px;
width: calc(100vw - 200px);
margin-left: 200px;
padding-right: var(--space-4);
&.project-sidebar {
width: calc(100vw - 52px);
margin-left: 52px;
}
}
</style>
+9 -1
View File
@@ -6,11 +6,17 @@ export const enum Mode {
SELF_HOSTED = 'self-hosted'
}
export const enum Profile {
CONSOLE = 'console',
STUDIO = 'studio'
}
export const VARS = {
CONSOLE_MODE: (env.PUBLIC_CONSOLE_MODE as Mode) ?? undefined,
APPWRITE_ENDPOINT: env.PUBLIC_APPWRITE_ENDPOINT ?? undefined,
GROWTH_ENDPOINT: env.PUBLIC_GROWTH_ENDPOINT ?? undefined,
PUBLIC_STRIPE_KEY: env.PUBLIC_STRIPE_KEY ?? undefined
PUBLIC_STRIPE_KEY: env.PUBLIC_STRIPE_KEY ?? undefined,
PROJECT_PROFILE: (env.PUBLIC_PROJECT_PROFILE as Profile) ?? undefined
};
export const ENV = {
@@ -21,7 +27,9 @@ export const ENV = {
};
export const MODE = VARS.CONSOLE_MODE === Mode.CLOUD ? Mode.CLOUD : Mode.SELF_HOSTED;
export const PROFILE = VARS.PROJECT_PROFILE === Profile.CONSOLE ? Profile.CONSOLE : Profile.STUDIO;
export const isCloud = MODE === Mode.CLOUD;
export const isStudio = PROFILE === Profile.STUDIO;
export const isSelfHosted = MODE !== Mode.CLOUD;
export const isDev = ENV.DEV;
export const isProd = ENV.PROD;
+6 -1
View File
@@ -3,6 +3,7 @@
import { BillingPlan, INTERVAL } from '$lib/constants';
import Footer from '$lib/layout/footer.svelte';
import Shell from '$lib/layout/shell.svelte';
import ShellStudio from '$lib/layout/shellStudio.svelte';
import { app } from '$lib/stores/app';
import { newOrgModal, organization, type Organization } from '$lib/stores/organization';
import { database, checkForDatabaseBackupPolicies } from '$lib/stores/database';
@@ -32,7 +33,7 @@
import { openMigrationWizard } from './(migration-wizard)';
import { project } from './project-[project]/store';
import { feedback } from '$lib/stores/feedback';
import { hasStripePublicKey, isCloud, VARS } from '$lib/system';
import { hasStripePublicKey, isCloud, isStudio, VARS } from '$lib/system';
import { stripe } from '$lib/stores/stripe';
import MobileSupportModal from './wizard/support/mobileSupportModal.svelte';
import { showSupportModal } from './wizard/support/store';
@@ -329,6 +330,9 @@
</script>
<CommandCenter />
{#if isStudio}
<ShellStudio bind:loadedProjects><slot /></ShellStudio>
{:else}
<Shell
showSideNavigation={page.url.pathname !== '/console' &&
!page?.params.organization &&
@@ -343,6 +347,7 @@
<slot />
<Footer slot="footer" />
</Shell>
{/if}
{#if $wizard.show && $wizard.component}
<svelte:component this={$wizard.component} {...$wizard.props} />
@@ -4,7 +4,7 @@
import { Container } from '$lib/layout';
import CreateProject from './createProject.svelte';
import CreateOrganization from '../createOrganization.svelte';
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import { GRACE_PERIOD_OVERRIDE, isCloud, isStudio } from '$lib/system';
import { page } from '$app/state';
import { registerCommands } from '$lib/commandCenter';
import {
@@ -28,7 +28,7 @@
import { onMount, type ComponentType } from 'svelte';
import { canWriteProjects } from '$lib/stores/roles';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
import { Badge, Icon, Typography } from '@appwrite.io/pink-svelte';
import { Badge, Divider, Icon, Typography, Card, Layout } from '@appwrite.io/pink-svelte';
import {
IconAndroid,
IconApple,
@@ -48,6 +48,7 @@
let showCreateProjectCloud = false;
function allServiceDisabled(project: Models.Project): boolean {
return false;
let disabled = true;
services.load(project);
$services.list.forEach((service) => {
@@ -142,94 +143,124 @@
}
</script>
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Typography.Title>Projects</Typography.Title>
{#if isStudio}
<Card.Base>
<Layout.Stack>
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography.Text variant="m-500" color="--fgcolor-neutral-secondary"
>Projects</Typography.Text>
<Button size="s">Create project</Button>
</Layout.Stack>
<DropList bind:show={showDropdown} placement="bottom-end">
{#if $canWriteProjects}
<Button
on:click={handleCreateProject}
event="create_project"
disabled={$readOnly && !GRACE_PERIOD_OVERRIDE}>
<Icon icon={IconPlus} slot="start" size="s" />
Create project
</Button>
{/if}
<svelte:fragment slot="list">
<DropListItem on:click={() => (showCreate = true)}>Empty project</DropListItem>
<DropListItem on:click={importProject}>
<div class="u-flex u-gap-8 u-cross-center">
Import project <span class="tag eyebrow-heading-3">Experimental</span>
</div>
</DropListItem>
</svelte:fragment>
</DropList>
</div>
<Divider />
{#if data.projects.total}
<CardContainer
showEmpty={$canWriteProjects}
total={data.projects.total}
<Layout.Grid gap="l" columnsS={3} columns={4} columnsL={4} columnsXL={5}>
<!-- TODO: this now maps over projects, but probably will be more like artifacts (and location might be different-->
{#each data.projects.projects as project}
<a href={`${base}/project-${project.$id}`}
><Card.Media
src="https://picsum.photos/260/150"
alt=""
description={project.description}
title={project.name}></Card.Media
></a>
{/each}
</Layout.Grid>
</Layout.Stack>
</Card.Base>
{:else}
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Typography.Title>Projects</Typography.Title>
<DropList bind:show={showDropdown} placement="bottom-end">
{#if $canWriteProjects}
<Button
on:click={handleCreateProject}
event="create_project"
disabled={$readOnly && !GRACE_PERIOD_OVERRIDE}>
<Icon icon={IconPlus} slot="start" size="s" />
Create project
</Button>
{/if}
<svelte:fragment slot="list">
<DropListItem on:click={() => (showCreate = true)}>Empty project</DropListItem>
<DropListItem on:click={importProject}>
<div class="u-flex u-gap-8 u-cross-center">
Import project <span class="tag eyebrow-heading-3">Experimental</span>
</div>
</DropListItem>
</svelte:fragment>
</DropList>
</div>
{#if data.projects.total}
<CardContainer
showEmpty={$canWriteProjects}
total={data.projects.total}
offset={data.offset}
on:click={handleCreateProject}>
{#each data.projects.projects as project}
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
)}
<GridItem1 href={`${base}/project-${project.$id}`}>
<svelte:fragment slot="eyebrow">
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
</svelte:fragment>
<svelte:fragment slot="title">
{project.name}
</svelte:fragment>
{#if allServiceDisabled(project)}
<p>
<span class="icon-pause" aria-hidden="true"></span> All services are
disabled.
</p>
{/if}
{#each platforms as platform, i}
{#if i < 3}
{@const icon = getIconForPlatform(platform.icon)}
<Badge variant="secondary" content={platform.name}>
<Icon {icon} size="s" slot="start" />
</Badge>
{/if}
{/each}
{#if platforms?.length > 3}
<Badge
variant="secondary"
content={`+${project.platforms.length - 3}`} />
{/if}
<svelte:fragment slot="icons">
{#if isCloud && regions}
{@const region = findRegion(project)}
<span class="u-color-text-gray u-medium u-line-height-2">
{region?.name}
</span>
{/if}
</svelte:fragment>
</GridItem1>
{/each}
<svelte:fragment slot="empty">
<p>Create a new project</p>
</svelte:fragment>
</CardContainer>
{:else}
<Empty
single
allowCreate={$canWriteProjects}
on:click={handleCreateProject}
target="project"
href="https://appwrite.io/docs/quick-starts"></Empty>
{/if}
<PaginationWithLimit
name="Projects"
limit={data.limit}
offset={data.offset}
on:click={handleCreateProject}>
{#each data.projects.projects as project}
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
)}
<GridItem1 href={`${base}/project-${project.$id}`}>
<svelte:fragment slot="eyebrow">
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
</svelte:fragment>
<svelte:fragment slot="title">
{project.name}
</svelte:fragment>
{#if allServiceDisabled(project)}
<p>
<span class="icon-pause" aria-hidden="true"></span> All services are disabled.
</p>
{/if}
{#each platforms as platform, i}
{#if i < 3}
{@const icon = getIconForPlatform(platform.icon)}
<Badge variant="secondary" content={platform.name}>
<Icon {icon} size="s" slot="start" />
</Badge>
{/if}
{/each}
{#if platforms?.length > 3}
<Badge variant="secondary" content={`+${project.platforms.length - 3}`} />
{/if}
<svelte:fragment slot="icons">
{#if isCloud && regions}
{@const region = findRegion(project)}
<span class="u-color-text-gray u-medium u-line-height-2">
{region?.name}
</span>
{/if}
</svelte:fragment>
</GridItem1>
{/each}
<svelte:fragment slot="empty">
<p>Create a new project</p>
</svelte:fragment>
</CardContainer>
{:else}
<Empty
single
allowCreate={$canWriteProjects}
on:click={handleCreateProject}
target="project"
href="https://appwrite.io/docs/quick-starts"></Empty>
{/if}
<PaginationWithLimit
name="Projects"
limit={data.limit}
offset={data.offset}
total={data.projects.total} />
</Container>
total={data.projects.total} />
</Container>
{/if}
<CreateOrganization bind:show={addOrganization} />
<CreateProject bind:show={showCreate} teamId={page.params.organization} />
@@ -4,6 +4,7 @@ import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
import { CARD_LIMIT, Dependencies } from '$lib/constants';
import type { PageLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { base } from '$app/paths';
export const load: PageLoad = async ({ params, url, route, depends, parent }) => {
const { scopes } = await parent();
@@ -12,7 +13,7 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) =>
const limit = getLimit(url, route, CARD_LIMIT);
const offset = pageToOffset(page, limit);
if (!scopes.includes('projects.read') && scopes.includes('billing.read')) {
return redirect(301, `/console/organization-${params.organization}/billing`);
return redirect(301, `${base}/organization-${params.organization}/billing`);
}
return {
@@ -5,12 +5,13 @@ import { sdk } from '$lib/stores/sdk';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import { Query } from '@appwrite.io/console';
import { base } from '$app/paths';
export const load: PageLoad = async ({ parent, depends }) => {
const { organization, scopes } = await parent();
if (!scopes.includes('billing.read')) {
return redirect(301, `/console/organization-${organization.$id}`);
return redirect(301, `${base}/organization-${organization.$id}`);
}
depends(Dependencies.PAYMENT_METHODS);
depends(Dependencies.ORGANIZATION);
@@ -8,6 +8,7 @@
import { addNotification } from '$lib/stores/notifications';
import type { OrganizationList } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { base } from '$app/paths';
export let domain: Domain;
export let organizations: OrganizationList;
@@ -20,7 +21,7 @@
type: 'success',
message: 'Domain moved successfully'
});
await goto(`/console/organization-${selectedOrg}/domains/`);
await goto(`${base}/organization-${selectedOrg}/domains/`);
await invalidate(Dependencies.ORGANIZATION);
await invalidate(Dependencies.DOMAINS);
} catch (e) {
+80
View File
@@ -0,0 +1,80 @@
<script lang="ts">
import { Avatar, Layout } from '@appwrite.io/pink-svelte';
import { AvatarInitials, Breadcrumbs } from '$lib/components';
import { page } from '$app/stores';
import { BillingPlan } from '$lib/constants';
import { isCloud } from '$lib/system';
import { tierToPlan } from '$lib/stores/billing';
import { organization, organizationList } from '$lib/stores/organization';
import { user } from '$lib/stores/user';
import SidebarOrganization from '$lib/components/studio/sidebarOrganization.svelte';
import SidebarProject from '$lib/components/studio/sidebarProject.svelte';
$: hasProjectSidebar = $page.url.pathname.startsWith('/studio/proj');
$: loadedProjects = $page.data.projects.map((project) => {
return {
name: project?.name,
$id: project.$id,
isSelected: $page.data.currentProjectId === project.$id,
platformCount: project.platforms.length,
pingCount: project.pingCount
};
});
$: organizations = $organizationList.teams.map((org) => {
const billingPlan = org['billingPlan'];
return {
name: org.name,
$id: org.$id,
showUpgrade: billingPlan === BillingPlan.FREE,
tierName: isCloud ? (tierToPlan(billingPlan)?.name ?? '') : null,
isSelected: $page.data.currentOrganization?.$id === org.$id,
projects: loadedProjects
};
});
</script>
<main>
<Layout.Stack>
<header>
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Breadcrumbs {organizations} />
<AvatarInitials name={$user?.name ?? ''} size="s" />
</Layout.Stack>
</header>
<div class="studio-content" class:project-sidebar={hasProjectSidebar}><slot /></div>
{#if hasProjectSidebar}
<SidebarProject />
{:else}
<SidebarOrganization organization={$page.data.currentOrganization} />
{/if}
</Layout.Stack>
</main>
<style lang="scss">
main {
min-height: 100vh;
background-color: var(--bgcolor-neutral-default);
}
header {
position: fixed;
top: 0;
width: 100%;
padding: var(--space-4);
}
.studio-content {
min-height: calc(100vh - 48px);
margin-top: 48px;
width: calc(100vw - 200px);
margin-left: 200px;
padding-right: var(--space-4);
&.project-sidebar {
width: calc(100vw - 52px);
margin-left: 52px;
}
}
</style>
+70
View File
@@ -0,0 +1,70 @@
import { Dependencies } from '$lib/constants';
import type { Plan } from '$lib/sdk/billing';
import type { Tier } from '$lib/stores/billing';
import { sdk } from '$lib/stores/sdk';
import { isCloud } from '$lib/system';
import type { LayoutLoad } from './$types';
import { Query } from '@appwrite.io/console';
export const load: LayoutLoad = async ({ params, fetch, depends, parent }) => {
await parent();
depends(Dependencies.RUNTIMES);
depends(Dependencies.CONSOLE_VARIABLES);
depends(Dependencies.ORGANIZATION);
const prefs = await sdk.forConsole.account.getPrefs();
const { endpoint, project } = sdk.forConsole.client.config;
const versionPromise = fetch(`${endpoint}/health/version`, {
headers: {
'X-Appwrite-Project': project
}
}).then((response) => response.json() as { version?: string });
const variablesPromise = sdk.forConsole.console.variables();
const [data, variables] = await Promise.all([versionPromise, variablesPromise]);
let plansInfo = new Map<Tier, Plan>();
if (isCloud) {
const plansArray = await sdk.forConsole.billing.getPlansInfo();
plansInfo = plansArray.plans.reduce((map, plan) => {
map.set(plan.$id as Tier, plan);
return map;
}, new Map<Tier, Plan>());
}
const organizations = !isCloud
? await sdk.forConsole.teams.list()
: await sdk.forConsole.billing.listOrganization();
let projects = [];
let currentOrgId = params.organization ? params.organization : prefs.organization;
if (!currentOrgId && organizations.teams.length > 0) {
currentOrgId = organizations.teams[0].$id;
}
if (currentOrgId) {
const orgProjects = await sdk.forConsole.projects.list([
Query.equal('teamId', currentOrgId),
Query.limit(100),
Query.orderDesc('$updatedAt')
]);
projects = orgProjects.projects.length > 0 ? orgProjects.projects : [];
}
const selectedOrganizationId =
params && params.organization ? params.organization : prefs.organization;
return {
consoleVariables: variables,
version: data?.version ?? null,
plansInfo,
roles: [],
scopes: [],
projects: projects,
currentProjectId: params.project ?? '',
organizations: organizations,
currentOrganization: organizations.teams.find((org) => org.$id === selectedOrganizationId)
};
};
@@ -0,0 +1,30 @@
<script lang="ts">
import { Card, Typography, Layout, Divider, Button } from '@appwrite.io/pink-svelte';
import { base } from '$app/paths';
export let data;
</script>
<Card.Base>
<Layout.Stack>
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography.Text variant="m-500" color="--fgcolor-neutral-secondary"
>Projects</Typography.Text>
<Button.Button size="s">Create project</Button.Button>
</Layout.Stack>
<Divider />
<Layout.Grid gap="l" columnsS={3} columns={4} columnsL={4} columnsXL={5}>
{#each data.projects as project}
<a href={`${base}/proj-${project.$id}`}
><Card.Media
src="https://picsum.photos/260/150"
alt=""
description={project.description}
title={project.name}></Card.Media
></a>
{/each}
</Layout.Grid>
</Layout.Stack>
</Card.Base>
@@ -0,0 +1,30 @@
<script lang="ts">
import { Layout, Card, Typography, Divider } from '@appwrite.io/pink-svelte';
</script>
<Layout.Stack direction="row">
<section class="chat">
<Card.Base>
<Layout.Stack height="calc(100vh - 120px)">
<Typography.Text>Chat</Typography.Text>
<Divider />
<Typography.Text>
Phasellus a ultricies nulla. Proin tristique aliquam dui. Phasellus porta
sodales justo, et feugiat ligula pulvinar sed. Morbi et nulla hendrerit,
pellentesque est sodales, euismod magna. Suspendisse vel massa arcu. Nulla
ultrices sit amet metus ac euismod. Curabitur ultrices aliquet libero, a
pharetra orci tristique in. Etiam egestas fermentum justo ut semper. Sed
vulputate eros erat, ac sollicitudin nulla condimentum volutpat. Sed vulputate
est ante.
</Typography.Text>
</Layout.Stack>
</Card.Base>
</section>
<Card.Base>Iframe</Card.Base>
</Layout.Stack>
<style>
.chat {
width: 600px;
}
</style>
+11 -4
View File
@@ -14,16 +14,23 @@
import { user } from '$lib/stores/user';
import { loading } from '$routes/store';
import { Root } from '@appwrite.io/pink-svelte';
import { ThemeDark, ThemeLight, ThemeDarkCloud, ThemeLightCloud } from '../themes';
import {
ThemeDark,
ThemeLight,
ThemeDarkCloud,
ThemeLightCloud,
ThemeLightStudio
} from '../themes';
import { isSmallViewport, updateViewport } from '$lib/stores/viewport';
import { feedback } from '$lib/stores/feedback';
import { isStudio } from '$lib/system.js';
function resolveTheme(theme: AppStore['themeInUse']) {
switch (theme) {
case 'dark':
return isCloud ? ThemeDarkCloud : ThemeDark;
case 'light':
return isCloud ? ThemeLightCloud : ThemeLight;
return isStudio ? ThemeLightStudio : isCloud ? ThemeLightCloud : ThemeLight;
}
}
@@ -149,14 +156,14 @@
{#each preloadFonts as font}
<link rel="preload" href={font} as="font" type="font/woff2" crossorigin="anonymous" />
{/each}
<link rel="preload" as="style" type="text/css" href="/console/fonts/main.css" />
<link rel="preload" as="style" type="text/css" href={`${base}/fonts/main.css`} />
<link rel="stylesheet" href={`${base}/fonts/main.css`} />
{#if isCloud}
{#each preloadFontsCloud as font}
<link rel="preload" href={font} as="font" type="font/woff2" crossorigin="anonymous" />
{/each}
<link rel="preload" as="style" type="text/css" href="/console/fonts/cloud.css" />
<link rel="preload" as="style" type="text/css" href={`${base}/fonts/cloud.css`} />
<link rel="stylesheet" href={`${base}/fonts/cloud.css`} />
{/if}
</svelte:head>
+7 -2
View File
@@ -2,7 +2,7 @@ import { redirect } from '@sveltejs/kit';
import { base } from '$app/paths';
import type { PageLoad } from './$types';
import { sdk } from '$lib/stores/sdk';
import { VARS } from '$lib/system';
import { isStudio, VARS } from '$lib/system';
const handleGithubEducationMembership = async (name: string, email: string) => {
const result = await sdk.forConsole.billing.setMembership('github-student-developer');
@@ -39,7 +39,12 @@ export const load: PageLoad = async ({ parent, url }) => {
if (!teamId) {
redirect(303, `${base}/account/organizations${url.search}`);
} else {
redirect(303, `${base}/organization-${teamId}${url.search}`);
redirect(
303,
isStudio
? `${base}/org-${teamId}${url.search}`
: `${base}/organization-${teamId}${url.search}`
);
}
} else {
redirect(303, `${base}/onboarding/create-project${url.search}`);
+5
View File
@@ -3,3 +3,8 @@ export { default as ThemeLight } from './light.json';
export { default as ThemeDarkCloud } from './dark-cloud.json';
export { default as ThemeLightCloud } from './light-cloud.json';
import { default as StudioLightOverride } from './light-studio.json';
import { default as ThemeLightBase } from './light.json';
export const ThemeLightStudio = { ...ThemeLightBase, ...StudioLightOverride };
+4
View File
@@ -0,0 +1,4 @@
{
"bgcolor-accent": "var(--neutral-1000)",
"bgcolor-accent-secondary": "var(--neutral-900)"
}
+4 -1
View File
@@ -1,6 +1,9 @@
import adapter from '@sveltejs/adapter-static';
import { sveltePreprocess } from 'svelte-preprocess';
import { preprocessMeltUI, sequence } from '@melt-ui/pp';
import 'dotenv/config';
const projectProfile = process.env.PUBLIC_PROJECT_PROFILE || 'console';
/** @type {import('@sveltejs/kit').Config} */
const config = {
@@ -26,7 +29,7 @@ const config = {
precompress: true
}),
paths: {
base: '/console'
base: `/${projectProfile}`
}
},
vitePlugin: {
+1 -1
View File
@@ -12,7 +12,7 @@ export default defineConfig({
project: 'console'
}
}),
sveltekit()
sveltekit(),
],
optimizeDeps: {
include: ['echarts', 'prismjs']