From 3715da8fd303c1b2bd156f44d3a6ee17a379e9ea Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Wed, 20 May 2026 10:31:49 +0200 Subject: [PATCH] Add release channel selector components, subject autocomplete, and related tests - Implement components for `ReleaseChannelSelector`, including panel and sidebar variants. - Add `ReleaseAssignmentSubjectAutocomplete` for user, subuser, and customer search functionality. - Update i18n phrases for channel selector and subject assignment. - Enhance unit and E2E tests for release channel availability, switching, and runtime configuration. - Modify modules for channel-related state handling and runtime refresh mechanics. --- src/components/menus/user/UserLeftMenu.vue | 4 +- .../ReleaseAssignmentSubjectAutocomplete.vue | 404 ++++++++++++++ .../release/ReleaseChannelSelector.vue | 305 +++++++++++ .../release/ReleaseChannelSidebarSelector.vue | 34 ++ .../release/ReleaseChannelSwitched.vue | 15 +- .../release/ReleaseChannelUnavailable.vue | 38 +- src/components/session/token/SessionUser.vue | 32 +- .../page/headers/DesktopNavigationBuefy.vue | 2 + src/i18n/generated/da-v2.json | 31 +- src/i18n/generated/de-v2.json | 31 +- src/i18n/generated/en-v2.json | 31 +- src/i18n/generated/global-v2.json | 29 +- src/i18n/generated/no-v2.json | 31 +- src/i18n/generated/sv-v2.json | 31 +- src/i18n/locales/da.json | 31 +- src/i18n/locales/de.json | 31 +- src/i18n/locales/en.json | 31 +- src/i18n/locales/no.json | 31 +- src/i18n/locales/sv.json | 31 +- .../phrases/compat/configuration/index.json | 31 +- .../phrases/compat/configuration/index.json | 31 +- .../phrases/compat/configuration/index.json | 31 +- .../global/shared/configuration/index.json | 29 +- .../phrases/compat/configuration/index.json | 31 +- .../phrases/compat/configuration/index.json | 31 +- src/services/releaseChannelAvailability.js | 268 +++++++--- src/services/releaseTimeline.js | 59 +- src/services/superuserReleases.js | 6 + .../ConfigurationReleaseManager.vue | 504 ++++++------------ tests/e2e/release-channel-switched.spec.js | 16 +- tests/e2e/release-channel-unavailable.spec.js | 29 +- tests/e2e/release-manager.spec.js | 259 +++++++-- .../unit/release-channel-availability.spec.js | 117 +++- tests/unit/release-channel-selector.spec.js | 71 +++ tests/unit/release-timeline.spec.js | 5 +- 35 files changed, 2090 insertions(+), 601 deletions(-) create mode 100644 src/components/release/ReleaseAssignmentSubjectAutocomplete.vue create mode 100644 src/components/release/ReleaseChannelSelector.vue create mode 100644 src/components/release/ReleaseChannelSidebarSelector.vue create mode 100644 tests/unit/release-channel-selector.spec.js diff --git a/src/components/menus/user/UserLeftMenu.vue b/src/components/menus/user/UserLeftMenu.vue index e9e6dd51..f85ca91d 100644 --- a/src/components/menus/user/UserLeftMenu.vue +++ b/src/components/menus/user/UserLeftMenu.vue @@ -1,6 +1,7 @@ + + + + diff --git a/src/components/release/ReleaseChannelSelector.vue b/src/components/release/ReleaseChannelSelector.vue new file mode 100644 index 00000000..6a1a8ca3 --- /dev/null +++ b/src/components/release/ReleaseChannelSelector.vue @@ -0,0 +1,305 @@ + + + + + diff --git a/src/components/release/ReleaseChannelSidebarSelector.vue b/src/components/release/ReleaseChannelSidebarSelector.vue new file mode 100644 index 00000000..65e49f2e --- /dev/null +++ b/src/components/release/ReleaseChannelSidebarSelector.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/components/release/ReleaseChannelSwitched.vue b/src/components/release/ReleaseChannelSwitched.vue index e55f945e..3330456c 100644 --- a/src/components/release/ReleaseChannelSwitched.vue +++ b/src/components/release/ReleaseChannelSwitched.vue @@ -3,7 +3,6 @@ import { computed } from "vue"; import { useI18n } from "vue-i18n"; import { acknowledgeReleaseChannelSwitch, - redirectToConfiguredReleaseFrontend, releaseChannelSwitchedStatus, } from "@/services/releaseChannelAvailability.js"; @@ -18,10 +17,11 @@ const localizedChannelDescription = computed(() => { const key = `configuration.release_manager.channel_descriptions.${status.value.channelSlug}`; return status.value.channelSlug && te(key) ? t(key) : status.value.description; }); -const frontendLabel = computed(() => status.value.frontendBaseUrl || tr("current_app_image")); -const apiLabel = computed(() => status.value.apiBaseUrl || tr("current_api")); const frontendVersion = computed(() => status.value.versions?.frontend || null); const apiVersion = computed(() => status.value.versions?.api || null); +const bundleLabel = computed(() => + status.value.versions?.bundle_id ? `#${status.value.versions.bundle_id}` : tr("assigned_channel") +); const versionLabel = (version, fallback) => { return version?.version_label || version?.commit_sha || fallback; @@ -29,7 +29,6 @@ const versionLabel = (version, fallback) => { const continueToApp = () => { acknowledgeReleaseChannelSwitch(status.value.channel, status.value.principalKey); - redirectToConfiguredReleaseFrontend(); }; @@ -66,12 +65,8 @@ const continueToApp = () => {
{{ tr("frontend") }}{{ frontendLabel }} - {{ tr("api") }}{{ apiLabel }}{{ tr("bundle") }}{{ bundleLabel }} {{ tr("frontend_version") }} t(`configuration.release_manager.channel_unavai const checking = ref(false); const checkError = ref(""); const secondsUntilNextCheck = ref(RELEASE_CHANNEL_CHECK_INTERVAL_MS / 1000); +const switchingSlug = ref(""); let pollTimer = null; let countdownTimer = null; @@ -30,9 +33,12 @@ const localizedChannelDescription = computed(() => { return status.value.channelSlug && te(key) ? t(key) : status.value.description; }); const missingLabels = computed(() => - status.value.missing.map((key) => - key === "frontend_base_url" ? tr("frontend_url") : key === "api_base_url" ? tr("api_url") : key - ) + status.value.missing.map((key) => { + if (key === "release_bundle") return tr("release_bundle"); + if (key === "frontend_version") return tr("frontend_version"); + if (key === "api_version") return tr("api_version"); + return key; + }) ); const resetCountdown = () => { @@ -48,7 +54,6 @@ const checkAgain = async () => { checkError.value = ""; try { await SessionUser.refreshReleaseRuntime(); - redirectToConfiguredReleaseFrontend(); } catch (error) { checkError.value = tr("refresh_error"); } finally { @@ -61,6 +66,24 @@ const ignoreForFiveMinutes = () => { ignoreUnavailableReleaseChannel(status.value.channel); }; +const switchChannel = async (option) => { + const slug = selectReleaseChannel(option.channel); + if (!slug || switchingSlug.value) { + return; + } + + switchingSlug.value = slug; + checkError.value = ""; + try { + await SessionUser.refreshReleaseRuntime(); + } catch (error) { + checkError.value = tr("refresh_error"); + } finally { + switchingSlug.value = ""; + resetCountdown(); + } +}; + const logout = async () => { await SessionUser.auth.logout(); router.push({ name: "login" }); @@ -120,6 +143,11 @@ onBeforeUnmount(() => {
{{ label }}
+

{{ tr("checking_again", { seconds: secondsUntilNextCheck }) }}

diff --git a/src/components/session/token/SessionUser.vue b/src/components/session/token/SessionUser.vue index 927912e5..254f887d 100644 --- a/src/components/session/token/SessionUser.vue +++ b/src/components/session/token/SessionUser.vue @@ -48,7 +48,11 @@ import Swal from "sweetalert2"; import { SubuserGrants } from "@/components/session/token/SessionUser/Objects/SubuserGrants.vue"; import { Subusers } from "@/components/session/token/SessionUser/Objects/Subusers.vue"; import { configureReleaseRuntime } from "@/services/releaseTimeline.js"; -import { setReleaseChannelSwitchNoticePrincipal } from "@/services/releaseChannelAvailability.js"; +import { + reconcileSelectedReleaseChannel, + releaseChannelRuntimeRequestParams, + setReleaseChannelSwitchNoticePrincipal, +} from "@/services/releaseChannelAvailability.js"; const normalizePositiveInteger = (value) => { const parsedValue = Number.parseInt(String(value ?? ""), 10); @@ -92,16 +96,18 @@ const hydrateSessionFromStorage = () => { const applyReleaseRuntimeConfig = (runtime) => { const normalizedRuntime = runtime || {}; configureReleaseRuntime(normalizedRuntime); + reconcileSelectedReleaseChannel(normalizedRuntime); SessionUser.runtimeConfig.release.traceId.value = normalizedRuntime.trace_id || null; SessionUser.runtimeConfig.release.channel.value = normalizedRuntime.channel || null; - SessionUser.runtimeConfig.release.versions.value = normalizedRuntime.versions || { frontend: null, api: null }; - SessionUser.runtimeConfig.release.frontendBaseUrl.value = - normalizedRuntime.frontend_base_url || - normalizedRuntime.frontendBaseUrl || - normalizedRuntime?.channel?.frontend_base_url || - null; - SessionUser.runtimeConfig.release.apiBaseUrl.value = - normalizedRuntime.api_base_url || normalizedRuntime.apiBaseUrl || normalizedRuntime?.channel?.api_base_url || null; + SessionUser.runtimeConfig.release.availableChannels.value = + normalizedRuntime.available_channels || normalizedRuntime.availableChannels || []; + SessionUser.runtimeConfig.release.versions.value = normalizedRuntime.versions || { + frontend: null, + api: null, + bundle_id: null, + }; + SessionUser.runtimeConfig.release.frontendBaseUrl.value = null; + SessionUser.runtimeConfig.release.apiBaseUrl.value = null; SessionUser.runtimeConfig.release.availability.value = normalizedRuntime.availability || { configured: true, missing: [], @@ -117,7 +123,7 @@ const applyReleaseRuntimeConfig = (runtime) => { }; export const refreshReleaseRuntime = async () => { - return authenticatedRequest("/release/runtime", "GET") + return authenticatedRequest("/release/runtime", "GET", releaseChannelRuntimeRequestParams()) .then((response) => { applyReleaseRuntimeConfig(response?.data?.data || response?.data || {}); }) @@ -272,7 +278,7 @@ export const getSubuserSessionData = async () => { * @returns {Promise} */ export const getSessionData = async () => { - return await authenticatedRequest("/auth/session", "GET") + return await authenticatedRequest("/auth/session", "GET", releaseChannelRuntimeRequestParams()) .then((response) => { SessionUser.user.id.value = response.data.data.id; SessionUser.user.customer_number.value = response.data.data.customer_number; @@ -487,6 +493,7 @@ export const SessionUser = { release: { traceId: ref(null), channel: ref(null), + availableChannels: ref([]), versions: ref({ frontend: null, api: null }), frontendBaseUrl: ref(null), apiBaseUrl: ref(null), @@ -743,7 +750,8 @@ export const SessionUser = { SessionUser.runtimeConfig.economic.defaultDistributionDepartmentId.value = null; SessionUser.runtimeConfig.release.traceId.value = null; SessionUser.runtimeConfig.release.channel.value = null; - SessionUser.runtimeConfig.release.versions.value = { frontend: null, api: null }; + SessionUser.runtimeConfig.release.availableChannels.value = []; + SessionUser.runtimeConfig.release.versions.value = { frontend: null, api: null, bundle_id: null }; SessionUser.runtimeConfig.release.frontendBaseUrl.value = null; SessionUser.runtimeConfig.release.apiBaseUrl.value = null; SessionUser.runtimeConfig.release.availability.value = { diff --git a/src/components/viewport/page/headers/DesktopNavigationBuefy.vue b/src/components/viewport/page/headers/DesktopNavigationBuefy.vue index e57a7cbf..003261ee 100644 --- a/src/components/viewport/page/headers/DesktopNavigationBuefy.vue +++ b/src/components/viewport/page/headers/DesktopNavigationBuefy.vue @@ -8,6 +8,7 @@ import { ADMIN_DEPARTMENT_SELECTION_CLASS } from "@/components/models/navigation import { isDepartmentLabelValid, sortByDepartmentPriorityOrder } from "@/services/departmentVisibility.js"; import { isSmall } from "@/components/displays/pagination/PaginationDisplayIsSmall.vue"; import LanguageSelector from "@/components/i18n/LanguageSelector.vue"; +import ReleaseChannelSidebarSelector from "@/components/release/ReleaseChannelSidebarSelector.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue"; import NavigationMenuGlobalSearch from "@/components/viewport/page/headers/menu/NavigationMenuGlobalSearch.vue"; @@ -452,6 +453,7 @@ const getChildBadgeTestId = (item: NavigationMenuItem) => (isDraftsNavigationChi +
diff --git a/src/i18n/generated/da-v2.json b/src/i18n/generated/da-v2.json index eaef79e1..c40cc249 100644 --- a/src/i18n/generated/da-v2.json +++ b/src/i18n/generated/da-v2.json @@ -2569,9 +2569,10 @@ "kicker": "Release Manager", "title": "Release-kanalen er ikke klar", "summary_prefix": "Din konto er tildelt", - "summary_suffix": ", men kanalen mangler den konfiguration, der skal bruges for at indlæse dens release-image.", - "frontend_url": "Frontend-URL", - "api_url": "API-URL", + "summary_suffix": ", men kanalen mangler et aktivt release-bundle.", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-version", + "api_version": "API-version", "checking_again": "Tjekker igen om {seconds}s", "refresh_error": "Release-status kunne ikke opdateres. Det næste automatiske tjek prøver igen.", "ignore": "Ignorer de næste 5 minutter", @@ -2584,6 +2585,7 @@ "title": "Du er nu på {channel}", "summary_prefix": "Din konto er blevet tildelt release-kanalen", "summary_suffix": "Denne enhed husker, at du har set denne besked.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend-version", @@ -2594,6 +2596,17 @@ "base_image": "Basis-image", "continue": "Fortsæt" }, + "channel_selector": { + "title": "Release-kanal", + "subtitle": "Vælg hvilken tildelt release-kanal denne enhed skal bruge.", + "sidebar_title": "Release", + "default": "Standard", + "ready": "Klar", + "unavailable": "Ikke klar", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-version", + "api_version": "API-version" + }, "common": { "yes": "ja", "no": "nej", @@ -2743,6 +2756,18 @@ "assignments": { "title": "Tildelinger", "subtitle": "Fastgør brugere, medarbejdere eller kunder til release-kanaler.", + "subject": "Emne", + "subject_message": "Søg efter brugere, medarbejdere eller kunder, eller skriv et manuelt emne som user:42.", + "subject_placeholder": "Søg eller skriv emne", + "subject_empty": "Ingen emner fundet", + "group_users": "Brugere", + "group_subusers": "Medarbejdere", + "group_customers": "Kunder", + "group_manual": "Manuel", + "manual_user": "Bruger #{id}", + "manual_subuser": "Medarbejder #{id}", + "manual_customer": "Kunde #{id}", + "manual_description": "Brug indtastet værdi {subject}", "subject_type": "Emnetype", "subject_type_message": "Vælg identitetstypen, der skal fastgøres.", "customer": "Kunde", diff --git a/src/i18n/generated/de-v2.json b/src/i18n/generated/de-v2.json index e58d86ab..71ceb5e6 100644 --- a/src/i18n/generated/de-v2.json +++ b/src/i18n/generated/de-v2.json @@ -2679,9 +2679,10 @@ "kicker": "Release Manager", "title": "Release-Kanal ist nicht bereit", "summary_prefix": "Dein Konto ist zugewiesen zu", - "summary_suffix": ", aber diesem Kanal fehlt die Konfiguration, um sein Release-Image zu laden.", - "frontend_url": "Frontend-URL", - "api_url": "API-URL", + "summary_suffix": ", aber diesem Kanal fehlt ein aktives Release-Bundle.", + "release_bundle": "Release-Bundle", + "frontend_version": "Frontend-Version", + "api_version": "API-Version", "checking_again": "Erneute Prüfung in {seconds}s", "refresh_error": "Release-Status konnte nicht aktualisiert werden. Die nächste automatische Prüfung versucht es erneut.", "ignore": "Nächste 5 Minuten ignorieren", @@ -2694,6 +2695,7 @@ "title": "Du bist jetzt auf {channel}", "summary_prefix": "Dein Konto wurde dem Release-Kanal", "summary_suffix": "zugewiesen. Dieses Gerät merkt sich, dass du diesen Hinweis gesehen hast.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend-Version", @@ -2704,6 +2706,17 @@ "base_image": "Basis-Image", "continue": "Fortfahren" }, + "channel_selector": { + "title": "Release-Kanal", + "subtitle": "Wählen Sie, welchen zugewiesenen Release-Kanal dieses Gerät verwenden soll.", + "sidebar_title": "Release", + "default": "Standard", + "ready": "Bereit", + "unavailable": "Nicht bereit", + "release_bundle": "Release-Bundle", + "frontend_version": "Frontend-Version", + "api_version": "API-Version" + }, "common": { "yes": "ja", "no": "nein", @@ -2853,6 +2866,18 @@ "assignments": { "title": "Zuweisungen", "subtitle": "Benutzer, Mitarbeiter oder Kunden an Release-Kanäle binden.", + "subject": "Subjekt", + "subject_message": "Benutzer, Subuser oder Kunden suchen oder ein manuelles Subjekt wie user:42 eingeben.", + "subject_placeholder": "Subjekt suchen oder eingeben", + "subject_empty": "Keine Subjekte gefunden", + "group_users": "Benutzer", + "group_subusers": "Subuser", + "group_customers": "Kunden", + "group_manual": "Manuell", + "manual_user": "Benutzer #{id}", + "manual_subuser": "Subuser #{id}", + "manual_customer": "Kunde #{id}", + "manual_description": "Eingegebenen Wert {subject} verwenden", "subject_type": "Subjekttyp", "subject_type_message": "Identitätstyp zum Binden auswählen.", "customer": "Customer", diff --git a/src/i18n/generated/en-v2.json b/src/i18n/generated/en-v2.json index bffd0195..605b357a 100644 --- a/src/i18n/generated/en-v2.json +++ b/src/i18n/generated/en-v2.json @@ -2403,9 +2403,10 @@ "kicker": "Release Manager", "title": "Release channel is not ready", "summary_prefix": "Your account is assigned to", - "summary_suffix": ", but that channel is missing the configuration needed to load its release image.", - "frontend_url": "Frontend URL", - "api_url": "API URL", + "summary_suffix": ", but that channel is missing an active release bundle.", + "release_bundle": "Release bundle", + "frontend_version": "Frontend version", + "api_version": "API version", "checking_again": "Checking again in {seconds}s", "refresh_error": "Release status could not be refreshed. The next automatic check will try again.", "ignore": "Ignore next 5 minutes", @@ -2418,6 +2419,7 @@ "title": "You are now on {channel}", "summary_prefix": "Your account has been assigned to the", "summary_suffix": "release channel. This device will remember that you have seen this notice.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend version", @@ -2428,6 +2430,17 @@ "base_image": "Base image", "continue": "Continue" }, + "channel_selector": { + "title": "Release channel", + "subtitle": "Choose which assigned release channel this device should use.", + "sidebar_title": "Release", + "default": "Default", + "ready": "Ready", + "unavailable": "Not ready", + "release_bundle": "Release bundle", + "frontend_version": "Frontend version", + "api_version": "API version" + }, "common": { "yes": "yes", "no": "no", @@ -2577,6 +2590,18 @@ "assignments": { "title": "Assignments", "subtitle": "Pin users, subusers, or customers to release channels.", + "subject": "Subject", + "subject_message": "Search users, subusers, or customers, or type a manual subject like user:42.", + "subject_placeholder": "Search or type subject", + "subject_empty": "No subjects found", + "group_users": "Users", + "group_subusers": "Subusers", + "group_customers": "Customers", + "group_manual": "Manual", + "manual_user": "User #{id}", + "manual_subuser": "Subuser #{id}", + "manual_customer": "Customer #{id}", + "manual_description": "Use typed value {subject}", "subject_type": "Subject type", "subject_type_message": "Choose the identity type to pin.", "customer": "Customer", diff --git a/src/i18n/generated/global-v2.json b/src/i18n/generated/global-v2.json index f31953ec..d0993073 100644 --- a/src/i18n/generated/global-v2.json +++ b/src/i18n/generated/global-v2.json @@ -1629,8 +1629,9 @@ "title": "@:{'templates.generated.compat.configuration.release_manager.channel_unavailable.title'}", "summary_prefix": "@:{'templates.generated.compat.configuration.release_manager.channel_unavailable.summary_prefix'}", "summary_suffix": "@:{'templates.generated.compat.configuration.release_manager.channel_unavailable.summary_suffix'}", - "frontend_url": "@:{'templates.generated.compat.configuration.release_manager.channel_unavailable.frontend_url'}", - "api_url": "@:{'templates.generated.compat.configuration.release_manager.channel_unavailable.api_url'}", + "release_bundle": "@:{'templates.generated.compat.configuration.release_manager.channel_unavailable.release_bundle'}", + "frontend_version": "@:{'templates.generated.compat.configuration.release_manager.channel_unavailable.frontend_version'}", + "api_version": "@:{'templates.generated.compat.configuration.release_manager.channel_unavailable.api_version'}", "checking_again": "@:{'templates.generated.compat.configuration.release_manager.channel_unavailable.checking_again'}", "refresh_error": "@:{'templates.generated.compat.configuration.release_manager.channel_unavailable.refresh_error'}", "ignore": "@:{'templates.generated.compat.configuration.release_manager.channel_unavailable.ignore'}", @@ -1643,6 +1644,7 @@ "title": "@:{'templates.generated.compat.configuration.release_manager.channel_switched.title'}", "summary_prefix": "@:{'templates.generated.compat.configuration.release_manager.channel_switched.summary_prefix'}", "summary_suffix": "@:{'templates.generated.compat.configuration.release_manager.channel_switched.summary_suffix'}", + "bundle": "@:{'templates.generated.compat.configuration.release_manager.channel_switched.bundle'}", "frontend": "@:{'templates.generated.compat.configuration.release_manager.channel_switched.frontend'}", "api": "@:{'templates.generated.compat.configuration.release_manager.channel_switched.api'}", "frontend_version": "@:{'templates.generated.compat.configuration.release_manager.channel_switched.frontend_version'}", @@ -1653,6 +1655,17 @@ "base_image": "@:{'templates.generated.compat.configuration.release_manager.channel_switched.base_image'}", "continue": "@:{'templates.generated.compat.configuration.release_manager.channel_switched.continue'}" }, + "channel_selector": { + "title": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.title'}", + "subtitle": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.subtitle'}", + "sidebar_title": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.sidebar_title'}", + "default": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.default'}", + "ready": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.ready'}", + "unavailable": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.unavailable'}", + "release_bundle": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.release_bundle'}", + "frontend_version": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.frontend_version'}", + "api_version": "@:{'templates.generated.compat.configuration.release_manager.channel_selector.api_version'}" + }, "common": { "yes": "@:{'templates.generated.compat.configuration.release_manager.common.yes'}", "no": "@:{'templates.generated.compat.configuration.release_manager.common.no'}", @@ -1802,6 +1815,18 @@ "assignments": { "title": "@:{'templates.generated.compat.configuration.release_manager.assignments.title'}", "subtitle": "@:{'templates.generated.compat.configuration.release_manager.assignments.subtitle'}", + "subject": "@:{'templates.generated.compat.configuration.release_manager.assignments.subject'}", + "subject_message": "@:{'templates.generated.compat.configuration.release_manager.assignments.subject_message'}", + "subject_placeholder": "@:{'templates.generated.compat.configuration.release_manager.assignments.subject_placeholder'}", + "subject_empty": "@:{'templates.generated.compat.configuration.release_manager.assignments.subject_empty'}", + "group_users": "@:{'templates.generated.compat.configuration.release_manager.assignments.group_users'}", + "group_subusers": "@:{'templates.generated.compat.configuration.release_manager.assignments.group_subusers'}", + "group_customers": "@:{'templates.generated.compat.configuration.release_manager.assignments.group_customers'}", + "group_manual": "@:{'templates.generated.compat.configuration.release_manager.assignments.group_manual'}", + "manual_user": "@:{'templates.generated.compat.configuration.release_manager.assignments.manual_user'}", + "manual_subuser": "@:{'templates.generated.compat.configuration.release_manager.assignments.manual_subuser'}", + "manual_customer": "@:{'templates.generated.compat.configuration.release_manager.assignments.manual_customer'}", + "manual_description": "@:{'templates.generated.compat.configuration.release_manager.assignments.manual_description'}", "subject_type": "@:{'templates.generated.compat.configuration.release_manager.assignments.subject_type'}", "subject_type_message": "@:{'templates.generated.compat.configuration.release_manager.assignments.subject_type_message'}", "customer": "@:{'templates.generated.compat.configuration.release_manager.assignments.customer'}", diff --git a/src/i18n/generated/no-v2.json b/src/i18n/generated/no-v2.json index 6bff508f..ef54a73d 100644 --- a/src/i18n/generated/no-v2.json +++ b/src/i18n/generated/no-v2.json @@ -2680,9 +2680,10 @@ "kicker": "Release Manager", "title": "Release-kanalen er ikke klar", "summary_prefix": "Kontoen din er tildelt", - "summary_suffix": ", men kanalen mangler konfigurasjonen som trengs for å laste release-imaget.", - "frontend_url": "Frontend-URL", - "api_url": "API-URL", + "summary_suffix": ", men kanalen mangler en aktiv release-bundle.", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-versjon", + "api_version": "API-versjon", "checking_again": "Sjekker igjen om {seconds}s", "refresh_error": "Release-status kunne ikke oppdateres. Neste automatiske sjekk prøver igjen.", "ignore": "Ignorer de neste 5 minuttene", @@ -2695,6 +2696,7 @@ "title": "Du er nå på {channel}", "summary_prefix": "Kontoen din er tildelt release-kanalen", "summary_suffix": "Denne enheten husker at du har sett denne meldingen.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend-versjon", @@ -2705,6 +2707,17 @@ "base_image": "Basis-image", "continue": "Fortsett" }, + "channel_selector": { + "title": "Release-kanal", + "subtitle": "Velg hvilken tildelt release-kanal denne enheten skal bruke.", + "sidebar_title": "Release", + "default": "Standard", + "ready": "Klar", + "unavailable": "Ikke klar", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-versjon", + "api_version": "API-versjon" + }, "common": { "yes": "ja", "no": "nei", @@ -2854,6 +2867,18 @@ "assignments": { "title": "Tildelinger", "subtitle": "Fest brukere, medarbeidere eller kunder til release-kanaler.", + "subject": "Emne", + "subject_message": "Søk etter brukere, medarbeidere eller kunder, eller skriv et manuelt emne som user:42.", + "subject_placeholder": "Søk eller skriv emne", + "subject_empty": "Ingen emner funnet", + "group_users": "Brukere", + "group_subusers": "Medarbeidere", + "group_customers": "Kunder", + "group_manual": "Manuell", + "manual_user": "Bruker #{id}", + "manual_subuser": "Medarbeider #{id}", + "manual_customer": "Kunde #{id}", + "manual_description": "Bruk inntastet verdi {subject}", "subject_type": "Emnetype", "subject_type_message": "Vælg identitetstypen, der skal fastgøres.", "customer": "Kunde", diff --git a/src/i18n/generated/sv-v2.json b/src/i18n/generated/sv-v2.json index 1f371a94..4986fab8 100644 --- a/src/i18n/generated/sv-v2.json +++ b/src/i18n/generated/sv-v2.json @@ -2730,9 +2730,10 @@ "kicker": "Release Manager", "title": "Release-kanalen är inte klar", "summary_prefix": "Ditt konto är tilldelat", - "summary_suffix": ", men kanalen saknar konfigurationen som behövs för att läsa in dess release-image.", - "frontend_url": "Frontend-URL", - "api_url": "API-URL", + "summary_suffix": ", men kanalen saknar ett aktivt release-bundle.", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-version", + "api_version": "API-version", "checking_again": "Kontrollerar igen om {seconds}s", "refresh_error": "Release-status kunde inte uppdateras. Nästa automatiska kontroll försöker igen.", "ignore": "Ignorera de kommande 5 minuterna", @@ -2745,6 +2746,7 @@ "title": "Du är nu på {channel}", "summary_prefix": "Ditt konto har tilldelats release-kanalen", "summary_suffix": "Den här enheten kommer ihåg att du har sett detta meddelande.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend-version", @@ -2755,6 +2757,17 @@ "base_image": "Bas-image", "continue": "Fortsätt" }, + "channel_selector": { + "title": "Release-kanal", + "subtitle": "Välj vilken tilldelad release-kanal den här enheten ska använda.", + "sidebar_title": "Release", + "default": "Standard", + "ready": "Klar", + "unavailable": "Inte klar", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-version", + "api_version": "API-version" + }, "common": { "yes": "ja", "no": "nej", @@ -2904,6 +2917,18 @@ "assignments": { "title": "Tilldelningar", "subtitle": "Fäst användare, medarbetare eller kunder till release-kanaler.", + "subject": "Ämne", + "subject_message": "Sök efter användare, medarbetare eller kunder, eller skriv ett manuellt ämne som user:42.", + "subject_placeholder": "Sök eller skriv ämne", + "subject_empty": "Inga ämnen hittades", + "group_users": "Användare", + "group_subusers": "Medarbetare", + "group_customers": "Kunder", + "group_manual": "Manuell", + "manual_user": "Användare #{id}", + "manual_subuser": "Medarbetare #{id}", + "manual_customer": "Kund #{id}", + "manual_description": "Använd inmatat värde {subject}", "subject_type": "Ämnestyp", "subject_type_message": "Välj identitetstypen som ska fästas.", "customer": "Kunde", diff --git a/src/i18n/locales/da.json b/src/i18n/locales/da.json index c3d08c28..a6cb3658 100644 --- a/src/i18n/locales/da.json +++ b/src/i18n/locales/da.json @@ -1627,9 +1627,10 @@ "kicker": "Release Manager", "title": "Release-kanalen er ikke klar", "summary_prefix": "Din konto er tildelt", - "summary_suffix": ", men kanalen mangler den konfiguration, der skal bruges for at indlæse dens release-image.", - "frontend_url": "Frontend-URL", - "api_url": "API-URL", + "summary_suffix": ", men kanalen mangler et aktivt release-bundle.", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-version", + "api_version": "API-version", "checking_again": "Tjekker igen om {seconds}s", "refresh_error": "Release-status kunne ikke opdateres. Det næste automatiske tjek prøver igen.", "ignore": "Ignorer de næste 5 minutter", @@ -1642,6 +1643,7 @@ "title": "Du er nu på {channel}", "summary_prefix": "Din konto er blevet tildelt release-kanalen", "summary_suffix": "Denne enhed husker, at du har set denne besked.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend-version", @@ -1652,6 +1654,17 @@ "base_image": "Basis-image", "continue": "Fortsæt" }, + "channel_selector": { + "title": "Release-kanal", + "subtitle": "Vælg hvilken tildelt release-kanal denne enhed skal bruge.", + "sidebar_title": "Release", + "default": "Standard", + "ready": "Klar", + "unavailable": "Ikke klar", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-version", + "api_version": "API-version" + }, "common": { "yes": "ja", "no": "nej", @@ -1801,6 +1814,18 @@ "assignments": { "title": "Tildelinger", "subtitle": "Fastgør brugere, medarbejdere eller kunder til release-kanaler.", + "subject": "Emne", + "subject_message": "Søg efter brugere, medarbejdere eller kunder, eller skriv et manuelt emne som user:42.", + "subject_placeholder": "Søg eller skriv emne", + "subject_empty": "Ingen emner fundet", + "group_users": "Brugere", + "group_subusers": "Medarbejdere", + "group_customers": "Kunder", + "group_manual": "Manuel", + "manual_user": "Bruger #{id}", + "manual_subuser": "Medarbejder #{id}", + "manual_customer": "Kunde #{id}", + "manual_description": "Brug indtastet værdi {subject}", "subject_type": "Emnetype", "subject_type_message": "Vælg identitetstypen, der skal fastgøres.", "customer": "Kunde", diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index bcb2beb2..f0305305 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -1627,9 +1627,10 @@ "kicker": "Release Manager", "title": "Release-Kanal ist nicht bereit", "summary_prefix": "Dein Konto ist zugewiesen zu", - "summary_suffix": ", aber diesem Kanal fehlt die Konfiguration, um sein Release-Image zu laden.", - "frontend_url": "Frontend-URL", - "api_url": "API-URL", + "summary_suffix": ", aber diesem Kanal fehlt ein aktives Release-Bundle.", + "release_bundle": "Release-Bundle", + "frontend_version": "Frontend-Version", + "api_version": "API-Version", "checking_again": "Erneute Prüfung in {seconds}s", "refresh_error": "Release-Status konnte nicht aktualisiert werden. Die nächste automatische Prüfung versucht es erneut.", "ignore": "Nächste 5 Minuten ignorieren", @@ -1642,6 +1643,7 @@ "title": "Du bist jetzt auf {channel}", "summary_prefix": "Dein Konto wurde dem Release-Kanal", "summary_suffix": "zugewiesen. Dieses Gerät merkt sich, dass du diesen Hinweis gesehen hast.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend-Version", @@ -1652,6 +1654,17 @@ "base_image": "Basis-Image", "continue": "Fortfahren" }, + "channel_selector": { + "title": "Release-Kanal", + "subtitle": "Wählen Sie, welchen zugewiesenen Release-Kanal dieses Gerät verwenden soll.", + "sidebar_title": "Release", + "default": "Standard", + "ready": "Bereit", + "unavailable": "Nicht bereit", + "release_bundle": "Release-Bundle", + "frontend_version": "Frontend-Version", + "api_version": "API-Version" + }, "common": { "yes": "ja", "no": "nein", @@ -1801,6 +1814,18 @@ "assignments": { "title": "Zuweisungen", "subtitle": "Benutzer, Mitarbeiter oder Kunden an Release-Kanäle binden.", + "subject": "Subjekt", + "subject_message": "Benutzer, Subuser oder Kunden suchen oder ein manuelles Subjekt wie user:42 eingeben.", + "subject_placeholder": "Subjekt suchen oder eingeben", + "subject_empty": "Keine Subjekte gefunden", + "group_users": "Benutzer", + "group_subusers": "Subuser", + "group_customers": "Kunden", + "group_manual": "Manuell", + "manual_user": "Benutzer #{id}", + "manual_subuser": "Subuser #{id}", + "manual_customer": "Kunde #{id}", + "manual_description": "Eingegebenen Wert {subject} verwenden", "subject_type": "Subjekttyp", "subject_type_message": "Identitätstyp zum Binden auswählen.", "customer": "Customer", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 0061715e..9110240c 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1627,9 +1627,10 @@ "kicker": "Release Manager", "title": "Release channel is not ready", "summary_prefix": "Your account is assigned to", - "summary_suffix": ", but that channel is missing the configuration needed to load its release image.", - "frontend_url": "Frontend URL", - "api_url": "API URL", + "summary_suffix": ", but that channel is missing an active release bundle.", + "release_bundle": "Release bundle", + "frontend_version": "Frontend version", + "api_version": "API version", "checking_again": "Checking again in {seconds}s", "refresh_error": "Release status could not be refreshed. The next automatic check will try again.", "ignore": "Ignore next 5 minutes", @@ -1642,6 +1643,7 @@ "title": "You are now on {channel}", "summary_prefix": "Your account has been assigned to the", "summary_suffix": "release channel. This device will remember that you have seen this notice.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend version", @@ -1652,6 +1654,17 @@ "base_image": "Base image", "continue": "Continue" }, + "channel_selector": { + "title": "Release channel", + "subtitle": "Choose which assigned release channel this device should use.", + "sidebar_title": "Release", + "default": "Default", + "ready": "Ready", + "unavailable": "Not ready", + "release_bundle": "Release bundle", + "frontend_version": "Frontend version", + "api_version": "API version" + }, "common": { "yes": "yes", "no": "no", @@ -1801,6 +1814,18 @@ "assignments": { "title": "Assignments", "subtitle": "Pin users, subusers, or customers to release channels.", + "subject": "Subject", + "subject_message": "Search users, subusers, or customers, or type a manual subject like user:42.", + "subject_placeholder": "Search or type subject", + "subject_empty": "No subjects found", + "group_users": "Users", + "group_subusers": "Subusers", + "group_customers": "Customers", + "group_manual": "Manual", + "manual_user": "User #{id}", + "manual_subuser": "Subuser #{id}", + "manual_customer": "Customer #{id}", + "manual_description": "Use typed value {subject}", "subject_type": "Subject type", "subject_type_message": "Choose the identity type to pin.", "customer": "Customer", diff --git a/src/i18n/locales/no.json b/src/i18n/locales/no.json index f7ae3091..96576fd1 100644 --- a/src/i18n/locales/no.json +++ b/src/i18n/locales/no.json @@ -1627,9 +1627,10 @@ "kicker": "Release Manager", "title": "Release-kanalen er ikke klar", "summary_prefix": "Kontoen din er tildelt", - "summary_suffix": ", men kanalen mangler konfigurasjonen som trengs for å laste release-imaget.", - "frontend_url": "Frontend-URL", - "api_url": "API-URL", + "summary_suffix": ", men kanalen mangler en aktiv release-bundle.", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-versjon", + "api_version": "API-versjon", "checking_again": "Sjekker igjen om {seconds}s", "refresh_error": "Release-status kunne ikke oppdateres. Neste automatiske sjekk prøver igjen.", "ignore": "Ignorer de neste 5 minuttene", @@ -1642,6 +1643,7 @@ "title": "Du er nå på {channel}", "summary_prefix": "Kontoen din er tildelt release-kanalen", "summary_suffix": "Denne enheten husker at du har sett denne meldingen.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend-versjon", @@ -1652,6 +1654,17 @@ "base_image": "Basis-image", "continue": "Fortsett" }, + "channel_selector": { + "title": "Release-kanal", + "subtitle": "Velg hvilken tildelt release-kanal denne enheten skal bruke.", + "sidebar_title": "Release", + "default": "Standard", + "ready": "Klar", + "unavailable": "Ikke klar", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-versjon", + "api_version": "API-versjon" + }, "common": { "yes": "ja", "no": "nei", @@ -1801,6 +1814,18 @@ "assignments": { "title": "Tildelinger", "subtitle": "Fest brukere, medarbeidere eller kunder til release-kanaler.", + "subject": "Emne", + "subject_message": "Søk etter brukere, medarbeidere eller kunder, eller skriv et manuelt emne som user:42.", + "subject_placeholder": "Søk eller skriv emne", + "subject_empty": "Ingen emner funnet", + "group_users": "Brukere", + "group_subusers": "Medarbeidere", + "group_customers": "Kunder", + "group_manual": "Manuell", + "manual_user": "Bruker #{id}", + "manual_subuser": "Medarbeider #{id}", + "manual_customer": "Kunde #{id}", + "manual_description": "Bruk inntastet verdi {subject}", "subject_type": "Emnetype", "subject_type_message": "Vælg identitetstypen, der skal fastgøres.", "customer": "Kunde", diff --git a/src/i18n/locales/sv.json b/src/i18n/locales/sv.json index 7bf7c27f..e9bd815d 100644 --- a/src/i18n/locales/sv.json +++ b/src/i18n/locales/sv.json @@ -1627,9 +1627,10 @@ "kicker": "Release Manager", "title": "Release-kanalen är inte klar", "summary_prefix": "Ditt konto är tilldelat", - "summary_suffix": ", men kanalen saknar konfigurationen som behövs för att läsa in dess release-image.", - "frontend_url": "Frontend-URL", - "api_url": "API-URL", + "summary_suffix": ", men kanalen saknar ett aktivt release-bundle.", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-version", + "api_version": "API-version", "checking_again": "Kontrollerar igen om {seconds}s", "refresh_error": "Release-status kunde inte uppdateras. Nästa automatiska kontroll försöker igen.", "ignore": "Ignorera de kommande 5 minuterna", @@ -1642,6 +1643,7 @@ "title": "Du är nu på {channel}", "summary_prefix": "Ditt konto har tilldelats release-kanalen", "summary_suffix": "Den här enheten kommer ihåg att du har sett detta meddelande.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend-version", @@ -1652,6 +1654,17 @@ "base_image": "Bas-image", "continue": "Fortsätt" }, + "channel_selector": { + "title": "Release-kanal", + "subtitle": "Välj vilken tilldelad release-kanal den här enheten ska använda.", + "sidebar_title": "Release", + "default": "Standard", + "ready": "Klar", + "unavailable": "Inte klar", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-version", + "api_version": "API-version" + }, "common": { "yes": "ja", "no": "nej", @@ -1801,6 +1814,18 @@ "assignments": { "title": "Tilldelningar", "subtitle": "Fäst användare, medarbetare eller kunder till release-kanaler.", + "subject": "Ämne", + "subject_message": "Sök efter användare, medarbetare eller kunder, eller skriv ett manuellt ämne som user:42.", + "subject_placeholder": "Sök eller skriv ämne", + "subject_empty": "Inga ämnen hittades", + "group_users": "Användare", + "group_subusers": "Medarbetare", + "group_customers": "Kunder", + "group_manual": "Manuell", + "manual_user": "Användare #{id}", + "manual_subuser": "Medarbetare #{id}", + "manual_customer": "Kund #{id}", + "manual_description": "Använd inmatat värde {subject}", "subject_type": "Ämnestyp", "subject_type_message": "Välj identitetstypen som ska fästas.", "customer": "Kunde", diff --git a/src/i18n/source/da/phrases/compat/configuration/index.json b/src/i18n/source/da/phrases/compat/configuration/index.json index f86ab3f9..90045c3c 100644 --- a/src/i18n/source/da/phrases/compat/configuration/index.json +++ b/src/i18n/source/da/phrases/compat/configuration/index.json @@ -430,9 +430,10 @@ "kicker": "Release Manager", "title": "Release-kanalen er ikke klar", "summary_prefix": "Din konto er tildelt", - "summary_suffix": ", men kanalen mangler den konfiguration, der skal bruges for at indlæse dens release-image.", - "frontend_url": "Frontend-URL", - "api_url": "API-URL", + "summary_suffix": ", men kanalen mangler et aktivt release-bundle.", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-version", + "api_version": "API-version", "checking_again": "Tjekker igen om {seconds}s", "refresh_error": "Release-status kunne ikke opdateres. Det næste automatiske tjek prøver igen.", "ignore": "Ignorer de næste 5 minutter", @@ -445,6 +446,7 @@ "title": "Du er nu på {channel}", "summary_prefix": "Din konto er blevet tildelt release-kanalen", "summary_suffix": "Denne enhed husker, at du har set denne besked.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend-version", @@ -455,6 +457,17 @@ "base_image": "Basis-image", "continue": "Fortsæt" }, + "channel_selector": { + "title": "Release-kanal", + "subtitle": "Vælg hvilken tildelt release-kanal denne enhed skal bruge.", + "sidebar_title": "Release", + "default": "Standard", + "ready": "Klar", + "unavailable": "Ikke klar", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-version", + "api_version": "API-version" + }, "common": { "yes": "ja", "no": "nej", @@ -604,6 +617,18 @@ "assignments": { "title": "Tildelinger", "subtitle": "Fastgør brugere, medarbejdere eller kunder til release-kanaler.", + "subject": "Emne", + "subject_message": "Søg efter brugere, medarbejdere eller kunder, eller skriv et manuelt emne som user:42.", + "subject_placeholder": "Søg eller skriv emne", + "subject_empty": "Ingen emner fundet", + "group_users": "Brugere", + "group_subusers": "Medarbejdere", + "group_customers": "Kunder", + "group_manual": "Manuel", + "manual_user": "Bruger #{id}", + "manual_subuser": "Medarbejder #{id}", + "manual_customer": "Kunde #{id}", + "manual_description": "Brug indtastet værdi {subject}", "subject_type": "Emnetype", "subject_type_message": "Vælg identitetstypen, der skal fastgøres.", "customer": "Kunde", diff --git a/src/i18n/source/de/phrases/compat/configuration/index.json b/src/i18n/source/de/phrases/compat/configuration/index.json index b6917a11..74f856dd 100644 --- a/src/i18n/source/de/phrases/compat/configuration/index.json +++ b/src/i18n/source/de/phrases/compat/configuration/index.json @@ -430,9 +430,10 @@ "kicker": "Release Manager", "title": "Release-Kanal ist nicht bereit", "summary_prefix": "Dein Konto ist zugewiesen zu", - "summary_suffix": ", aber diesem Kanal fehlt die Konfiguration, um sein Release-Image zu laden.", - "frontend_url": "Frontend-URL", - "api_url": "API-URL", + "summary_suffix": ", aber diesem Kanal fehlt ein aktives Release-Bundle.", + "release_bundle": "Release-Bundle", + "frontend_version": "Frontend-Version", + "api_version": "API-Version", "checking_again": "Erneute Prüfung in {seconds}s", "refresh_error": "Release-Status konnte nicht aktualisiert werden. Die nächste automatische Prüfung versucht es erneut.", "ignore": "Nächste 5 Minuten ignorieren", @@ -445,6 +446,7 @@ "title": "Du bist jetzt auf {channel}", "summary_prefix": "Dein Konto wurde dem Release-Kanal", "summary_suffix": "zugewiesen. Dieses Gerät merkt sich, dass du diesen Hinweis gesehen hast.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend-Version", @@ -455,6 +457,17 @@ "base_image": "Basis-Image", "continue": "Fortfahren" }, + "channel_selector": { + "title": "Release-Kanal", + "subtitle": "Wählen Sie, welchen zugewiesenen Release-Kanal dieses Gerät verwenden soll.", + "sidebar_title": "Release", + "default": "Standard", + "ready": "Bereit", + "unavailable": "Nicht bereit", + "release_bundle": "Release-Bundle", + "frontend_version": "Frontend-Version", + "api_version": "API-Version" + }, "common": { "yes": "ja", "no": "nein", @@ -604,6 +617,18 @@ "assignments": { "title": "Zuweisungen", "subtitle": "Benutzer, Mitarbeiter oder Kunden an Release-Kanäle binden.", + "subject": "Subjekt", + "subject_message": "Benutzer, Subuser oder Kunden suchen oder ein manuelles Subjekt wie user:42 eingeben.", + "subject_placeholder": "Subjekt suchen oder eingeben", + "subject_empty": "Keine Subjekte gefunden", + "group_users": "Benutzer", + "group_subusers": "Subuser", + "group_customers": "Kunden", + "group_manual": "Manuell", + "manual_user": "Benutzer #{id}", + "manual_subuser": "Subuser #{id}", + "manual_customer": "Kunde #{id}", + "manual_description": "Eingegebenen Wert {subject} verwenden", "subject_type": "Subjekttyp", "subject_type_message": "Identitätstyp zum Binden auswählen.", "customer": "Customer", diff --git a/src/i18n/source/en/phrases/compat/configuration/index.json b/src/i18n/source/en/phrases/compat/configuration/index.json index 7098e92f..655925b2 100644 --- a/src/i18n/source/en/phrases/compat/configuration/index.json +++ b/src/i18n/source/en/phrases/compat/configuration/index.json @@ -430,9 +430,10 @@ "kicker": "Release Manager", "title": "Release channel is not ready", "summary_prefix": "Your account is assigned to", - "summary_suffix": ", but that channel is missing the configuration needed to load its release image.", - "frontend_url": "Frontend URL", - "api_url": "API URL", + "summary_suffix": ", but that channel is missing an active release bundle.", + "release_bundle": "Release bundle", + "frontend_version": "Frontend version", + "api_version": "API version", "checking_again": "Checking again in {seconds}s", "refresh_error": "Release status could not be refreshed. The next automatic check will try again.", "ignore": "Ignore next 5 minutes", @@ -445,6 +446,7 @@ "title": "You are now on {channel}", "summary_prefix": "Your account has been assigned to the", "summary_suffix": "release channel. This device will remember that you have seen this notice.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend version", @@ -455,6 +457,17 @@ "base_image": "Base image", "continue": "Continue" }, + "channel_selector": { + "title": "Release channel", + "subtitle": "Choose which assigned release channel this device should use.", + "sidebar_title": "Release", + "default": "Default", + "ready": "Ready", + "unavailable": "Not ready", + "release_bundle": "Release bundle", + "frontend_version": "Frontend version", + "api_version": "API version" + }, "common": { "yes": "yes", "no": "no", @@ -604,6 +617,18 @@ "assignments": { "title": "Assignments", "subtitle": "Pin users, subusers, or customers to release channels.", + "subject": "Subject", + "subject_message": "Search users, subusers, or customers, or type a manual subject like user:42.", + "subject_placeholder": "Search or type subject", + "subject_empty": "No subjects found", + "group_users": "Users", + "group_subusers": "Subusers", + "group_customers": "Customers", + "group_manual": "Manual", + "manual_user": "User #{id}", + "manual_subuser": "Subuser #{id}", + "manual_customer": "Customer #{id}", + "manual_description": "Use typed value {subject}", "subject_type": "Subject type", "subject_type_message": "Choose the identity type to pin.", "customer": "Customer", diff --git a/src/i18n/source/global/shared/configuration/index.json b/src/i18n/source/global/shared/configuration/index.json index 4838cb2a..5f36e2ba 100644 --- a/src/i18n/source/global/shared/configuration/index.json +++ b/src/i18n/source/global/shared/configuration/index.json @@ -484,8 +484,9 @@ "title": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.title'}", "summary_prefix": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.summary_prefix'}", "summary_suffix": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.summary_suffix'}", - "frontend_url": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.frontend_url'}", - "api_url": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.api_url'}", + "release_bundle": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.release_bundle'}", + "frontend_version": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.frontend_version'}", + "api_version": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.api_version'}", "checking_again": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.checking_again'}", "refresh_error": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.refresh_error'}", "ignore": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.ignore'}", @@ -498,6 +499,7 @@ "title": "@:{'phrases.compat.configuration.release_manager.channel_switched.title'}", "summary_prefix": "@:{'phrases.compat.configuration.release_manager.channel_switched.summary_prefix'}", "summary_suffix": "@:{'phrases.compat.configuration.release_manager.channel_switched.summary_suffix'}", + "bundle": "@:{'phrases.compat.configuration.release_manager.channel_switched.bundle'}", "frontend": "@:{'phrases.compat.configuration.release_manager.channel_switched.frontend'}", "api": "@:{'phrases.compat.configuration.release_manager.channel_switched.api'}", "frontend_version": "@:{'phrases.compat.configuration.release_manager.channel_switched.frontend_version'}", @@ -508,6 +510,17 @@ "base_image": "@:{'phrases.compat.configuration.release_manager.channel_switched.base_image'}", "continue": "@:{'phrases.compat.configuration.release_manager.channel_switched.continue'}" }, + "channel_selector": { + "title": "@:{'phrases.compat.configuration.release_manager.channel_selector.title'}", + "subtitle": "@:{'phrases.compat.configuration.release_manager.channel_selector.subtitle'}", + "sidebar_title": "@:{'phrases.compat.configuration.release_manager.channel_selector.sidebar_title'}", + "default": "@:{'phrases.compat.configuration.release_manager.channel_selector.default'}", + "ready": "@:{'phrases.compat.configuration.release_manager.channel_selector.ready'}", + "unavailable": "@:{'phrases.compat.configuration.release_manager.channel_selector.unavailable'}", + "release_bundle": "@:{'phrases.compat.configuration.release_manager.channel_selector.release_bundle'}", + "frontend_version": "@:{'phrases.compat.configuration.release_manager.channel_selector.frontend_version'}", + "api_version": "@:{'phrases.compat.configuration.release_manager.channel_selector.api_version'}" + }, "common": { "yes": "@:{'phrases.compat.configuration.release_manager.common.yes'}", "no": "@:{'phrases.compat.configuration.release_manager.common.no'}", @@ -657,6 +670,18 @@ "assignments": { "title": "@:{'phrases.compat.configuration.release_manager.assignments.title'}", "subtitle": "@:{'phrases.compat.configuration.release_manager.assignments.subtitle'}", + "subject": "@:{'phrases.compat.configuration.release_manager.assignments.subject'}", + "subject_message": "@:{'phrases.compat.configuration.release_manager.assignments.subject_message'}", + "subject_placeholder": "@:{'phrases.compat.configuration.release_manager.assignments.subject_placeholder'}", + "subject_empty": "@:{'phrases.compat.configuration.release_manager.assignments.subject_empty'}", + "group_users": "@:{'phrases.compat.configuration.release_manager.assignments.group_users'}", + "group_subusers": "@:{'phrases.compat.configuration.release_manager.assignments.group_subusers'}", + "group_customers": "@:{'phrases.compat.configuration.release_manager.assignments.group_customers'}", + "group_manual": "@:{'phrases.compat.configuration.release_manager.assignments.group_manual'}", + "manual_user": "@:{'phrases.compat.configuration.release_manager.assignments.manual_user'}", + "manual_subuser": "@:{'phrases.compat.configuration.release_manager.assignments.manual_subuser'}", + "manual_customer": "@:{'phrases.compat.configuration.release_manager.assignments.manual_customer'}", + "manual_description": "@:{'phrases.compat.configuration.release_manager.assignments.manual_description'}", "subject_type": "@:{'phrases.compat.configuration.release_manager.assignments.subject_type'}", "subject_type_message": "@:{'phrases.compat.configuration.release_manager.assignments.subject_type_message'}", "customer": "@:{'phrases.compat.configuration.release_manager.assignments.customer'}", diff --git a/src/i18n/source/no/phrases/compat/configuration/index.json b/src/i18n/source/no/phrases/compat/configuration/index.json index 20fe8e7b..d8c287f4 100644 --- a/src/i18n/source/no/phrases/compat/configuration/index.json +++ b/src/i18n/source/no/phrases/compat/configuration/index.json @@ -430,9 +430,10 @@ "kicker": "Release Manager", "title": "Release-kanalen er ikke klar", "summary_prefix": "Kontoen din er tildelt", - "summary_suffix": ", men kanalen mangler konfigurasjonen som trengs for å laste release-imaget.", - "frontend_url": "Frontend-URL", - "api_url": "API-URL", + "summary_suffix": ", men kanalen mangler en aktiv release-bundle.", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-versjon", + "api_version": "API-versjon", "checking_again": "Sjekker igjen om {seconds}s", "refresh_error": "Release-status kunne ikke oppdateres. Neste automatiske sjekk prøver igjen.", "ignore": "Ignorer de neste 5 minuttene", @@ -445,6 +446,7 @@ "title": "Du er nå på {channel}", "summary_prefix": "Kontoen din er tildelt release-kanalen", "summary_suffix": "Denne enheten husker at du har sett denne meldingen.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend-versjon", @@ -455,6 +457,17 @@ "base_image": "Basis-image", "continue": "Fortsett" }, + "channel_selector": { + "title": "Release-kanal", + "subtitle": "Velg hvilken tildelt release-kanal denne enheten skal bruke.", + "sidebar_title": "Release", + "default": "Standard", + "ready": "Klar", + "unavailable": "Ikke klar", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-versjon", + "api_version": "API-versjon" + }, "common": { "yes": "ja", "no": "nei", @@ -604,6 +617,18 @@ "assignments": { "title": "Tildelinger", "subtitle": "Fest brukere, medarbeidere eller kunder til release-kanaler.", + "subject": "Emne", + "subject_message": "Søk etter brukere, medarbeidere eller kunder, eller skriv et manuelt emne som user:42.", + "subject_placeholder": "Søk eller skriv emne", + "subject_empty": "Ingen emner funnet", + "group_users": "Brukere", + "group_subusers": "Medarbeidere", + "group_customers": "Kunder", + "group_manual": "Manuell", + "manual_user": "Bruker #{id}", + "manual_subuser": "Medarbeider #{id}", + "manual_customer": "Kunde #{id}", + "manual_description": "Bruk inntastet verdi {subject}", "subject_type": "Emnetype", "subject_type_message": "Vælg identitetstypen, der skal fastgøres.", "customer": "Kunde", diff --git a/src/i18n/source/sv/phrases/compat/configuration/index.json b/src/i18n/source/sv/phrases/compat/configuration/index.json index a94a8d41..2c887c9d 100644 --- a/src/i18n/source/sv/phrases/compat/configuration/index.json +++ b/src/i18n/source/sv/phrases/compat/configuration/index.json @@ -430,9 +430,10 @@ "kicker": "Release Manager", "title": "Release-kanalen är inte klar", "summary_prefix": "Ditt konto är tilldelat", - "summary_suffix": ", men kanalen saknar konfigurationen som behövs för att läsa in dess release-image.", - "frontend_url": "Frontend-URL", - "api_url": "API-URL", + "summary_suffix": ", men kanalen saknar ett aktivt release-bundle.", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-version", + "api_version": "API-version", "checking_again": "Kontrollerar igen om {seconds}s", "refresh_error": "Release-status kunde inte uppdateras. Nästa automatiska kontroll försöker igen.", "ignore": "Ignorera de kommande 5 minuterna", @@ -445,6 +446,7 @@ "title": "Du är nu på {channel}", "summary_prefix": "Ditt konto har tilldelats release-kanalen", "summary_suffix": "Den här enheten kommer ihåg att du har sett detta meddelande.", + "bundle": "Bundle", "frontend": "Frontend", "api": "API", "frontend_version": "Frontend-version", @@ -455,6 +457,17 @@ "base_image": "Bas-image", "continue": "Fortsätt" }, + "channel_selector": { + "title": "Release-kanal", + "subtitle": "Välj vilken tilldelad release-kanal den här enheten ska använda.", + "sidebar_title": "Release", + "default": "Standard", + "ready": "Klar", + "unavailable": "Inte klar", + "release_bundle": "Release-bundle", + "frontend_version": "Frontend-version", + "api_version": "API-version" + }, "common": { "yes": "ja", "no": "nej", @@ -604,6 +617,18 @@ "assignments": { "title": "Tilldelningar", "subtitle": "Fäst användare, medarbetare eller kunder till release-kanaler.", + "subject": "Ämne", + "subject_message": "Sök efter användare, medarbetare eller kunder, eller skriv ett manuellt ämne som user:42.", + "subject_placeholder": "Sök eller skriv ämne", + "subject_empty": "Inga ämnen hittades", + "group_users": "Användare", + "group_subusers": "Medarbetare", + "group_customers": "Kunder", + "group_manual": "Manuell", + "manual_user": "Användare #{id}", + "manual_subuser": "Medarbetare #{id}", + "manual_customer": "Kund #{id}", + "manual_description": "Använd inmatat värde {subject}", "subject_type": "Ämnestyp", "subject_type_message": "Välj identitetstypen som ska fästas.", "customer": "Kunde", diff --git a/src/services/releaseChannelAvailability.js b/src/services/releaseChannelAvailability.js index a93ab8cf..4c2f6046 100644 --- a/src/services/releaseChannelAvailability.js +++ b/src/services/releaseChannelAvailability.js @@ -3,6 +3,7 @@ import { releaseRuntimeState } from "@/services/releaseTimeline.js"; export const RELEASE_CHANNEL_IGNORE_STORAGE_KEY = "release_channel_unavailable_ignore_until"; export const RELEASE_CHANNEL_SWITCH_NOTICE_STORAGE_KEY = "release_channel_switch_notice_seen"; +export const RELEASE_CHANNEL_SELECTION_STORAGE_KEY = "release_channel_selected_slug"; export const RELEASE_CHANNEL_IGNORE_MS = 5 * 60 * 1000; export const RELEASE_CHANNEL_CHECK_INTERVAL_MS = 10 * 1000; @@ -56,11 +57,50 @@ const writeSwitchNoticeMap = (map) => { } }; +const normalizeReleaseChannelSlug = (value) => { + const slug = String(value || "") + .trim() + .toLowerCase() + .replace(/[^a-z0-9_-]/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); + return slug.slice(0, 64); +}; + +const readSelectedChannelSlug = () => { + if (typeof window === "undefined") { + return ""; + } + + try { + return normalizeReleaseChannelSlug(window.localStorage.getItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY) || ""); + } catch { + return ""; + } +}; + +const writeSelectedChannelSlug = (slug) => { + if (typeof window === "undefined") { + return; + } + + try { + if (slug) { + window.localStorage.setItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY, slug); + } else { + window.localStorage.removeItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY); + } + } catch { + // Storage is optional; the runtime endpoint will fall back to the assigned channel. + } +}; + const state = reactive({ now: Date.now(), ignoredUntilByChannel: readIgnoreMap(), switchNoticeSeenByKey: readSwitchNoticeMap(), switchNoticePrincipalKey: "", + selectedChannelSlug: readSelectedChannelSlug(), }); let clockTimer = null; @@ -72,7 +112,7 @@ export const setReleaseChannelSwitchNoticePrincipal = (principalKey) => { }; export const releaseChannelKey = (channel) => { - const slug = String(channel?.slug || "").trim(); + const slug = normalizeReleaseChannelSlug(channel?.slug || ""); if (slug) { return slug; } @@ -80,39 +120,16 @@ export const releaseChannelKey = (channel) => { return id ? `id:${id}` : ""; }; -export const normalizeReleaseUrl = (value) => { - const raw = String(value || "").trim(); - if (!raw) { - return ""; - } - - try { - const url = new URL(/^https?:\/\//i.test(raw) ? raw : `https://${raw}`); - return `${url.protocol}//${url.host}`; - } catch { - return raw.replace(/\/+$/, ""); - } -}; - -const runtimeUrl = (runtime, key) => { - const camelKey = key === "frontend_base_url" ? "frontendBaseUrl" : "apiBaseUrl"; - return normalizeReleaseUrl(runtime?.[camelKey] || runtime?.[key] || runtime?.channel?.[key]); -}; - const hasOwn = (value, key) => Boolean(value && Object.prototype.hasOwnProperty.call(value, key)); -const hasExplicitRuntimeTargets = (runtime) => { +const hasExplicitReleaseRuntime = (runtime) => { return ( - hasOwn(runtime, "frontend_base_url") || - hasOwn(runtime, "frontendBaseUrl") || - hasOwn(runtime, "api_base_url") || - hasOwn(runtime, "apiBaseUrl") || - hasOwn(runtime?.channel, "frontend_base_url") || - hasOwn(runtime?.channel, "api_base_url") + Boolean(runtime?.availability && typeof runtime.availability === "object" && runtime.availability.explicit !== false) || + hasOwn(runtime, "versions") ); }; -const runtimeAvailability = (runtime, frontendBaseUrl, apiBaseUrl) => { +const runtimeAvailability = (runtime, channel) => { if (runtime?.availability && typeof runtime.availability === "object") { const missing = Array.isArray(runtime.availability.missing) ? runtime.availability.missing : []; const configured = @@ -126,7 +143,7 @@ const runtimeAvailability = (runtime, frontendBaseUrl, apiBaseUrl) => { }; } - if (!hasExplicitRuntimeTargets(runtime)) { + if (!hasOwn(runtime, "versions")) { return { configured: true, missing: [], @@ -134,12 +151,26 @@ const runtimeAvailability = (runtime, frontendBaseUrl, apiBaseUrl) => { }; } - const missing = []; - if (!frontendBaseUrl) { - missing.push("frontend_base_url"); + const isDefaultChannel = + channel?.default_channel === true || channel?.default_channel === 1 || String(channel?.slug || "") === "stable"; + if (isDefaultChannel) { + return { + configured: true, + missing: [], + status: "ready", + }; } - if (!apiBaseUrl) { - missing.push("api_base_url"); + + const versions = runtime?.versions || {}; + const missing = []; + if (!versions.bundle_id) { + missing.push("release_bundle"); + } + if (!versions.frontend) { + missing.push("frontend_version"); + } + if (!versions.api) { + missing.push("api_version"); } return { @@ -149,19 +180,89 @@ const runtimeAvailability = (runtime, frontendBaseUrl, apiBaseUrl) => { }; }; +const channelDisplayName = (channel, fallback = "Release channel") => { + const slug = String(channel?.slug || "").trim(); + return String(channel?.name || slug || fallback).trim(); +}; + +const normalizeReleaseChannelOption = (entry = {}, runtime = {}) => { + const channel = entry?.channel && typeof entry.channel === "object" ? entry.channel : entry; + const channelSlug = normalizeReleaseChannelSlug(channel?.slug || ""); + const channelName = channelDisplayName(channel); + const defaultChannel = + channel?.default_channel === true || channel?.default_channel === 1 || channelSlug === "stable"; + const versions = + entry?.versions || + (releaseChannelKey(channel) === releaseChannelKey(runtime?.channel) ? runtime?.versions || {} : {}); + const availability = + entry?.availability && typeof entry.availability === "object" + ? entry.availability + : runtimeAvailability({ channel, versions }, channel); + + return { + channel, + channelSlug, + channelName, + defaultChannel, + description: channel?.description || "", + versions, + availability, + configured: availability.configured !== false, + missing: Array.isArray(availability.missing) ? availability.missing : [], + status: availability.status || (availability.configured === false ? "unconfigured" : "ready"), + }; +}; + +export const getReleaseChannelOptions = (runtime = {}) => { + const source = Array.isArray(runtime?.availableChannels) + ? runtime.availableChannels + : Array.isArray(runtime?.available_channels) + ? runtime.available_channels + : []; + const entries = source.length > 0 ? source : runtime?.channel ? [{ channel: runtime.channel }] : []; + const optionsByKey = new Map(); + + entries.forEach((entry) => { + const option = normalizeReleaseChannelOption(entry, runtime); + const key = releaseChannelKey(option.channel); + if (key && !optionsByKey.has(key)) { + optionsByKey.set(key, option); + } + }); + + if (runtime?.channel) { + const currentOption = normalizeReleaseChannelOption( + { + channel: runtime.channel, + versions: runtime.versions || {}, + availability: runtime.availability || null, + }, + runtime + ); + const key = releaseChannelKey(currentOption.channel); + if (key && !optionsByKey.has(key)) { + optionsByKey.set(key, currentOption); + } + } + + return [...optionsByKey.values()]; +}; + +export const hasSelectableReleaseChannels = (runtime = {}) => { + const options = getReleaseChannelOptions(runtime); + const hasOnlyDefaultStable = + options.length === 1 && options[0].defaultChannel === true && options[0].channelSlug === "stable"; + return options.length > 1 && !hasOnlyDefaultStable; +}; + export const getReleaseChannelUnavailableStatus = (runtime = {}, now = Date.now(), ignoredUntil = 0) => { const channel = runtime?.channel || null; - const channelSlug = String(channel?.slug || "").trim(); - const channelName = String(channel?.name || channelSlug || "Release channel").trim(); + const channelSlug = normalizeReleaseChannelSlug(channel?.slug || ""); + const channelName = channelDisplayName(channel); const isDefaultChannel = channel?.default_channel === true || channel?.default_channel === 1 || channelSlug === "stable"; - const frontendBaseUrl = runtimeUrl(runtime, "frontend_base_url"); - const apiBaseUrl = runtimeUrl(runtime, "api_base_url"); - const explicitAvailability = - runtime?.availability?.explicit === false - ? false - : Boolean(runtime?.availability && typeof runtime.availability === "object") || hasExplicitRuntimeTargets(runtime); - const availability = runtimeAvailability(runtime, frontendBaseUrl, apiBaseUrl); + const explicitAvailability = hasExplicitReleaseRuntime(runtime); + const availability = runtimeAvailability(runtime, channel); const ignored = Number(ignoredUntil || 0) > now; const unavailable = Boolean(channelSlug) && !isDefaultChannel && availability.configured === false; @@ -172,8 +273,6 @@ export const getReleaseChannelUnavailableStatus = (runtime = {}, now = Date.now( defaultChannel: isDefaultChannel, explicitAvailability, description: channel?.description || "", - frontendBaseUrl, - apiBaseUrl, missing: availability.missing, configured: availability.configured, unavailable, @@ -191,6 +290,50 @@ export const releaseChannelUnavailableStatus = computed(() => { return getReleaseChannelUnavailableStatus(releaseRuntimeState, state.now, ignoredUntil); }); +export const releaseChannelOptions = computed(() => getReleaseChannelOptions(releaseRuntimeState)); + +export const releaseChannelSelectorVisible = computed(() => hasSelectableReleaseChannels(releaseRuntimeState)); + +export const selectedReleaseChannelSlug = computed(() => state.selectedChannelSlug); + +export const getSelectedReleaseChannelSlug = () => state.selectedChannelSlug || readSelectedChannelSlug(); + +export const releaseChannelRuntimeRequestParams = () => { + const slug = getSelectedReleaseChannelSlug(); + return slug ? { release_channel: slug } : {}; +}; + +export const selectReleaseChannel = (channelOrSlug) => { + const slug = normalizeReleaseChannelSlug( + typeof channelOrSlug === "string" ? channelOrSlug : channelOrSlug?.slug || channelOrSlug?.channelSlug || "" + ); + state.selectedChannelSlug = slug; + writeSelectedChannelSlug(slug); + return slug; +}; + +export const clearSelectedReleaseChannel = () => { + state.selectedChannelSlug = ""; + writeSelectedChannelSlug(""); +}; + +export const reconcileSelectedReleaseChannel = (runtime = releaseRuntimeState) => { + const hasExplicitOptions = + Array.isArray(runtime?.availableChannels) || Array.isArray(runtime?.available_channels); + const selected = getSelectedReleaseChannelSlug(); + if (!hasExplicitOptions || !selected) { + return selected; + } + + const hasSelectedOption = getReleaseChannelOptions(runtime).some((option) => option.channelSlug === selected); + if (!hasSelectedOption) { + clearSelectedReleaseChannel(); + return ""; + } + + return selected; +}; + export const releaseChannelSwitchNoticeKey = (channel, principalKey = state.switchNoticePrincipalKey) => { const channelKey = releaseChannelKey(channel); const principal = String(principalKey || "").trim(); @@ -304,47 +447,20 @@ export const stopReleaseChannelAvailabilityClock = () => { clockTimer = null; }; -export const buildReleaseFrontendRedirectUrl = (runtime = releaseRuntimeState, currentLocation = window.location) => { - const frontendBaseUrl = runtimeUrl(runtime, "frontend_base_url"); - if (!frontendBaseUrl || !currentLocation) { - return null; - } +export const buildReleaseFrontendRedirectUrl = () => null; - const target = new URL(frontendBaseUrl); - const currentOrigin = currentLocation.origin || `${currentLocation.protocol}//${currentLocation.host}`; - if (target.origin === currentOrigin) { - return null; - } - - target.pathname = currentLocation.pathname || "/"; - target.search = currentLocation.search || ""; - target.hash = currentLocation.hash || ""; - return target.toString(); -}; - -export const redirectToConfiguredReleaseFrontend = (runtime = releaseRuntimeState) => { - const status = getReleaseChannelUnavailableStatus(runtime, Date.now(), 0); - if (!status.configured) { - return false; - } - - const redirectUrl = buildReleaseFrontendRedirectUrl(runtime); - if (!redirectUrl) { - return false; - } - - window.location.assign(redirectUrl); - return true; -}; +export const redirectToConfiguredReleaseFrontend = () => false; export const __resetReleaseChannelAvailabilityForTests = () => { state.now = Date.now(); state.ignoredUntilByChannel = {}; state.switchNoticeSeenByKey = {}; state.switchNoticePrincipalKey = ""; + state.selectedChannelSlug = ""; stopReleaseChannelAvailabilityClock(); if (typeof window !== "undefined") { window.localStorage.removeItem(RELEASE_CHANNEL_IGNORE_STORAGE_KEY); window.localStorage.removeItem(RELEASE_CHANNEL_SWITCH_NOTICE_STORAGE_KEY); + window.localStorage.removeItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY); } }; diff --git a/src/services/releaseTimeline.js b/src/services/releaseTimeline.js index 26cf6166..c9d1731a 100644 --- a/src/services/releaseTimeline.js +++ b/src/services/releaseTimeline.js @@ -39,9 +39,11 @@ const readTraceId = () => { const releaseRuntimeStateMutable = reactive({ traceId: readTraceId(), channel: null, + availableChannels: [], versions: { frontend: null, api: null, + bundle_id: null, }, frontendBaseUrl: null, apiBaseUrl: null, @@ -69,16 +71,9 @@ export const releaseRuntimeState = readonly(releaseRuntimeStateMutable); const hasOwn = (value, key) => Boolean(value && Object.prototype.hasOwnProperty.call(value, key)); -const hasExplicitRuntimeTargets = (runtime) => { - return ( - hasOwn(runtime, "frontend_base_url") || - hasOwn(runtime, "frontendBaseUrl") || - hasOwn(runtime, "api_base_url") || - hasOwn(runtime, "apiBaseUrl") || - hasOwn(runtime?.channel, "frontend_base_url") || - hasOwn(runtime?.channel, "api_base_url") - ); -}; +const hasExplicitReleaseRuntime = (runtime) => + Boolean(runtime?.availability && typeof runtime.availability === "object" && runtime.availability.explicit !== false) || + hasOwn(runtime, "versions"); export const configureReleaseRuntime = (runtime = {}) => { if (!runtime || typeof runtime !== "object") { @@ -95,32 +90,43 @@ export const configureReleaseRuntime = (runtime = {}) => { } releaseRuntimeStateMutable.channel = runtime.channel || null; + releaseRuntimeStateMutable.availableChannels = Array.isArray(runtime.available_channels) + ? runtime.available_channels + : Array.isArray(runtime.availableChannels) + ? runtime.availableChannels + : []; releaseRuntimeStateMutable.versions = { frontend: runtime?.versions?.frontend || null, api: runtime?.versions?.api || null, + bundle_id: runtime?.versions?.bundle_id || null, }; - const frontendBaseUrl = - runtime.frontend_base_url || runtime.frontendBaseUrl || runtime?.channel?.frontend_base_url || null; - const apiBaseUrl = runtime.api_base_url || runtime.apiBaseUrl || runtime?.channel?.api_base_url || null; - const missingRuntimeTargets = []; - if (!frontendBaseUrl) { - missingRuntimeTargets.push("frontend_base_url"); + releaseRuntimeStateMutable.frontendBaseUrl = null; + releaseRuntimeStateMutable.apiBaseUrl = null; + const channel = runtime.channel || null; + const isDefaultChannel = + channel?.default_channel === true || channel?.default_channel === 1 || String(channel?.slug || "") === "stable"; + const missingReleaseContent = []; + if (!isDefaultChannel && hasOwn(runtime, "versions")) { + if (!runtime?.versions?.bundle_id) { + missingReleaseContent.push("release_bundle"); + } + if (!runtime?.versions?.frontend) { + missingReleaseContent.push("frontend_version"); + } + if (!runtime?.versions?.api) { + missingReleaseContent.push("api_version"); + } } - if (!apiBaseUrl) { - missingRuntimeTargets.push("api_base_url"); - } - releaseRuntimeStateMutable.frontendBaseUrl = frontendBaseUrl; - releaseRuntimeStateMutable.apiBaseUrl = apiBaseUrl; releaseRuntimeStateMutable.availability = runtime.availability ? { ...runtime.availability, explicit: true, } - : hasExplicitRuntimeTargets(runtime) + : hasExplicitReleaseRuntime(runtime) ? { - configured: missingRuntimeTargets.length === 0, - missing: missingRuntimeTargets, - status: missingRuntimeTargets.length === 0 ? "ready" : "unconfigured", + configured: missingReleaseContent.length === 0, + missing: missingReleaseContent, + status: missingReleaseContent.length === 0 ? "ready" : "unconfigured", explicit: true, } : { @@ -468,7 +474,8 @@ export const __resetReleaseTimelineForTests = () => { customTransport = null; releaseRuntimeStateMutable.traceId = "test-trace"; releaseRuntimeStateMutable.channel = null; - releaseRuntimeStateMutable.versions = { frontend: null, api: null }; + releaseRuntimeStateMutable.availableChannels = []; + releaseRuntimeStateMutable.versions = { frontend: null, api: null, bundle_id: null }; releaseRuntimeStateMutable.frontendBaseUrl = null; releaseRuntimeStateMutable.apiBaseUrl = null; releaseRuntimeStateMutable.availability = { diff --git a/src/services/superuserReleases.js b/src/services/superuserReleases.js index c02eaa51..2d88df8a 100644 --- a/src/services/superuserReleases.js +++ b/src/services/superuserReleases.js @@ -169,8 +169,14 @@ export const updateReleaseChannel = (id, payload) => export const rollbackReleaseChannel = (id) => requestReleaseManager(`/superuser/releases/channels/${id}/rollback`, "POST", {}); +export const setReleaseChannelBundle = (id, payload) => + requestReleaseManager(`/superuser/releases/channels/${id}/bundle`, "POST", payload); + export const listReleaseAssignments = () => requestReleaseManager("/superuser/releases/assignments", "GET", {}); +export const searchReleaseAssignmentSubjects = (params = {}) => + requestReleaseManager("/superuser/releases/assignment-subjects", "GET", params); + export const createReleaseAssignment = (payload) => requestReleaseManager("/superuser/releases/assignments", "POST", payload); diff --git a/src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue b/src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue index 1c8611d4..4eee8f56 100644 --- a/src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue +++ b/src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue @@ -1,5 +1,5 @@