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 @@
+
+
+
+
+
+
+ {{ group }}
+
+
+
+
+
+
+
+
+ {{ slotProps.option.title }}
+ {{ slotProps.option.description }}
+
+ {{ valueLabel(slotProps.option.subject_type) }}
+
+
+
+ {{ tr("subject_empty") }}
+
+
+
+
+
+
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 @@