Replace vue-toast-notification with custom useAppToast across components and enhance Edge Gateway installer state management.
This commit is contained in:
+42949
File diff suppressed because it is too large
Load Diff
@@ -3,15 +3,15 @@ import {computed, onMounted, ref, watch} from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
import {BSwitch} from "buefy";
|
||||
import {useToast} from 'vue-toast-notification';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import { useAppToast } from "@/composables/useAppToast.js";
|
||||
import {
|
||||
getDepartmentSelfServeEnabled,
|
||||
setDepartmentSelfServeEnabled,
|
||||
} from "@/composables/departmentSelfServeEnabled.js";
|
||||
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
const toast = useAppToast();
|
||||
const { t } = useI18n();
|
||||
|
||||
const selfServeEnabled = ref(false);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { defineProps, computed, watch } from 'vue';
|
||||
import type { NavigationItemProps } from '@/components/models/navigation/NavigationItem.vue';
|
||||
import { toggleExpanded } from '../ViewportHeaderSettings.vue';
|
||||
import { SessionUser } from '@/components/session/token/SessionUser.vue';
|
||||
import { useAppToast } from "@/composables/useAppToast.js";
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object as () => NavigationItemProps,
|
||||
@@ -20,11 +21,10 @@ const props = defineProps({
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
import { useToast } from 'vue-toast-notification';
|
||||
import 'vue-toast-notification/dist/theme-sugar.css';
|
||||
|
||||
// In your setup:
|
||||
const toast = useToast();
|
||||
const toast = useAppToast();
|
||||
|
||||
const redirect = async (to: string) => {
|
||||
if (!to) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
createEdgeGatewayOperation,
|
||||
deleteEdgeGateway,
|
||||
getEdgeGateway,
|
||||
getEdgeGatewayInstallTokenStatus,
|
||||
listEdgeGatewayDepartments,
|
||||
listEdgeGateways,
|
||||
peekCachedEdgeGateway,
|
||||
@@ -79,6 +80,16 @@ const installerLabel = ref("");
|
||||
const installerCommand = ref("");
|
||||
const installerCopyState = ref("idle");
|
||||
const installerClaimPollToken = ref(0);
|
||||
const createInstallerStatus = () => ({
|
||||
visible: false,
|
||||
claimTokenId: null,
|
||||
state: "Idle",
|
||||
step: "Waiting to start",
|
||||
error: "",
|
||||
diagnostics: [],
|
||||
updatedAt: null,
|
||||
});
|
||||
const installerStatus = ref(createInstallerStatus());
|
||||
const flashMessage = ref("");
|
||||
const errorState = ref(null);
|
||||
const unavailableGatewayId = ref(null);
|
||||
@@ -147,6 +158,29 @@ const tone = (status) => ({ ONLINE: "success", DEGRADED: "warning", OFFLINE: "da
|
||||
const statusLabel = (status) => ({ ONLINE: "Online", DEGRADED: "Needs attention", OFFLINE: "Offline" }[status] || "Unknown");
|
||||
const discoveryLabel = (status) => ({ READY: "Ready", STALE: "Stale", PENDING: "Pending", FAILED: "Failed" }[status] || "Unknown");
|
||||
const transportLabel = (mode) => ({ gateway: "Gateway", cloud: "Cloud" }[mode] || mode || "Unknown");
|
||||
const installerStateLabel = (status) =>
|
||||
({
|
||||
PENDING: "Pending",
|
||||
RUNNING: "Running",
|
||||
CLAIMED: "Connected",
|
||||
FAILED: "Failed",
|
||||
}[String(status || "").toUpperCase()] || "Unknown");
|
||||
const installerStepLabel = (step) =>
|
||||
({
|
||||
PENDING: "Waiting to start",
|
||||
VERIFY_TOKEN: "Verifying token",
|
||||
INSTALL_PACKAGES: "Installing packages",
|
||||
DOWNLOAD_ARTIFACTS: "Downloading artifacts",
|
||||
WRITE_CONFIG: "Writing config",
|
||||
START_STACK: "Starting stack",
|
||||
WAIT_FOR_CLAIM: "Waiting for gateway claim",
|
||||
CLAIMED: "Gateway claimed",
|
||||
FAILED: "Installer failed",
|
||||
}[String(step || "").toUpperCase()] ||
|
||||
String(step || "Waiting to start")
|
||||
.toLowerCase()
|
||||
.replace(/_/g, " ")
|
||||
.replace(/^\w/, (letter) => letter.toUpperCase()));
|
||||
const deptName = (id) => departments.value.find((item) => Number(item.id) === Number(id))?.name || `Department ${id}`;
|
||||
const metricText = (value, suffix = "") => (Number.isFinite(Number(value)) ? `${Number(value)}${suffix}` : "No data");
|
||||
const normalizeFleetUsage = (value) => {
|
||||
@@ -406,6 +440,23 @@ const scheduleInstallerCopyReset = () => {
|
||||
}, 1800);
|
||||
};
|
||||
|
||||
const syncInstallerStatus = (session, overrides = {}) => {
|
||||
installerStatus.value = {
|
||||
...createInstallerStatus(),
|
||||
...installerStatus.value,
|
||||
visible: true,
|
||||
claimTokenId: session?.claim_token_id ?? installerStatus.value.claimTokenId ?? null,
|
||||
state: installerStateLabel(session?.status),
|
||||
step: installerStepLabel(session?.step),
|
||||
error:
|
||||
session?.last_error ||
|
||||
(String(session?.status || "").toUpperCase() === "FAILED" ? session?.message || "Installer failed." : ""),
|
||||
diagnostics: Array.isArray(session?.diagnostics) ? session.diagnostics : [],
|
||||
updatedAt: session?.updated_at || null,
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const copyPlainText = async (value) => {
|
||||
const text = String(value || "");
|
||||
if (!text.trim()) {
|
||||
@@ -747,46 +798,46 @@ const load = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const pollForClaimedGateway = async ({ existingIds, baseline, departmentId, label, pollToken }) => {
|
||||
const normalizedDepartmentId = Number(departmentId || 0);
|
||||
const normalizedLabel = normLabel(label);
|
||||
|
||||
const pollForClaimedGateway = async ({ claimTokenId, pollToken }) => {
|
||||
for (let attempt = 0; attempt < 60; attempt += 1) {
|
||||
if (installerClaimPollToken.value !== pollToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sleep(attempt === 0 ? 1500 : 2000);
|
||||
await refreshFleet();
|
||||
|
||||
const matchingGateways = gateways.value.filter((item) => {
|
||||
if (Number(item.department_id) !== normalizedDepartmentId) return false;
|
||||
if (normalizedLabel && normLabel(item.label) !== normalizedLabel) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const claimed =
|
||||
matchingGateways.find((item) => !existingIds.has(Number(item.id))) ||
|
||||
(() => {
|
||||
const reusedGateways = sortByMostRecentHeartbeat(
|
||||
matchingGateways.filter((item) => gatewayClaimStateChanged(item, baseline.get(String(item.id))))
|
||||
);
|
||||
if (reusedGateways.length === 1) {
|
||||
return reusedGateways[0];
|
||||
const sessionResponse = await getEdgeGatewayInstallTokenStatus(claimTokenId);
|
||||
const session = unwrap(sessionResponse, null);
|
||||
if (!session) {
|
||||
continue;
|
||||
}
|
||||
if (normalizedLabel && reusedGateways.length > 1) {
|
||||
return reusedGateways[0];
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
|
||||
if (claimed) {
|
||||
flashMessage.value = existingIds.has(Number(claimed.id)) ? "Gateway reconnected." : "Gateway connected.";
|
||||
syncInstallerStatus(session);
|
||||
|
||||
if (String(session.status || "").toUpperCase() === "FAILED") {
|
||||
fail(new Error(session.last_error || session.message || "Installer failed."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (String(session.status || "").toUpperCase() === "CLAIMED" && session.gateway_id) {
|
||||
flashMessage.value = session.message || "Gateway connected.";
|
||||
clearError();
|
||||
await selectGateway(claimed.id, "overview");
|
||||
await refreshFleet({ forceRefresh: true });
|
||||
await selectGateway(session.gateway_id, "overview");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
syncInstallerStatus(
|
||||
{
|
||||
claim_token_id: claimTokenId,
|
||||
status: "FAILED",
|
||||
step: "WAIT_FOR_CLAIM",
|
||||
last_error: "The gateway did not claim the installer token in time.",
|
||||
},
|
||||
{
|
||||
error: "The gateway did not claim the installer token in time.",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const generateInstaller = async () => {
|
||||
@@ -798,18 +849,44 @@ const generateInstaller = async () => {
|
||||
|
||||
loading.value.installer = true;
|
||||
installerCopyState.value = "idle";
|
||||
syncInstallerStatus(
|
||||
{
|
||||
claim_token_id: null,
|
||||
status: "RUNNING",
|
||||
step: "PENDING",
|
||||
last_error: null,
|
||||
diagnostics: [],
|
||||
},
|
||||
{
|
||||
step: "Waiting to start",
|
||||
error: "",
|
||||
diagnostics: [],
|
||||
}
|
||||
);
|
||||
try {
|
||||
const label = installerLabel.value.trim();
|
||||
const pollToken = installerClaimPollToken.value + 1;
|
||||
const existingIds = new Set(gateways.value.map((item) => Number(item.id)));
|
||||
const baseline = buildGatewayClaimBaseline(gateways.value);
|
||||
const response = await createEdgeGatewayInstallToken({ department_id: departmentId, label: label || undefined });
|
||||
const installSession = unwrap(response, null) || {};
|
||||
installerClaimPollToken.value = pollToken;
|
||||
installerCommand.value = unwrap(response, null)?.install_command || "";
|
||||
installerCommand.value = installSession.install_command || "";
|
||||
syncInstallerStatus(
|
||||
{
|
||||
claim_token_id: installSession.claim_token_id || null,
|
||||
status: "RUNNING",
|
||||
step: "PENDING",
|
||||
last_error: null,
|
||||
diagnostics: [],
|
||||
},
|
||||
{
|
||||
claimTokenId: installSession.claim_token_id || null,
|
||||
}
|
||||
);
|
||||
flashMessage.value = "Installer token generated.";
|
||||
clearError();
|
||||
pollForClaimedGateway({ existingIds, baseline, departmentId, label, pollToken }).catch(() => {});
|
||||
pollForClaimedGateway({ claimTokenId: installSession.claim_token_id, pollToken }).catch((error) => fail(error));
|
||||
} catch (error) {
|
||||
installerStatus.value = createInstallerStatus();
|
||||
fail(error);
|
||||
} finally {
|
||||
loading.value.installer = false;
|
||||
@@ -1218,6 +1295,30 @@ onUnmounted(() => {
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-if="!filteredGatewayViews.length" class="edge-state-card" data-testid="gateway-empty-state">
|
||||
No gateways are currently available for this fleet view.
|
||||
</div>
|
||||
|
||||
<article v-if="installerStatus.visible" class="edge-stat-card" data-testid="gateway-installer-status">
|
||||
<p class="edge-card-label">Installer status</p>
|
||||
<strong data-testid="gateway-installer-status-state">{{ installerStatus.state }}</strong>
|
||||
<p data-testid="gateway-installer-status-step">{{ installerStatus.step }}</p>
|
||||
<p v-if="installerStatus.error" data-testid="gateway-installer-status-error">{{ installerStatus.error }}</p>
|
||||
<details
|
||||
v-if="installerStatus.diagnostics.length"
|
||||
class="edge-installer-diagnostics"
|
||||
data-testid="gateway-installer-status-diagnostics"
|
||||
>
|
||||
<summary>Diagnostics</summary>
|
||||
<ul class="edge-operation-list">
|
||||
<li v-for="diagnostic in installerStatus.diagnostics" :key="`${diagnostic.name}-${diagnostic.output}`">
|
||||
<strong>{{ diagnostic.name }}</strong>
|
||||
<pre class="edge-json-block">{{ diagnostic.output }}</pre>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</article>
|
||||
|
||||
<div v-if="allowDestructive" class="edge-onboarding-card" data-testid="gateway-onboarding">
|
||||
<div class="edge-panel-head">
|
||||
<h3 class="title is-6">Install new gateway</h3>
|
||||
@@ -1272,6 +1373,15 @@ onUnmounted(() => {
|
||||
</p>
|
||||
</div>
|
||||
<div class="edge-action-group">
|
||||
<button
|
||||
v-if="props.departmentId && currentView !== 'overview'"
|
||||
type="button"
|
||||
class="button is-light"
|
||||
data-testid="gateway-open-full-page"
|
||||
@click="emit('open-gateway-page', selectedGatewayView.id)"
|
||||
>
|
||||
Open full page
|
||||
</button>
|
||||
<span class="tag is-light">{{ selectedGatewayView.discoveryStatusLabel }}</span>
|
||||
<span class="tag is-light">{{ selectedGatewayView.departmentTransportModeLabel }}</span>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import NotFoundFallBackPageWrapper from "@/components/page/wrappers/NotFoundFall
|
||||
import RequiresPermission from "@/components/displays/permissionbased/RequiresPermission.vue";
|
||||
import { Goals } from "@/components/session/token/SessionUser/Objects/Goals.vue";
|
||||
import { showGoalCreateModal, showGoalEditModal } from "./functions/showGoalFormModal";
|
||||
import { useToast } from 'vue-toast-notification';
|
||||
import { useAppToast } from "@/composables/useAppToast.js";
|
||||
import { getDepartmentName } from "@/components/pagination/departmentTabs.vue";
|
||||
import {BToast, BTooltip} from "buefy";
|
||||
import Swal from "sweetalert2";
|
||||
@@ -26,7 +26,7 @@ const departmentId = ref(SessionUser.functions.getDepartmentIdFromUrl());
|
||||
const goals = ref([]);
|
||||
const loading = ref(true);
|
||||
const activeTab = ref('active');
|
||||
const toast = useToast();
|
||||
const toast = useAppToast();
|
||||
const { t } = useI18n();
|
||||
|
||||
const timeframe = ref('all');
|
||||
|
||||
+2
-2
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
|
||||
import DepartmentDashboardPageWrapper from "@/views/dashboards/departmentDashboard/DepartmentDashboardPageWrapper.vue";
|
||||
import NotFoundFallBackPageWrapper from "@/components/page/wrappers/NotFoundFallBackPageWrapper.vue";
|
||||
@@ -10,10 +9,11 @@ import SelfServeTryModal from "@/components/displays/department/tables/SelfServe
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
import { useAppToast } from "@/composables/useAppToast.js";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
const toast = useAppToast();
|
||||
|
||||
const parseIntOrZero = (value) => {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, defineProps, defineEmits, defineExpose } from 'vue';
|
||||
import {useToast} from 'vue-toast-notification';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAppToast } from "@/composables/useAppToast.js";
|
||||
|
||||
const { t } = useI18n();
|
||||
//import 'vue-toast-notification/dist/theme-sugar.css';
|
||||
@@ -106,7 +106,7 @@ const toastDuration = ref(0);
|
||||
|
||||
let instance = null;
|
||||
|
||||
const $toast = useToast();
|
||||
const $toast = useAppToast();
|
||||
const createToast = () => {
|
||||
if (!(props.queued > 0 || props.succeeded > 0 || props.failed > 0 )) {
|
||||
return; // Do not create a toast if maxProgress is not greater than 0
|
||||
|
||||
@@ -7,10 +7,10 @@ import SuperUserDashboardNavigation from "@/views/dashboards/superUserDashboard/
|
||||
import PageTitle from "@/components/global/PageTitle.vue";
|
||||
import ModulesPagination from "@/components/displays/pagination/models/SuperUser/ModulesPagination.vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToast } from 'vue-toast-notification';
|
||||
import { useAppToast } from "@/composables/useAppToast.js";
|
||||
|
||||
const { t } = useI18n();
|
||||
const toast = useToast();
|
||||
const toast = useAppToast();
|
||||
const router = useRouter();
|
||||
const laneId = router.currentRoute.value.params.id;
|
||||
const lane = ref(null);
|
||||
|
||||
+28
-4
@@ -1,9 +1,10 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import { viteSingleFile } from 'vite-plugin-singlefile'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import VueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import { execSync } from 'node:child_process'
|
||||
|
||||
@@ -60,10 +61,28 @@ function patchBuefyCssMediaQuery() {
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
async function loadVitePwaPlugin(workspaceRoot) {
|
||||
const localPluginEntry = path.resolve(workspaceRoot, 'node_modules.external-link', 'vite-plugin-pwa', 'dist', 'index.js')
|
||||
|
||||
try {
|
||||
if (fs.existsSync(localPluginEntry)) {
|
||||
return await import(pathToFileURL(localPluginEntry).href)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load vite-plugin-pwa from node_modules.external-link:', error.message)
|
||||
}
|
||||
|
||||
console.warn('vite-plugin-pwa is unavailable; continuing without PWA support in this environment.')
|
||||
return null
|
||||
}
|
||||
|
||||
export default defineConfig(async ({ mode }) => {
|
||||
const isProd = mode === 'production'
|
||||
const isPlaywrightRuntime = process.env.PLAYWRIGHT === '1'
|
||||
const shouldDisableDepOptimizer = process.platform === 'win32'
|
||||
const workspaceRoot = process.cwd()
|
||||
const vitePwaModule = await loadVitePwaPlugin(workspaceRoot)
|
||||
const VitePWA = vitePwaModule?.VitePWA
|
||||
|
||||
// Set COMMIT_HASH env var for use in the app
|
||||
const version = process.env.npm_package_version || '0.0.0'
|
||||
@@ -106,7 +125,7 @@ export default defineConfig(({ mode }) => {
|
||||
VueJsx(),
|
||||
!isProd && !isPlaywrightRuntime && vueDevTools(),
|
||||
enableSingleFile && viteSingleFile(),
|
||||
VitePWA({
|
||||
VitePWA && VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
injectRegister: isProd ? 'auto' : false,
|
||||
strategies: 'generateSW',
|
||||
@@ -222,7 +241,12 @@ export default defineConfig(({ mode }) => {
|
||||
ignored: ['**/output/playwright/**']
|
||||
}
|
||||
},
|
||||
optimizeDeps: isPlaywrightRuntime
|
||||
optimizeDeps: shouldDisableDepOptimizer
|
||||
? {
|
||||
noDiscovery: true,
|
||||
include: []
|
||||
}
|
||||
: isPlaywrightRuntime
|
||||
? {
|
||||
entries: ['index.html']
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user