From bdb2484b632fcd711088c88c17697644067e8845 Mon Sep 17 00:00:00 2001 From: Julien Leicher Date: Thu, 23 May 2024 11:41:19 +0200 Subject: [PATCH] feat: add custom registries support, closes #61 --- api.http | 42 +++- .../front/src/components/data-table.svelte | 2 +- .../front/src/components/registry-card.svelte | 30 +++ cmd/serve/front/src/lib/fetcher/cache.ts | 7 +- cmd/serve/front/src/lib/fetcher/index.ts | 5 + cmd/serve/front/src/lib/localization/en.ts | 39 ++- cmd/serve/front/src/lib/localization/fr.ts | 39 ++- cmd/serve/front/src/lib/path.ts | 5 +- .../front/src/lib/resources/registries.ts | 84 +++++++ cmd/serve/front/src/lib/resources/targets.ts | 9 - .../front/src/routes/(main)/+page.svelte | 4 +- .../src/routes/(main)/apps/[id]/+page.svelte | 6 +- .../src/routes/(main)/apps/app-form.svelte | 16 +- .../routes/(main)/registries/+error.svelte | 21 ++ .../src/routes/(main)/registries/+page.svelte | 30 +++ .../src/routes/(main)/registries/+page.ts | 9 + .../(main)/registries/[id]/edit/+page.svelte | 55 +++++ .../(main)/registries/[id]/edit/+page.ts | 14 ++ .../routes/(main)/registries/new/+page.svelte | 24 ++ .../(main)/registries/registry-form.svelte | 108 ++++++++ .../src/routes/(main)/targets/+page.svelte | 4 +- .../routes/(main)/targets/new/+page.svelte | 6 +- .../routes/(main)/targets/target-form.svelte | 2 +- .../front/src/routes/(main)/topbar.svelte | 1 + cmd/serve/registries.go | 95 ++++++++ cmd/serve/server.go | 5 + cmd/serve/targets.go | 4 +- .../app/create_registry/create_registry.go | 77 ++++++ .../create_registry/create_registry_test.go | 64 +++++ .../app/delete_registry/delete_registry.go | 33 +++ .../delete_registry/delete_registry_test.go | 44 ++++ internal/deployment/app/deploy/deploy.go | 9 +- internal/deployment/app/deploy/deploy_test.go | 5 +- .../app/get_registries/get_registries.go | 12 + .../app/get_registry/get_registry.go | 35 +++ .../app/update_registry/update_registry.go | 102 ++++++++ .../update_registry/update_registry_test.go | 152 ++++++++++++ internal/deployment/domain/app.go | 2 +- internal/deployment/domain/credentials.go | 28 +++ .../deployment/domain/credentials_test.go | 35 +++ internal/deployment/domain/provider.go | 2 +- internal/deployment/domain/registry.go | 230 ++++++++++++++++++ internal/deployment/domain/registry_test.go | 96 ++++++++ internal/deployment/domain/requirement.go | 22 ++ internal/deployment/domain/target.go | 2 +- internal/deployment/domain/url.go | 53 ++-- internal/deployment/domain/url_test.go | 3 + internal/deployment/domain/version_control.go | 4 - .../deployment/domain/version_control_test.go | 2 +- .../deployment/infra/memory/registries.go | 110 +++++++++ internal/deployment/infra/mod.go | 11 +- .../infra/provider/docker/client.go | 44 +++- .../infra/provider/docker/data_test.go | 66 +++-- .../infra/provider/docker/provider.go | 22 +- .../infra/provider/docker/provider_test.go | 4 +- internal/deployment/infra/provider/facade.go | 4 +- .../deployment/infra/provider/facade_test.go | 2 +- internal/deployment/infra/sqlite/gateway.go | 68 ++++++ .../1716482091_create_registries.up.sql | 12 + .../deployment/infra/sqlite/registries.go | 119 +++++++++ 60 files changed, 2006 insertions(+), 134 deletions(-) create mode 100644 cmd/serve/front/src/components/registry-card.svelte create mode 100644 cmd/serve/front/src/lib/resources/registries.ts create mode 100644 cmd/serve/front/src/routes/(main)/registries/+error.svelte create mode 100644 cmd/serve/front/src/routes/(main)/registries/+page.svelte create mode 100644 cmd/serve/front/src/routes/(main)/registries/+page.ts create mode 100644 cmd/serve/front/src/routes/(main)/registries/[id]/edit/+page.svelte create mode 100644 cmd/serve/front/src/routes/(main)/registries/[id]/edit/+page.ts create mode 100644 cmd/serve/front/src/routes/(main)/registries/new/+page.svelte create mode 100644 cmd/serve/front/src/routes/(main)/registries/registry-form.svelte create mode 100644 cmd/serve/registries.go create mode 100644 internal/deployment/app/create_registry/create_registry.go create mode 100644 internal/deployment/app/create_registry/create_registry_test.go create mode 100644 internal/deployment/app/delete_registry/delete_registry.go create mode 100644 internal/deployment/app/delete_registry/delete_registry_test.go create mode 100644 internal/deployment/app/get_registries/get_registries.go create mode 100644 internal/deployment/app/get_registry/get_registry.go create mode 100644 internal/deployment/app/update_registry/update_registry.go create mode 100644 internal/deployment/app/update_registry/update_registry_test.go create mode 100644 internal/deployment/domain/credentials.go create mode 100644 internal/deployment/domain/credentials_test.go create mode 100644 internal/deployment/domain/registry.go create mode 100644 internal/deployment/domain/registry_test.go create mode 100644 internal/deployment/infra/memory/registries.go create mode 100644 internal/deployment/infra/sqlite/migrations/1716482091_create_registries.up.sql create mode 100644 internal/deployment/infra/sqlite/registries.go diff --git a/api.http b/api.http index 62cb6899..a658ecac 100644 --- a/api.http +++ b/api.http @@ -61,7 +61,9 @@ Content-Type: application/json { "name": "docker outside", "url": "http://docker.localhost", - "docker": {} + "docker": { + + } } ### @@ -76,6 +78,44 @@ GET {{url}}/targets/{{createTarget.response.body.$.id}} DELETE {{url}}/targets/{{createTarget.response.body.$.id}} +### + +GET {{url}}/registries + +### + +# @name createRegistry + +POST {{url}}/registries +Content-Type: application/json + +{ + "name": "Local registry", + "url": "http://localhost:5000" +} + +### + +PATCH {{url}}/registries/{{createRegistry.response.body.$.id}} +Content-Type: application/json + +{ + "name": "Local registry", + "url": "http://localhost:5001", + "credentials": { + "username": "admin", + "password": "admin" + } +} + +### + +DELETE {{url}}/registries/{{createRegistry.response.body.$.id}} + +### + +GET {{url}}/registries/{{createRegistry.response.body.$.id}} + ### # @name createApp diff --git a/cmd/serve/front/src/components/data-table.svelte b/cmd/serve/front/src/components/data-table.svelte index 5fb81f7f..2718ff5d 100644 --- a/cmd/serve/front/src/components/data-table.svelte +++ b/cmd/serve/front/src/components/data-table.svelte @@ -30,7 +30,7 @@ {#if !data || data.length === 0} - +

{l.translate('datatable.no_data')}

diff --git a/cmd/serve/front/src/components/registry-card.svelte b/cmd/serve/front/src/components/registry-card.svelte new file mode 100644 index 00000000..89dfeccc --- /dev/null +++ b/cmd/serve/front/src/components/registry-card.svelte @@ -0,0 +1,30 @@ + + + +

{data.name}

+
{data.url}
+
+ + diff --git a/cmd/serve/front/src/lib/fetcher/cache.ts b/cmd/serve/front/src/lib/fetcher/cache.ts index c8ba2f29..ecc77a28 100644 --- a/cmd/serve/front/src/lib/fetcher/cache.ts +++ b/cmd/serve/front/src/lib/fetcher/cache.ts @@ -133,7 +133,12 @@ export default class CacheFetchService implements FetchService { const result = await api(method, url, body, options); // Invalidate all the cache entries that matches the base key - const keys = [url, ...(options?.invalidate ?? [])]; + const keys = options?.invalidate ?? []; + + if (!options?.skipUrlInvalidate) { + keys.push(url); + } + await this._cache.invalidate(...keys); return result; diff --git a/cmd/serve/front/src/lib/fetcher/index.ts b/cmd/serve/front/src/lib/fetcher/index.ts index a2a845ba..92b35ae3 100644 --- a/cmd/serve/front/src/lib/fetcher/index.ts +++ b/cmd/serve/front/src/lib/fetcher/index.ts @@ -31,6 +31,11 @@ export type MutateOptions = Pick & { * url but you can specify other urls to invalidate here. */ invalidate?: string[]; + /** + * By default the url at which the mutate is done is invalidated but you + * can override this behavior by setting this flag to true. + */ + skipUrlInvalidate?: boolean; }; export type QueryResult = { diff --git a/cmd/serve/front/src/lib/localization/en.ts b/cmd/serve/front/src/lib/localization/en.ts index 3293eb2c..e890a111 100644 --- a/cmd/serve/front/src/lib/localization/en.ts +++ b/cmd/serve/front/src/lib/localization/en.ts @@ -1,4 +1,5 @@ import type { Locale, Translations } from '$lib/localization'; +import routes from '$lib/path'; const translations = { // Authentication @@ -10,8 +11,7 @@ const translations = { 'You need at least one target to deploy your application. Head to the create target page to create one.', 'app.not_found': "Looks like the application you're looking for does not exist. Head back to the", 'app.not_found.cta': 'homepage', - 'app.blankslate.title': 'Looks like you have no application yet. Start by', - 'app.blankslate.cta': 'creating one!', + 'app.blankslate': `Looks like you have no application yet.
Applications represents services you want to deploy on your infrastructure. Start by creating one!`, 'app.new': 'New application', 'app.edit': 'Edit application', 'app.delete': 'Delete application', @@ -38,9 +38,7 @@ This action is IRREVERSIBLE and will DELETE ALL DATA associated with this applic 'app.vcs.help': 'If not under version control, you will still be able to manually deploy your application.', 'app.vcs.token': 'Access token', - 'app.vcs.token.help.instructions': - 'Token used to fetch the provided repository. Generally known as Personal Access Token, you can find some instructions for', - 'app.vcs.token.help.leave_empty': ', leave empty if the repository is public.', + 'app.vcs.token.help': `Token used to fetch the provided repository. Generally known as Personal Access Token, you can find some instructions for Github and Gitlab, leave empty if the repository is public.`, 'app.environment.production': 'Production settings', 'app.environment.staging': 'Staging settings', 'app.environment.target': 'Deploy target', @@ -76,8 +74,7 @@ This action is IRREVERSIBLE and will DELETE ALL DATA associated with this applic 'target.configuring': 'Target configuration in progress', 'target.configuring.description': 'Needed infrastructure is being deployed on the target, please wait.', - 'target.blankslate.title': 'Looks like you have no target yet. Start by', - 'target.blankslate.cta': 'creating one!', + 'target.blankslate': `Looks like you have no target yet.
Targets determine on which host your applications will be deployed and which provider should be used. Start by creating one!`, 'target.general': 'General settings', 'target.name.help': 'The name is being used only for display, it can be anything you want.', 'target.url.help': @@ -127,6 +124,25 @@ You may reconsider and try to make the target reachable before deleting it.`, 'deployment.command.delete_target': 'Target removal', 'deployment.command.configure_target': 'Target configuration', 'deployment.command.deploy': 'Application deployment', + // Registries + 'registry.new': 'New registry', + 'registry.blankslate': `Looks like you have no custom registry yet.
If some of your images are hosted on private registries, configure them here to make them available.`, + 'registry.not_found': + "Looks like the registry you're looking for does not exist. Head back to the", + 'registry.not_found.cta': 'registries page', + 'registry.delete': 'Delete registry', + 'registry.delete.confirm': (name: string) => + `Are you sure you want to delete the registry ${name}?`, + 'registry.delete.failed': 'Deletion failed', + 'registry.general': 'General settings', + 'registry.url.help': + 'Url of the registry. For a private Docker Hub registry, use the url https://index.docker.io/v1/.', + 'registry.authentication': 'Authentication', + 'registry.auth': 'Need authentication', + 'registry.auth.help': 'Does the registry require authentication?', + 'registry.username': 'Username', + 'registry.password': 'Password', + 'registry.name.help': 'The name is being used only for display, it can be anything you want.', // Account 'profile.my': 'my profile', 'profile.logout': 'log out', @@ -155,8 +171,10 @@ You may reconsider and try to make the target reachable before deleting it.`, 'deployment.promote.confirm': (number: number) => `The deployment #${number} will be promoted to the production environment. Do you confirm this action?`, 'deployment.promote.failed': 'Promote failed', - 'deployment.blankslate.title': 'No deployment to show. Go ahead and', - 'deployment.blankslate.cta': 'create the first one!', + 'deployment.blankslate': (app: string) => + `No deployment to show. Go ahead and create the first one!`, 'deployment.environment': 'Environment', 'deployment.payload': 'Payload', 'deployment.payload.copy_curl': 'Copy cURL command', @@ -204,6 +222,9 @@ You may reconsider and try to make the target reachable before deleting it.`, 'breadcrumb.target.new': 'New target', 'breadcrumb.target.settings': (name: string) => `${name} settings`, 'breadcrumb.jobs': 'Jobs', + 'breadcrumb.registries': 'Registries', + 'breadcrumb.registry.new': 'New registry', + 'breadcrumb.registry.settings': (name: string) => `${name} settings`, 'breadcrumb.profile': 'Profile', 'breadcrumb.not_found': 'Not found', // Footer diff --git a/cmd/serve/front/src/lib/localization/fr.ts b/cmd/serve/front/src/lib/localization/fr.ts index 3ed1b37e..85058e3d 100644 --- a/cmd/serve/front/src/lib/localization/fr.ts +++ b/cmd/serve/front/src/lib/localization/fr.ts @@ -1,4 +1,5 @@ import type { AppTranslations, Locale } from '$lib/localization'; +import routes from '$lib/path'; export default { code: 'fr', @@ -14,8 +15,7 @@ export default { 'app.not_found': "Il semblerait que l'application que vous recherchez n'existe pas. Retournez à la", 'app.not_found.cta': "page d'accueil", - 'app.blankslate.title': 'Aucune application trouvée, commencez par', - 'app.blankslate.cta': 'en créer une !', + 'app.blankslate': `Aucune application pour le moment.
Les applications représentent les services que vous souhaitez déployer sur votre infrastructure. Commencez par en créer une !`, 'app.new': 'Nouvelle application', 'app.edit': "Modifier l'application", 'app.delete': "Supprimer l'application", @@ -42,9 +42,7 @@ Cette action est IRRÉVERSIBLE et supprimera TOUTES LES DONNÉES associées sur 'app.vcs.help': "Si vous n'utilisez pas de contrôle de version, vous pourrez toujours déployer manuellement votre application.", 'app.vcs.token': "Jeton d'accès", - 'app.vcs.token.help.instructions': - "Jeton utilisé pour vous authentifier auprès du dépôt. Généralement connu sous le nom de Jeton d'accès personnel, vous pouvez trouver des instructions pour", - 'app.vcs.token.help.leave_empty': ', laissez vide si le dépôt est public.', + 'app.vcs.token.help': `Jeton utilisé pour vous authentifier auprès du dépôt. Généralement connu sous le nom de Jeton d'accès personnel, vous pouvez trouver des instructions pour Github et Gitlab, laissez vide si le dépôt est public.`, 'app.environment.production': 'Paramètres de production', 'app.environment.staging': 'Paramètres de staging', 'app.environment.target': 'Cible de déploiement', @@ -82,8 +80,7 @@ Cette action est IRRÉVERSIBLE et supprimera TOUTES LES DONNÉES associées sur }, 'target.configuring': 'Configuration de la cible en cours', 'target.configuring.description': `L'infrastructure nécessaire est en cours de déploiement, veuillez patienter.`, - 'target.blankslate.title': 'Aucune cible trouvée, commencez par', - 'target.blankslate.cta': 'en créer une !', + 'target.blankslate': `Aucune cible pour le moment.
Les cibles déterminent sur quel hôte vos applications seront déployées. Commencez par en créer une !`, 'target.general': 'Paramètres généraux', 'target.name.help': `Le nom est utilisé uniquement pour l'affichage. Vous pouvez choisir ce que vous voulez.`, 'target.url.help': `Toutes les applications déployées sur cette cible seront disponibles en tant que sous-domaine de cette URL racine (sans sous-chemin). Elle doit être unique parmi les cibles. Vous DEVEZ configurer un DNS wildcard pour les sous-domaines de telle sorte que *.<url configurée> redirige vers l'IP de cette cible.`, @@ -130,6 +127,25 @@ Vous devriez probablement essayer de rendre la cible accessible avant de la supp 'deployment.command.delete_target': 'Suppression de la cible', 'deployment.command.configure_target': 'Configuration de la cible', 'deployment.command.deploy': "Déploiement de l'application", + // Registries + 'registry.new': 'Nouveau registre', + 'registry.blankslate': `Aucun registre pour le moment.
Si certaines de vos images sont hébergées sur des registres privés, configurer les ici de manière à les rendre disponibles.`, + 'registry.not_found': + "Il semblerait que le registre que vous recherchez n'existe pas. Retournez à la", + 'registry.not_found.cta': 'page des registres', + 'registry.delete': 'Supprimer le registre', + 'registry.delete.confirm': (name: string) => + `Voulez-vous vraiment supprimer le registre ${name} ?`, + 'registry.delete.failed': 'Erreur de suppression', + 'registry.general': 'Paramètres généraux', + 'registry.url.help': + "Url du registre. Pour un registre privé Docker Hub, utiliser l'url https://index.docker.io/v1/.", + 'registry.authentication': 'Authentification', + 'registry.auth': 'Authentification nécessaire', + 'registry.auth.help': 'Le registre nécessite t-il une authentification ?', + 'registry.username': "Nom d'utilisateur", + 'registry.password': 'Mot de passe', + 'registry.name.help': `Le nom est utilisé uniquement pour l'affichage. Vous pouvez choisir ce que vous voulez.`, // Account 'profile.my': 'mon profil', 'profile.logout': 'se déconnecter', @@ -159,8 +175,10 @@ Vous devriez probablement essayer de rendre la cible accessible avant de la supp 'deployment.promote.confirm': (number: number) => `Le déploiement #${number} sera promu sur l'environnement de production. Confirmez-vous cette action ?`, 'deployment.promote.failed': 'Erreur lors de la promotion', - 'deployment.blankslate.title': 'Aucun déploiement trouvé. Commencez par', - 'deployment.blankslate.cta': 'en créer un !', + 'deployment.blankslate': (app: string) => + `Aucun déploiement trouvé. Commencez par en créer un !`, 'deployment.environment': 'Environnement', 'deployment.payload': 'Charge utile', 'deployment.payload.copy_curl': 'Copier la commande cURL', @@ -209,6 +227,9 @@ Vous devriez probablement essayer de rendre la cible accessible avant de la supp 'breadcrumb.target.new': 'Nouvelle cible', 'breadcrumb.target.settings': (name: string) => `Paramètres de ${name}`, 'breadcrumb.jobs': 'Tâches', + 'breadcrumb.registries': 'Registres', + 'breadcrumb.registry.new': 'Nouveau registre', + 'breadcrumb.registry.settings': (name: string) => `Paramètes de ${name}`, 'breadcrumb.profile': 'Profil', 'breadcrumb.not_found': 'Ressource introuvable', // Footer diff --git a/cmd/serve/front/src/lib/path.ts b/cmd/serve/front/src/lib/path.ts index f7a0a65e..c97d0c52 100644 --- a/cmd/serve/front/src/lib/path.ts +++ b/cmd/serve/front/src/lib/path.ts @@ -14,7 +14,10 @@ const routes = { targets: '/targets', createTarget: '/targets/new', editTarget: (id: string) => `/targets/${id}/edit`, - jobs: '/jobs' + jobs: '/jobs', + registries: '/registries', + createRegistry: '/registries/new', + editRegistry: (id: string) => `/registries/${id}/edit` } as const; export default routes; diff --git a/cmd/serve/front/src/lib/resources/registries.ts b/cmd/serve/front/src/lib/resources/registries.ts new file mode 100644 index 00000000..4f4d3815 --- /dev/null +++ b/cmd/serve/front/src/lib/resources/registries.ts @@ -0,0 +1,84 @@ +import { POLLING_INTERVAL_MS } from '$lib/config'; +import fetcher, { type FetchOptions, type FetchService, type QueryResult } from '$lib/fetcher'; +import type { ByUserData } from '$lib/resources/users'; + +export type Registry = { + id: string; + name: string; + url: string; + credentials?: Credentials; + created_at: string; + created_by: ByUserData; +}; + +export type Credentials = { + username: string; + password: string; +}; + +export type CreateRegistry = { + name: string; + url: string; + credentials?: Credentials; +}; + +export type UpdateRegistry = { + name?: string; + url?: string; + credentials: Patch<{ + username: string; + password?: string; + }>; +}; + +export interface RegistriesService { + create(payload: CreateRegistry): Promise; + update(id: string, payload: UpdateRegistry): Promise; + delete(id: string): Promise; + fetchAll(options?: FetchOptions): Promise; + fetchById(id: string, options?: FetchOptions): Promise; + queryAll(): QueryResult; +} + +type Options = { + pollingInterval: number; +}; + +export class RemoteRegistriesService implements RegistriesService { + constructor(private readonly _fetcher: FetchService, private readonly _options: Options) {} + + create(payload: CreateRegistry): Promise { + return this._fetcher.post('/api/v1/registries', payload); + } + + update(id: string, payload: UpdateRegistry): Promise { + return this._fetcher.patch(`/api/v1/registries/${id}`, payload); + } + + delete(id: string): Promise { + return this._fetcher.delete(`/api/v1/registries/${id}`, { + invalidate: ['/api/v1/registries'], + skipUrlInvalidate: true + }); + } + + queryAll(): QueryResult { + return this._fetcher.query('/api/v1/registries', { + refreshInterval: this._options.pollingInterval + }); + } + + fetchAll(options?: FetchOptions): Promise { + return this._fetcher.get('/api/v1/registries', options); + } + + fetchById(id: string, options?: FetchOptions): Promise { + return this._fetcher.get(`/api/v1/registries/${id}`, options); + } +} + +const service: RegistriesService = new RemoteRegistriesService(fetcher, { + pollingInterval: POLLING_INTERVAL_MS +}); + +export default service; diff --git a/cmd/serve/front/src/lib/resources/targets.ts b/cmd/serve/front/src/lib/resources/targets.ts index 0cfead20..d65e0398 100644 --- a/cmd/serve/front/src/lib/resources/targets.ts +++ b/cmd/serve/front/src/lib/resources/targets.ts @@ -70,10 +70,7 @@ export interface TargetsService { fetchAll(filters?: GetTargetsFilters, options?: FetchOptions): Promise; fetchById(id: string, options?: FetchOptions): Promise; queryAll(): QueryResult; - queryById(id: string): QueryResult; delete(id: string): Promise; - // fetchAll(options?: FetchOptions): Promise; - // fetchById(id: string, options?: FetchOptions): Promise; } type Options = { @@ -121,12 +118,6 @@ export class RemoteTargetsService implements TargetsService { refreshInterval: this._options.pollingInterval }); } - - queryById(id: string): QueryResult { - return this._fetcher.query(`/api/v1/targets/${id}`, { - refreshInterval: this._options.pollingInterval - }); - } } const service: TargetsService = new RemoteTargetsService(fetcher, { diff --git a/cmd/serve/front/src/routes/(main)/+page.svelte b/cmd/serve/front/src/routes/(main)/+page.svelte index b2d73c66..cf1c88c3 100644 --- a/cmd/serve/front/src/routes/(main)/+page.svelte +++ b/cmd/serve/front/src/routes/(main)/+page.svelte @@ -4,7 +4,6 @@ import Breadcrumb from '$components/breadcrumb.svelte'; import CardsGrid from '$components/cards-grid.svelte'; import Button from '$components/button.svelte'; - import Link from '$components/link.svelte'; import routes from '$lib/path'; import service from '$lib/resources/apps'; import l from '$lib/localization'; @@ -25,8 +24,7 @@ {:else}

- {l.translate('app.blankslate.title')} - {l.translate('app.blankslate.cta')} + {@html l.translate('app.blankslate')}

{/if} diff --git a/cmd/serve/front/src/routes/(main)/apps/[id]/+page.svelte b/cmd/serve/front/src/routes/(main)/apps/[id]/+page.svelte index 905fa084..c2a6640a 100644 --- a/cmd/serve/front/src/routes/(main)/apps/[id]/+page.svelte +++ b/cmd/serve/front/src/routes/(main)/apps/[id]/+page.svelte @@ -4,7 +4,6 @@ import Button from '$components/button.svelte'; import CleanupNotice from '$components/cleanup-notice.svelte'; import EnvironmentCard from '$components/environment-card.svelte'; - import Link from '$components/link.svelte'; import routes from '$lib/path'; import service from '$lib/resources/apps'; import l from '$lib/localization'; @@ -39,10 +38,7 @@ {:else}

- {l.translate('deployment.blankslate.title')} - - {l.translate('deployment.blankslate.cta')} - + {@html l.translate('deployment.blankslate', [data.app.id])}

{/if} diff --git a/cmd/serve/front/src/routes/(main)/apps/app-form.svelte b/cmd/serve/front/src/routes/(main)/apps/app-form.svelte index cda3560c..80a1878f 100644 --- a/cmd/serve/front/src/routes/(main)/apps/app-form.svelte +++ b/cmd/serve/front/src/routes/(main)/apps/app-form.svelte @@ -4,7 +4,6 @@ import FormErrors from '$components/form-errors.svelte'; import FormSection from '$components/form-section.svelte'; import Form from '$components/form.svelte'; - import Link from '$components/link.svelte'; import Panel from '$components/panel.svelte'; import Stack from '$components/stack.svelte'; import TextInput from '$components/text-input.svelte'; @@ -204,20 +203,7 @@ bind:value={token} >

- {@html l.translate('app.vcs.token.help.instructions')} - GitHub - {l.translate('and')} - GitLab{l.translate('app.vcs.token.help.leave_empty')} + {@html l.translate('app.vcs.token.help')}

{/if} diff --git a/cmd/serve/front/src/routes/(main)/registries/+error.svelte b/cmd/serve/front/src/routes/(main)/registries/+error.svelte new file mode 100644 index 00000000..517398ae --- /dev/null +++ b/cmd/serve/front/src/routes/(main)/registries/+error.svelte @@ -0,0 +1,21 @@ + + + + + +

+ {l.translate('registry.not_found')} + {l.translate('registry.not_found.cta')}. +

+
diff --git a/cmd/serve/front/src/routes/(main)/registries/+page.svelte b/cmd/serve/front/src/routes/(main)/registries/+page.svelte new file mode 100644 index 00000000..66a7dd52 --- /dev/null +++ b/cmd/serve/front/src/routes/(main)/registries/+page.svelte @@ -0,0 +1,30 @@ + + + +