diff --git a/scripts/translations/.translation_state.json b/scripts/translations/.translation_state.json index 2fe3c3f1c7..682dd98836 100644 --- a/scripts/translations/.translation_state.json +++ b/scripts/translations/.translation_state.json @@ -366,6 +366,75 @@ "yourPlan": "bdaeec33ca6d1e39", "your_complete_license_key": "a9909fbea80f494c" }, + "accessBoundary": { + "chip": { + "ariaLabel": "212f743cadb836d3", + "label": "cc11d415d9326ccc", + "popoverLabel": "e451d992ff88b824" + }, + "copied": "ea61bc15688d1e48", + "copyDetails": "ec7ee2826c849564", + "denied": { + "create": "e64bc3593a603832", + "delete": "c346b3c06e9303ed", + "edit": "6649052d1364e44e", + "missing": "effeaa19f70b0fbd", + "missingIntro": "475cc73919dd1e9d", + "more": "5f4f590acbda8ee3", + "parts": "8b82b8f1aed3dad5", + "showLess": "94ea9b1d33a02975", + "view": "27e9f709ef02acf1" + }, + "resources": { + "ai": "11fb682be0a0233d", + "alerts": "f62e9267d196c761", + "cipherKeys": "9d5e36b817bf496e", + "dashboards": "a53bcafb67d960df", + "destinations": "72eb63f032e47e34", + "downtimes": "5d9972eaed771890", + "fleetConfigs": "affc3f6b5f45623a", + "functions": "75e942e5b366227e", + "groups": "39bbb719fa2b9d22", + "llmModelPricing": "794f915f46e1cc64", + "organizations": "2730183d6b776b40", + "passcode": "df5f51e2e793f3c9", + "pipelines": "d1a8fffeb935847e", + "publicDashboards": "b4bee918655d4e11", + "regexPatterns": "36e205848147c947", + "reports": "dacca3cba3f346a4", + "roles": "c25337055464f8d3", + "savedviews": "81184cb517eda985", + "serviceAccounts": "1642d9e225e63659", + "streams": "bfd297afbf9e7430", + "templates": "56b564b75c7fdb48", + "users": "6b0cc904d081a8c1" + }, + "resourcesInline": { + "ai": "11fb682be0a0233d", + "alerts": "deb3e366c077c228", + "cipherKeys": "a4ec922695af251c", + "dashboards": "21b2e2095fd38f7d", + "destinations": "1af0500e4d2be663", + "downtimes": "573ef711d75684dd", + "fleetConfigs": "fa54fb3d9e25631c", + "functions": "ef7d009c7e812da1", + "groups": "4ed379d418bb8629", + "llmModelPricing": "794f915f46e1cc64", + "organizations": "f8a665a97769f1d2", + "passcode": "583375d251142f0d", + "pipelines": "8719e4b9771d340e", + "publicDashboards": "55ff9bd4571fa4cb", + "regexPatterns": "f2677e20964b4314", + "reports": "7f26104f77a7ce47", + "roles": "66cf5513b37462be", + "savedviews": "1b46920dbc530fb7", + "serviceAccounts": "7c40134810b0f1e1", + "streams": "d5a08b6893ecd1aa", + "templates": "9c6641c1f9cfb0c6", + "unnamed": "644ebf22501f57b5", + "users": "7dfb4cf67742cb06" + } + }, "aiAssistant": { "aiChat": { "actionCancelledByUser": "7cb211f36eebddd0", @@ -10046,7 +10115,6 @@ "linkHint": "f876fec1488cf888", "loading": "88585a23b84075fd", "nextRefreshIn": "437006d85932d526", - "noPublishPermission": "320ebf35c94192ca", "notAvailable": "1165ac7c8d43a270", "offline": "5bca083dbc9d298b", "panelFrom": "d6f75b131c1b2d1c", @@ -16391,7 +16459,6 @@ }, "detailTable": { "addIcon": "ac9dfbff07ca5a3f", - "contentCopiedSuccessfully": "f98423629a72ca51", "noDataAvailable": "b00811d81b2f20d8", "sendToAiChat": "ca126ff149d20388" }, @@ -20930,6 +20997,23 @@ "executionErrorDesc": "a416e218fd2e8118", "expandRange": "951a8f1dd3ac554c", "expandRangeDesc": "608a2013ea43b54e", + "explain": { + "functionArgs": "23ccb311e23591dc", + "missingStream": "6c9df1ac8e6fdb24", + "notANumber": "e59bdfd66bc6af20", + "notAStatement": "ca113cdf9328b827", + "notGrouped": "26442db638b19196", + "streamNotFound": "590b2fb0f51f4840", + "unclosedBracket": "86c4cde703e1f012", + "unclosedDelimiter": "663da4ab2c44fde6", + "unclosedQuote": "33d6fba874d7f9c1", + "unexpectedEnd": "a925691b4ea0ff51", + "unexpectedEndAfter": "ec8170badd42dbd1", + "unexpectedToken": "8846e9f8b283322a", + "unknownField": "4f28d8cf4a5530b2", + "unknownFunction": "13ff0e1450a91e6f", + "unknownFunctionSuggest": "0747842459e85e05" + }, "fieldNotFound": "89920d262b850d8a", "fieldNotFoundDesc": "d705d7b17e7efe50", "fixQuery": "4ab707638498c314", @@ -27344,10 +27428,6 @@ "themeResetToOrganization": "defdaeea3b3a1bf3" }, "composables": { - "accessRequiredMessage": "b888a2a1c7c72a74", - "accessRequiredTitle": "8aba5015a4464812", - "copied": "ea61bc15688d1e48", - "copyDetails": "ec7ee2826c849564", "couldNotUpdateFavoriteDashboards": "22c705c3f2b18d78", "goToJobScheduler": "ca7c7c8927946335", "invalidSqlSyntax": "ef02d4a9e9c35300", @@ -27356,23 +27436,6 @@ "noPermissionToChangeFavorites": "206bbb174d753838", "pleaseWaitWhileLoadingStreams": "b1f886d2ec113e3d", "refresh": "0e91610117029a62", - "resourceFallback": "eb7a842ff958c49c", - "resources": { - "alerts": "f62e9267d196c761", - "dashboards": "a53bcafb67d960df", - "destinations": "72eb63f032e47e34", - "functions": "75e942e5b366227e", - "llmModelPricing": "9067acc52aa483f2", - "organizations": "2730183d6b776b40", - "passcode": "1b3dbe134ffb942b", - "pipelines": "d1a8fffeb935847e", - "reports": "dacca3cba3f346a4", - "roles": "c25337055464f8d3", - "savedviews": "8b4b9a23dfb5e991", - "streams": "bfd297afbf9e7430", - "templates": "56b564b75c7fdb48", - "users": "6b0cc904d081a8c1" - }, "thereAreSomeErrorsPleaseFix": "e0fa7e73ce02634d" }, "correlation": { @@ -27714,6 +27777,10 @@ "error": "d98ee0e5f9399db9", "errorCount": "6b8c59fe228558f0", "errorSpans": "19c03e5b2f215135", + "errorStrip": { + "menuTitle": "2a3929b2c638caac", + "spanLabel": "a35aeda058f35dd0" + }, "errorTraces": "e9aefb224ed93ab1", "errorsLabel": "be4bd56772770471", "escapedLabel": "bc78f872d7b6c370", @@ -27900,6 +27967,22 @@ "p50Median": "76661f0c8ac6ce54", "previousMatch": "daa2f8c3fc897cc7", "redMetrics": "a9d1ab9a2d6d4dac", + "results": { + "errorColumn": "54a0e8c17ebb21a1", + "eventCounts": "7a0e9c1af1b3ba17", + "eventExceptions": "2ea75cfd9a3ec2ed", + "eventKindException": "2104e56ef446302c", + "eventKindLog": "836ff184e7b41b1e", + "eventLogs": "1f2a2a07ec3f5874", + "eventOffset": "b108bcbf6c8aa6cf", + "eventsColumn": "8d14f6e72de8f18a", + "eventsHoverOrder": "b8d161636fead509", + "eventsHoverTitle": "201a1b82683cb7e8", + "fieldValueCountsCaption": "c6ce1d3d4d2c5d21", + "moreEvents": "ecccea94c62457a7", + "spansColumnTooltip": "3d4bc223031676a9", + "tracesWithMatchingSpan": "628f865548a5bc81" + }, "reviewAndApplyFilters": "28a6fd8929f1ffa5", "rowNav": { "firstResult": "9ca403a8f5809d02", diff --git a/tests/ui-testing/ci-matrix/smoke_config.json b/tests/ui-testing/ci-matrix/smoke_config.json index a242959537..f9e969a986 100644 --- a/tests/ui-testing/ci-matrix/smoke_config.json +++ b/tests/ui-testing/ci-matrix/smoke_config.json @@ -145,7 +145,6 @@ "Logs": [ "web/src/plugins/logs/**", "web/src/components/logs/**", - "web/src/services/httpsearch.*", "web/src/services/saved_views.*", "web/src/services/patterns.*", "tests/ui-testing/pages/logsPages/**", diff --git a/web/src/components/Header.vue b/web/src/components/Header.vue index ae87c3cb27..4fb04d26fa 100644 --- a/web/src/components/Header.vue +++ b/web/src/components/Header.vue @@ -190,6 +190,7 @@ along with this program. If not, see . /> + @@ -513,6 +514,7 @@ import OrganizationSelector from "./OrganizationSelector.vue"; import LinkedAccountsDialog from "./settings/LinkedAccountsDialog.vue"; import OIcon from "@/lib/core/Icon/OIcon.vue"; import OButton from "@/lib/core/Button/OButton.vue"; +import OPageHeaderStatus from "@/lib/core/PageHeader/OPageHeaderStatus.vue"; import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue"; import { checkFeatureAccess } from "@/utils/enterpriseFeatures"; import { readSlackOrgConfig } from "@/utils/slackSettings"; @@ -528,6 +530,7 @@ import { chartColor } from "@/utils/chartTheme"; export default defineComponent({ name: "HeaderComponent", components: { + OPageHeaderStatus, ThemeSwitcher, EnterpriseUpgradeDialog, OrganizationSelector, diff --git a/web/src/components/access/OAccessActionPopover.vue b/web/src/components/access/OAccessActionPopover.vue new file mode 100644 index 0000000000..91cfbdd0d0 --- /dev/null +++ b/web/src/components/access/OAccessActionPopover.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/web/src/components/access/OAccessBoundary.vue b/web/src/components/access/OAccessBoundary.vue new file mode 100644 index 0000000000..e81a075d54 --- /dev/null +++ b/web/src/components/access/OAccessBoundary.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/web/src/components/access/OAccessDeniedCard.vue b/web/src/components/access/OAccessDeniedCard.vue new file mode 100644 index 0000000000..2f19e99d54 --- /dev/null +++ b/web/src/components/access/OAccessDeniedCard.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/web/src/components/access/OAccessHeaderChip.vue b/web/src/components/access/OAccessHeaderChip.vue new file mode 100644 index 0000000000..2cdbd7b502 --- /dev/null +++ b/web/src/components/access/OAccessHeaderChip.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/web/src/components/access/OAccessLock.vue b/web/src/components/access/OAccessLock.vue new file mode 100644 index 0000000000..498a4353d2 --- /dev/null +++ b/web/src/components/access/OAccessLock.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/web/src/components/access/useAccessCopy.ts b/web/src/components/access/useAccessCopy.ts new file mode 100644 index 0000000000..0cd835c54e --- /dev/null +++ b/web/src/components/access/useAccessCopy.ts @@ -0,0 +1,37 @@ +// Copyright 2026 OpenObserve Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { onBeforeUnmount, ref } from "vue"; +import { copyToClipboard } from "@/utils/clipboard"; +import { useI18nTyped } from "@/types/i18n"; + +const COPIED_RESET_MS = 2000; + +/** Copies a 403's details and flips `copied` for a moment. */ +export function useAccessCopy(details: () => string) { + const { t } = useI18nTyped(); + const copied = ref(false); + let copiedTimer: ReturnType | undefined; + + async function copyDetails(): Promise { + if (!(await copyToClipboard(details(), t, { silent: true }))) return; + copied.value = true; + clearTimeout(copiedTimer); + copiedTimer = setTimeout(() => (copied.value = false), COPIED_RESET_MS); + } + + onBeforeUnmount(() => clearTimeout(copiedTimer)); + return { copied, copyDetails }; +} diff --git a/web/src/components/alerts/AlertInsights.vue b/web/src/components/alerts/AlertInsights.vue index 3e37a05ef0..13e23cd2ac 100644 --- a/web/src/components/alerts/AlertInsights.vue +++ b/web/src/components/alerts/AlertInsights.vue @@ -429,6 +429,7 @@ const fetchAlerts = async () => { ); alertsList.value = res.data.list || []; } catch (error) { + if ((error as { response?: { status?: number } })?.response?.status === 403) return; toast({ variant: "error", message: t("toastMessages.alerts.failedToLoadAlerts"), diff --git a/web/src/components/alerts/AlertList.vue b/web/src/components/alerts/AlertList.vue index 9840d2017f..344c96e48b 100644 --- a/web/src/components/alerts/AlertList.vue +++ b/web/src/components/alerts/AlertList.vue @@ -2366,7 +2366,6 @@ export default defineComponent({ console.error(error); dismiss(); forbidden.value = error?.response?.status === 403; - // The grouped access toast already reports a 403; a second red toast adds nothing. if (!forbidden.value) { toast({ variant: "error", @@ -2792,12 +2791,13 @@ export default defineComponent({ .then((list: any) => { destinations.value = list as any; }) - .catch(() => + .catch((err: { response?: { status?: number } }) => { + if (err?.response?.status === 403) return; toast({ variant: "error", message: t("toastMessages.alerts.errorWhileFetchingDestinations"), - }), - ); + }); + }); }; const getTemplates = (force = false) => { @@ -2815,12 +2815,13 @@ export default defineComponent({ .then((list: any) => { templates.value = list; }) - .catch(() => + .catch((err: { response?: { status?: number } }) => { + if (err?.response?.status === 403) return; toast({ variant: "error", message: t("toastMessages.alerts.errorWhileFetchingTemplates"), - }), - ); + }); + }); }; // The form's refresh event carries no payload, so a bare `getTemplates` handler would never see `force`. const refreshTemplates = () => getTemplates(true); @@ -4071,7 +4072,7 @@ export default defineComponent({ error.response?.data?.message || error?.message || t("alerts.messages.bulkDeleteAlertsFailed"); - if (error.response?.status != 403 || error?.status != 403) { + if (error.response?.status !== 403 && error?.status !== 403) { toast({ variant: "error", message: errorMessage, diff --git a/web/src/components/alerts/AlertsDestinationList.vue b/web/src/components/alerts/AlertsDestinationList.vue index 769df1fc02..726caf70b2 100644 --- a/web/src/components/alerts/AlertsDestinationList.vue +++ b/web/src/components/alerts/AlertsDestinationList.vue @@ -1144,7 +1144,7 @@ export default defineComponent({ error.response?.data?.message || error?.message || t("alerts.messages.bulkDeleteDestinationsFailed"); - if (error.response?.status != 403 || error?.status != 403) { + if (error.response?.status !== 403 && error?.status !== 403) { toast({ variant: "error", message: errorMessage, diff --git a/web/src/components/alerts/ExternalAlertSourcesList.vue b/web/src/components/alerts/ExternalAlertSourcesList.vue index 26e54e9a04..6d46a6eb42 100644 --- a/web/src/components/alerts/ExternalAlertSourcesList.vue +++ b/web/src/components/alerts/ExternalAlertSourcesList.vue @@ -628,7 +628,6 @@ export default defineComponent({ queryClient.getQueryState(options.queryKey)?.dataUpdatedAt ?? Date.now(); } catch (e: any) { this.forbidden = e?.response?.status === 403; - // The grouped access toast already reports a 403; a second red toast adds nothing. if (!this.forbidden) { toast({ variant: "error", message: this.t("alert_sources.error") }); } diff --git a/web/src/components/alerts/IncidentList.vue b/web/src/components/alerts/IncidentList.vue index 16d9d719f7..e3afba1f74 100644 --- a/web/src/components/alerts/IncidentList.vue +++ b/web/src/components/alerts/IncidentList.vue @@ -822,7 +822,6 @@ export default defineComponent({ { immediate: true }, ); watch(incidentsList.error, (error: any) => { - // The grouped access toast already reports a 403; a second red toast adds nothing. if (!error || forbidden.value) return; toast({ variant: "error", message: t("alerts.incidents.errorLoading") }); console.error("Failed to load incidents:", error); diff --git a/web/src/components/alerts/PipelinesDestinationList.vue b/web/src/components/alerts/PipelinesDestinationList.vue index b8b4db0add..52fc2c60e2 100644 --- a/web/src/components/alerts/PipelinesDestinationList.vue +++ b/web/src/components/alerts/PipelinesDestinationList.vue @@ -730,7 +730,7 @@ export default defineComponent({ error.response?.data?.message || error?.message || t("alerts.messages.bulkDeleteDestinationsFailed"); - if (error.response?.status != 403 || error?.status != 403) { + if (error.response?.status !== 403 && error?.status !== 403) { toast({ variant: "error", message: errorMessage, diff --git a/web/src/components/alerts/TemplateList.vue b/web/src/components/alerts/TemplateList.vue index 2cf6db45a4..6901206594 100644 --- a/web/src/components/alerts/TemplateList.vue +++ b/web/src/components/alerts/TemplateList.vue @@ -908,7 +908,7 @@ const bulkDeleteTemplates = () => { err.response?.data?.message || err?.message || t("alerts.messages.bulkDeleteTemplatesFailed"); - if (err.response?.status != 403 || err?.status != 403) { + if (err.response?.status !== 403 && err?.status !== 403) { toast({ variant: "error", message: errorMessage, diff --git a/web/src/components/alerts/downtimes/DowntimeDetail.vue b/web/src/components/alerts/downtimes/DowntimeDetail.vue index 410615468c..ac41f32c2b 100644 --- a/web/src/components/alerts/downtimes/DowntimeDetail.vue +++ b/web/src/components/alerts/downtimes/DowntimeDetail.vue @@ -106,7 +106,11 @@ along with this program. If not, see .
- + { return e?.response?.status ?? e?.status; }); const forbidden = computed(() => errorStatus.value === 403); +const detailDenials = computed(() => [ + viewDenial("downtimes", "get", failedRequests(detailQuery.error.value)), +]); const notFound = computed(() => errorStatus.value === 404); const downtimeFolders = useQuery(() => diff --git a/web/src/components/dashboards/PublicLinksPanel.vue b/web/src/components/dashboards/PublicLinksPanel.vue index 77f4408f17..ef88c4c522 100644 --- a/web/src/components/dashboards/PublicLinksPanel.vue +++ b/web/src/components/dashboards/PublicLinksPanel.vue @@ -55,10 +55,9 @@
- !!linksQuery.error.value && !forbidden.value); +const linksDenials = computed(() => [ + viewDenial("public_dashboards", "get", failedRequests(linksQuery.error.value)), +]); const linkIds = computed(() => links.value.map((link) => link.id)); const statusQuery = useQuery(() => Object.assign(publicLinksStatusBatchQuery(orgId.value, linkIds.value), { diff --git a/web/src/components/dashboards/addPanel/DashboardQueryEditor.vue b/web/src/components/dashboards/addPanel/DashboardQueryEditor.vue index 9bdeedcddb..eea5ba44c2 100644 --- a/web/src/components/dashboards/addPanel/DashboardQueryEditor.vue +++ b/web/src/components/dashboards/addPanel/DashboardQueryEditor.vue @@ -501,6 +501,7 @@ export default defineComponent({ functionOptions.value = [...functionList.value]; return; } catch (e) { + if ((e as { response?: { status?: number } })?.response?.status === 403) return; showErrorNotification(t("dashboard.errorFetchingFunctions")); } }; diff --git a/web/src/components/functions/FunctionList.vue b/web/src/components/functions/FunctionList.vue index dcf7cf12d2..2837890bb6 100644 --- a/web/src/components/functions/FunctionList.vue +++ b/web/src/components/functions/FunctionList.vue @@ -938,7 +938,7 @@ export default defineComponent({ error.response?.data?.message || error?.message || t("functions.bulkDeleteFunctionsFailed"); - if (error.response?.status != 403 || error?.status != 403) { + if (error.response?.status !== 403 && error?.status !== 403) { toast({ variant: "error", message: errorMessage, diff --git a/web/src/components/iam/IngestionTokens.vue b/web/src/components/iam/IngestionTokens.vue index 6c4320805b..d8efc47079 100644 --- a/web/src/components/iam/IngestionTokens.vue +++ b/web/src/components/iam/IngestionTokens.vue @@ -458,7 +458,6 @@ export default defineComponent({ // The query owns its failure now, so this reports it once per error however // the read was triggered. watch(tokensList.error, (e: any) => { - // The grouped access toast already reports a 403; a second red toast adds nothing. if (!e || forbidden.value) return; toast({ variant: "error", diff --git a/web/src/components/iam/groups/AppGroups.vue b/web/src/components/iam/groups/AppGroups.vue index 2b3c50484f..d44422f962 100644 --- a/web/src/components/iam/groups/AppGroups.vue +++ b/web/src/components/iam/groups/AppGroups.vue @@ -642,7 +642,7 @@ const bulkDeleteUserGroups = async () => { selectedGroups.value = []; confirmBulkDelete.value = false; } catch (error: any) { - if (error.response?.status != 403 || error?.status != 403) { + if (error.response?.status !== 403 && error?.status !== 403) { toast({ message: error.response?.data?.message || error?.message || t("iam.appGroups.errorDeletingGroups"), diff --git a/web/src/components/iam/roles/AppRoles.vue b/web/src/components/iam/roles/AppRoles.vue index 1db0f68bd7..e12805e9a4 100644 --- a/web/src/components/iam/roles/AppRoles.vue +++ b/web/src/components/iam/roles/AppRoles.vue @@ -544,7 +544,7 @@ const bulkDeleteUserRoles = async () => { selectedRoleNames.value = []; confirmBulkDelete.value = false; } catch (error: any) { - if (error.response?.status != 403 || error?.status != 403) { + if (error.response?.status !== 403 && error?.status !== 403) { toast({ message: error.response?.data?.message || error?.message || t("iam.appRoles.bulkDeleteRolesError"), diff --git a/web/src/components/iam/serviceAccounts/ServiceAccountsList.vue b/web/src/components/iam/serviceAccounts/ServiceAccountsList.vue index f99262b7f7..2ac02767eb 100644 --- a/web/src/components/iam/serviceAccounts/ServiceAccountsList.vue +++ b/web/src/components/iam/serviceAccounts/ServiceAccountsList.vue @@ -1194,7 +1194,7 @@ export default defineComponent({ confirmBulkDelete.value = false; await getServiceAccountsUsers(true); } catch (err: any) { - if (err.response?.status != 403 || err?.status != 403) { + if (err.response?.status !== 403 && err?.status !== 403) { toast({ message: err?.response?.data?.message || diff --git a/web/src/components/iam/users/AddUser.vue b/web/src/components/iam/users/AddUser.vue index 8614d846a9..efa656a697 100644 --- a/web/src/components/iam/users/AddUser.vue +++ b/web/src/components/iam/users/AddUser.vue @@ -517,7 +517,7 @@ export default defineComponent({ // without a remount. existingUser.value = false; } else { - if (err.response?.status != 403 || err?.status != 403) { + if (err.response?.status !== 403 && err?.status !== 403) { toast({ variant: "error", message: raw(err.response?.data?.message) || t("toastMessages.iam.userSaveFailed"), diff --git a/web/src/components/iam/users/InvitationList.vue b/web/src/components/iam/users/InvitationList.vue index 41881b3e71..a9132f8ebc 100644 --- a/web/src/components/iam/users/InvitationList.vue +++ b/web/src/components/iam/users/InvitationList.vue @@ -333,7 +333,6 @@ export default defineComponent({ const e = error as { response?: { status?: number; data?: { message?: string } } }; dismiss(); forbidden.value = e.response?.status === 403; - // The grouped access toast already reports a 403; a second red toast adds nothing. if (!forbidden.value) { toast({ message: raw(e.response?.data?.message || t("iam.invitationList.failedLoadPending")), diff --git a/web/src/components/iam/users/User.vue b/web/src/components/iam/users/User.vue index 371593bbbc..5950744dad 100644 --- a/web/src/components/iam/users/User.vue +++ b/web/src/components/iam/users/User.vue @@ -1054,7 +1054,6 @@ export default defineComponent({ console.error("Failed to fetch org members:", err); dismiss(); forbidden.value = err?.response?.status === 403; - // The grouped access toast already reports a 403; a second red toast adds nothing. if (!forbidden.value) { toast({ variant: "error", @@ -1583,7 +1582,7 @@ export default defineComponent({ }); } } catch (err: any) { - if (err.response?.status != 403 || err?.status != 403) { + if (err.response?.status !== 403 && err?.status !== 403) { toast({ message: blocked ? t("iam.user.blockedUsersNotRemoved", { count: emails.length }) diff --git a/web/src/components/ingestion/FirstDataPanel.vue b/web/src/components/ingestion/FirstDataPanel.vue index 06731f1947..8d7942b581 100644 --- a/web/src/components/ingestion/FirstDataPanel.vue +++ b/web/src/components/ingestion/FirstDataPanel.vue @@ -22,7 +22,6 @@ import { useQuery } from "@tanstack/vue-query"; import OButton from "@/lib/core/Button/OButton.vue"; import OCard from "@/lib/core/Card/OCard.vue"; import OCodeBlock from "@/lib/core/Code/OCodeBlock.vue"; -import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue"; import OIcon from "@/lib/core/Icon/OIcon.vue"; import OBanner from "@/lib/feedback/Banner/OBanner.vue"; import OTag from "@/lib/core/Badge/OTag.vue"; @@ -44,11 +43,13 @@ import { useCredentialSnippet, } from "@/composables/useCredentialSnippet"; import { useOrgCredential } from "@/composables/useOrgCredential"; +import { failedRequests, viewDenial } from "@/composables/useAccessStore"; import type { FirstEventResult, StreamSignal } from "@/composables/firstEvent/useFirstEventWatch"; import { firstSourceOption, readFirstSource } from "@/components/login/firstSourceOptions"; import { getDataSourceCard, hasDataSourceCard } from "./setupCard/registry"; import type { RichCardCode, RichCardContent } from "./setupCard/types"; import FirstEventStatus from "./FirstEventStatus.vue"; +import OAccessLock from "@/components/access/OAccessLock.vue"; // how long the card outlives the store flip while the watcher confirms the stream that caused it const CONFIRM_GRACE_MS = 10_000; @@ -121,6 +122,17 @@ const isForbidden = (err: unknown) => { const forbidden = computed( () => flagOn.value && queriesFor().some(({ query }) => isForbidden(query.error.value)), ); +const streamDenials = computed(() => [ + viewDenial( + "streams", + "list", + failedRequests( + ...queriesFor() + .map(({ query }) => query.error.value) + .filter(isForbidden), + ), + ), +]); // the compact card waits for MainLayout's list, so an org with data never sees it flash; a failed list reads as empty like a failed probe const knownEmpty = computed(() => queriesFor().every(({ query }) => @@ -411,10 +423,10 @@ const StatusLine = defineComponent({