mirror of
https://github.com/jcreek/OpenNetworkDiagram.git
synced 2026-09-11 07:33:43 +00:00
feat(#14): Add zero-setup Docker data initialization
This commit is contained in:
@@ -57,6 +57,8 @@
|
||||
| null;
|
||||
|
||||
const autosaveDelayMs = 800;
|
||||
const staticDemoMode = import.meta.env.OND_STATIC_DEMO;
|
||||
const staticDemoReason = 'Static demo deployment; changes are not persisted.';
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let diagramStage: HTMLDivElement;
|
||||
@@ -299,13 +301,19 @@
|
||||
let subnetEditorOpen = false;
|
||||
let showRackManager = false;
|
||||
|
||||
// First-run card: shown for an empty dataset or the untouched example
|
||||
// file, until dismissed (persisted) or the user makes their first edit.
|
||||
// Writable empty datasets use a session-only dismissal so onboarding
|
||||
// returns after reload until the user adds a real entity. Sample-data
|
||||
// dismissal remains persisted for returning demo users.
|
||||
const firstRunDismissKey = 'ond-firstrun-dismissed';
|
||||
let firstRunDismissed =
|
||||
typeof localStorage !== 'undefined' && localStorage.getItem(firstRunDismissKey) === '1';
|
||||
let emptyFirstRunDismissedThisSession = false;
|
||||
|
||||
function dismissFirstRun() {
|
||||
if (writable && totalEntities === 0) {
|
||||
emptyFirstRunDismissedThisSession = true;
|
||||
return;
|
||||
}
|
||||
firstRunDismissed = true;
|
||||
try {
|
||||
localStorage.setItem(firstRunDismissKey, '1');
|
||||
@@ -329,9 +337,9 @@
|
||||
].every((name) => name.trim().toLowerCase().startsWith('example'));
|
||||
$: showFirstRun =
|
||||
hasLoadedInitialData &&
|
||||
!firstRunDismissed &&
|
||||
!isLoadingData &&
|
||||
(totalEntities === 0 || (totalEntities <= 4 && allExampleNames));
|
||||
((writable && totalEntities === 0 && !emptyFirstRunDismissedThisSession) ||
|
||||
(!firstRunDismissed && totalEntities <= 4 && allExampleNames));
|
||||
let copiedIp: string | null = null;
|
||||
let copiedIpTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let pendingSubnetRemoval: number | null = null;
|
||||
@@ -695,6 +703,13 @@
|
||||
return `Read-only: ${reason}`;
|
||||
}
|
||||
|
||||
function resolveStaticFallbackNotice(reason: string | null): string {
|
||||
if (!reason) {
|
||||
return 'Read-only: API unavailable; using bundled static data.';
|
||||
}
|
||||
return `Read-only: ${reason} Using bundled static data.`;
|
||||
}
|
||||
|
||||
// Nodes are invisible hit targets (interaction, layout, edge anchoring);
|
||||
// the visible cards live in NodeCardLayer. Only edges are canvas-drawn,
|
||||
// styled from the same CSS tokens as the chrome.
|
||||
@@ -1257,6 +1272,7 @@
|
||||
return;
|
||||
}
|
||||
firstRunDismissed = true;
|
||||
emptyFirstRunDismissedThisSession = true;
|
||||
saveError = null;
|
||||
saveState = 'unsaved';
|
||||
if (!writable) {
|
||||
@@ -1898,46 +1914,56 @@
|
||||
isLoadingData = true;
|
||||
loadError = null;
|
||||
saveError = null;
|
||||
let apiFallbackReason: string | null = staticDemoMode ? staticDemoReason : null;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/network-data', {
|
||||
headers: { Accept: 'application/json' }
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`API responded with ${response.status}`);
|
||||
}
|
||||
const body = (await response.json()) as {
|
||||
data: NetworkData;
|
||||
writable: boolean;
|
||||
writableReason?: string | null;
|
||||
source: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
const validation = validateNetworkData(body.data);
|
||||
if (!validation.valid || !validation.data) {
|
||||
throw new Error('API returned invalid network data');
|
||||
}
|
||||
if (!staticDemoMode) {
|
||||
try {
|
||||
const response = await fetch('/api/network-data', {
|
||||
headers: { Accept: 'application/json' }
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => ({}))) as {
|
||||
error?: string;
|
||||
writableReason?: string | null;
|
||||
};
|
||||
apiFallbackReason =
|
||||
body.writableReason ?? body.error ?? `API responded with ${response.status}`;
|
||||
throw new Error(apiFallbackReason);
|
||||
}
|
||||
const body = (await response.json()) as {
|
||||
data: NetworkData;
|
||||
writable: boolean;
|
||||
writableReason?: string | null;
|
||||
source: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
const validation = validateNetworkData(body.data);
|
||||
if (!validation.valid || !validation.data) {
|
||||
throw new Error('API returned invalid network data');
|
||||
}
|
||||
|
||||
networkData = cloneNetworkData(validation.data);
|
||||
ensureSelectedTargetValid();
|
||||
writable = body.writable;
|
||||
readOnlyNotice = body.writable
|
||||
? defaultReadOnlyNotice
|
||||
: resolveReadOnlyNotice(body.writableReason);
|
||||
dataSourceLabel = body.source;
|
||||
lastSavedSnapshot = JSON.stringify(networkData);
|
||||
saveState = 'saved';
|
||||
connectionDraftByKey = {};
|
||||
revalidateDraft();
|
||||
refreshGraph();
|
||||
hasLoadedInitialData = true;
|
||||
isLoadingData = false;
|
||||
return;
|
||||
} catch (apiError) {
|
||||
console.warn(
|
||||
'[OpenNetworkDiagram] API load unavailable, falling back to static JSON',
|
||||
apiError
|
||||
);
|
||||
networkData = cloneNetworkData(validation.data);
|
||||
ensureSelectedTargetValid();
|
||||
writable = body.writable;
|
||||
readOnlyNotice = body.writable
|
||||
? defaultReadOnlyNotice
|
||||
: resolveReadOnlyNotice(body.writableReason);
|
||||
dataSourceLabel = body.source;
|
||||
lastSavedSnapshot = JSON.stringify(networkData);
|
||||
saveState = 'saved';
|
||||
connectionDraftByKey = {};
|
||||
revalidateDraft();
|
||||
refreshGraph();
|
||||
hasLoadedInitialData = true;
|
||||
isLoadingData = false;
|
||||
return;
|
||||
} catch (apiError) {
|
||||
apiFallbackReason ??= resolveErrorMessage(apiError);
|
||||
console.warn(
|
||||
'[OpenNetworkDiagram] API load unavailable, falling back to static JSON',
|
||||
apiError
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1945,7 +1971,7 @@
|
||||
networkData = cloneNetworkData(fallbackData);
|
||||
ensureSelectedTargetValid();
|
||||
writable = false;
|
||||
readOnlyNotice = 'Read-only: API unavailable; using bundled static data.';
|
||||
readOnlyNotice = resolveStaticFallbackNotice(apiFallbackReason);
|
||||
dataSourceLabel = jsonPath;
|
||||
lastSavedSnapshot = JSON.stringify(networkData);
|
||||
saveState = 'saved';
|
||||
@@ -2501,6 +2527,7 @@
|
||||
isSampleData={allExampleNames}
|
||||
{writable}
|
||||
hasMachines={networkData.machines.length > 0}
|
||||
storageReady={writable && totalEntities === 0}
|
||||
on:addmachine={addMachine}
|
||||
on:connectport={openFirstMachineEditor}
|
||||
on:openipam={() => (showIpamPanel = true)}
|
||||
|
||||
Reference in New Issue
Block a user