Merge branch 'feat-profiles' of https://github.com/appwrite/console into feat-profiles-polishing

This commit is contained in:
Torsten Dittmann
2025-11-17 23:36:55 +04:00
10 changed files with 200 additions and 18 deletions
+49
View File
@@ -0,0 +1,49 @@
name: Dockerize Profiles
on:
push:
branches: [feat-profiles]
pull_request:
types: [opened, synchronize, reopened]
branches: [feat-profiles]
workflow_dispatch:
jobs:
dockerize-profiles:
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: appwrite/console-profiles
tags: |
type=ref,event=branch,prefix=branch-
type=ref,event=pr
type=sha,prefix=sha-
type=raw,value=gh-${{ github.run_id}}
flavor: |
latest=false
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+13
View File
@@ -31,9 +31,16 @@ ARG PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS
ARG PUBLIC_APPWRITE_ENDPOINT
ARG PUBLIC_GROWTH_ENDPOINT
ARG PUBLIC_STRIPE_KEY
ARG PUBLIC_SENTRY_DSN
ARG PUBLIC_SENTRY_INCLUDE_PII
ARG PUBLIC_SENTRY_ENVIRONMENT
ARG PUBLIC_SENTRY_TRACES_SAMPLE_RATE
ARG PUBLIC_SENTRY_REPLAY_SAMPLE_RATE
ARG PUBLIC_SENTRY_DEBUG
ARG SENTRY_AUTH_TOKEN
ARG SENTRY_RELEASE
ENV PUBLIC_APPWRITE_ENDPOINT=$PUBLIC_APPWRITE_ENDPOINT
ENV PUBLIC_GROWTH_ENDPOINT=$PUBLIC_GROWTH_ENDPOINT
ENV PUBLIC_CONSOLE_MODE=$PUBLIC_CONSOLE_MODE
@@ -44,6 +51,12 @@ ENV PUBLIC_APPWRITE_MULTI_REGION=$PUBLIC_APPWRITE_MULTI_REGION
ENV PUBLIC_CONSOLE_EMAIL_VERIFICATION=$PUBLIC_CONSOLE_EMAIL_VERIFICATION
ENV PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=$PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS
ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY
ENV PUBLIC_SENTRY_DSN=$PUBLIC_SENTRY_DSN
ENV PUBLIC_SENTRY_INCLUDE_PII=$PUBLIC_SENTRY_INCLUDE_PII
ENV PUBLIC_SENTRY_ENVIRONMENT=$PUBLIC_SENTRY_ENVIRONMENT
ENV PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$PUBLIC_SENTRY_TRACES_SAMPLE_RATE
ENV PUBLIC_SENTRY_REPLAY_SAMPLE_RATE=$PUBLIC_SENTRY_REPLAY_SAMPLE_RATE
ENV PUBLIC_SENTRY_DEBUG=$PUBLIC_SENTRY_DEBUG
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
ENV SENTRY_RELEASE=$SENTRY_RELEASE
ENV PUBLIC_IMAGINE_CDN_URL=$PUBLIC_IMAGINE_CDN_URL
+4 -9
View File
@@ -1,20 +1,15 @@
import * as Sentry from '@sentry/sveltekit';
import { isCloud, isProd } from '$lib/system';
import { AppwriteException } from '@appwrite.io/console';
import type { HandleClientError } from '@sveltejs/kit';
import { setupSentry } from '$lib/sentry';
Sentry.init({
enabled: isCloud && isProd,
dsn: 'https://c7ce178bdedd486480317b72f282fd39@o1063647.ingest.us.sentry.io/4504158071422976',
tracesSampleRate: 1,
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0
setupSentry({
withSessionReplay: true
});
export const handleError: HandleClientError = ({ error, message, status }) => {
console.error(error);
let type;
let type: string;
if (error instanceof AppwriteException) {
status = error.code === 0 ? undefined : error.code;
message = error.message;
+3 -6
View File
@@ -1,12 +1,9 @@
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 { setupSentry } from '$lib/sentry';
Sentry.init({
enabled: isCloud && isProd,
dsn: 'https://c7ce178bdedd486480317b72f282fd39@o1063647.ingest.us.sentry.io/4504158071422976',
tracesSampleRate: 1.0
setupSentry({
withSessionReplay: false
});
export const handle = sequence(sentryHandle());
+64
View File
@@ -0,0 +1,64 @@
import { env } from '$env/dynamic/public';
import * as Sentry from '@sentry/sveltekit';
import { isCloud } from './system';
const timestamp = Date.now();
export function getSessionId(userId: string) {
return `${userId}-${timestamp}`;
}
export function identify(userId: string) {
if (isCloud) {
Sentry.setContext('Imagine Request Context', {
session_id: getSessionId(userId)
});
Sentry.setUser({
id: userId
});
}
}
export function setupSentry({ withSessionReplay }: { withSessionReplay: boolean }) {
const dsn = env.PUBLIC_SENTRY_DSN;
const environment = env.PUBLIC_SENTRY_ENVIRONMENT;
if (!dsn) {
return;
}
if (dsn && !environment) {
throw new Error(
"PUBLIC_SENTRY_ENVIRONMENT is required when PUBLIC_SENTRY_DSN is set. For local development, set it to your name, e.g. 'torsten'."
);
}
const variables = {
enabled: true,
includePII: env.PUBLIC_SENTRY_INCLUDE_PII === 'true',
tracesSampleRate: parseFloat(env.PUBLIC_SENTRY_TRACES_SAMPLE_RATE),
replaysSessionSampleRate: parseFloat(env.PUBLIC_SENTRY_REPLAY_SAMPLE_RATE),
debug: env.PUBLIC_SENTRY_DEBUG === 'true'
};
const integrations = [];
if (withSessionReplay) {
integrations.push(
Sentry.replayIntegration({
maskAllText: variables.includePII ? false : true,
maskAllInputs: variables.includePII ? false : true
})
);
}
Sentry.init({
enabled: variables.enabled,
dsn,
tracesSampleRate: variables.tracesSampleRate,
replaysSessionSampleRate: variables.replaysSessionSampleRate,
replaysOnErrorSampleRate: 1,
integrations,
debug: variables.debug,
sendDefaultPii: true
});
}
+53
View File
@@ -0,0 +1,53 @@
const MONACO_STYLE_ATTRIBUTE = 'data-appwrite-studio-monaco-style';
let monacoStyleObserver: MutationObserver | null = null;
function findMonacoEditorCssLink(): HTMLLinkElement | null {
if (typeof document === 'undefined') {
return null;
}
const link = document.head?.querySelector<HTMLLinkElement>(
'link[rel="stylesheet"][href*="monaco-editor"][href*="editor.main.css"]'
);
return link ?? null;
}
function syncMonacoStyles(shadow: ShadowRoot) {
if (typeof document === 'undefined') {
return;
}
// Check if already synced
if (shadow.querySelector(`[${MONACO_STYLE_ATTRIBUTE}]`)) {
return;
}
const link = findMonacoEditorCssLink();
if (!link) {
return;
}
const clone = link.cloneNode(true) as HTMLLinkElement;
clone.setAttribute(MONACO_STYLE_ATTRIBUTE, 'true');
shadow.appendChild(clone);
}
export function ensureMonacoStyles(shadow: ShadowRoot) {
syncMonacoStyles(shadow);
if (
monacoStyleObserver ||
typeof MutationObserver === 'undefined' ||
typeof document === 'undefined'
) {
return;
}
monacoStyleObserver = new MutationObserver(() => {
syncMonacoStyles(shadow);
});
monacoStyleObserver.observe(document.head, { childList: true });
}
+6
View File
@@ -3,7 +3,9 @@ import { app } from '$lib/stores/app';
import { get } from 'svelte/store';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { ensureMonacoStyles } from './monaco-style-manager';
import DEV_CSS_URL from '@imagine.dev/web-components/imagine-web-components.css?url';
import { getSessionId } from '$lib/sentry';
const COMPONENT_SELECTOR = 'imagine-web-components-wrapper[data-appwrite-studio]';
const STYLE_ATTRIBUTE = 'data-appwrite-studio-style';
@@ -87,6 +89,7 @@ function injectStyles(node: HTMLElement, attempt = 0) {
}
if (shadow.querySelector<HTMLLinkElement>(`link[${STYLE_ATTRIBUTE}]`)) {
ensureMonacoStyles(shadow);
return;
}
@@ -95,6 +98,7 @@ function injectStyles(node: HTMLElement, attempt = 0) {
link.href = DEV_OVERRIDE_WEB_COMPONENTS ? DEV_CSS_URL : CDN_CSS_URL;
link.setAttribute(STYLE_ATTRIBUTE, 'true');
shadow.prepend(link);
ensureMonacoStyles(shadow);
})
.catch(() => {
/* no-op */
@@ -272,6 +276,7 @@ export function hideStudio() {
export async function initImagine(
region: string,
projectId: string,
userId: string,
callbacks?: {
onProjectNameChange: () => void;
onAddDomain: () => void | Promise<void>;
@@ -290,6 +295,7 @@ export async function initImagine(
},
{
initialTheme: get(app).themeInUse,
consoleSessionId: getSessionId(userId),
callbacks
}
);
+4 -2
View File
@@ -18,10 +18,12 @@
const {
region,
projectId
projectId,
userId
}: {
region: string;
projectId: string;
userId: string;
} = $props();
const siteId = `project-${projectId}`;
@@ -31,7 +33,7 @@
onMount(() => {
ensureStudioComponent();
initImagine(region, projectId, {
initImagine(region, projectId, userId, {
onProjectNameChange: () => {
invalidate(Dependencies.PROJECT);
},
+1 -1
View File
@@ -333,7 +333,7 @@
});
</script>
<Studio region={page.params.region} projectId={page.params.project} />
<Studio region={page.params.region} projectId={page.params.project} userId={data.account.$id} />
<CommandCenter />
<Shell
showSideNavigation={page.url.pathname !== '/' &&
+3
View File
@@ -11,6 +11,7 @@ import type { Account } from '$lib/stores/user';
import { type AppwriteException, Query } from '@appwrite.io/console';
import { isCloud, VARS } from '$lib/system';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
import { identify } from '$lib/sentry';
import { resolvedProfile } from '$lib/profiles/index.svelte';
export const ssr = false;
@@ -24,6 +25,8 @@ export const load: LayoutLoad = async ({ depends, url, route }) => {
.then((response) => [response, null])
.catch((error) => [null, error])) as [Account, AppwriteException];
if (account) identify(account.$id);
if (url.searchParams.has('forceRedirect')) {
redirectTo.set(url.searchParams.get('forceRedirect') || null);
url.searchParams.delete('forceRedirect');