@trops/dash-core 0.1.132 → 0.1.134
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/electron/index.js +1338 -1152
- package/dist/electron/index.js.map +1 -1
- package/dist/index.esm.js +30 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/electron/index.js
CHANGED
|
@@ -1289,7 +1289,7 @@ const { getFileContents: getFileContents$6 } = file;
|
|
|
1289
1289
|
const configFilename$4 = "themes.json";
|
|
1290
1290
|
const appName$6 = "Dashboard";
|
|
1291
1291
|
|
|
1292
|
-
const themeController$
|
|
1292
|
+
const themeController$3 = {
|
|
1293
1293
|
/**
|
|
1294
1294
|
* saveTheme
|
|
1295
1295
|
* Create a new Theme that can be used in the application
|
|
@@ -1420,7 +1420,7 @@ const themeController$2 = {
|
|
|
1420
1420
|
},
|
|
1421
1421
|
};
|
|
1422
1422
|
|
|
1423
|
-
var themeController_1 = themeController$
|
|
1423
|
+
var themeController_1 = themeController$3;
|
|
1424
1424
|
|
|
1425
1425
|
/**
|
|
1426
1426
|
* Utils/tranaform
|
|
@@ -7991,6 +7991,25 @@ var properties = {
|
|
|
7991
7991
|
description: "Originating app package name (from package.json). Informational — indicates which app this dashboard was built for.",
|
|
7992
7992
|
maxLength: 200
|
|
7993
7993
|
},
|
|
7994
|
+
theme: {
|
|
7995
|
+
type: "object",
|
|
7996
|
+
description: "Optional bundled theme for the dashboard",
|
|
7997
|
+
properties: {
|
|
7998
|
+
key: {
|
|
7999
|
+
type: "string",
|
|
8000
|
+
description: "Theme key in the app"
|
|
8001
|
+
},
|
|
8002
|
+
data: {
|
|
8003
|
+
type: "object",
|
|
8004
|
+
description: "Full raw theme source object"
|
|
8005
|
+
},
|
|
8006
|
+
registryPackage: {
|
|
8007
|
+
type: "string",
|
|
8008
|
+
description: "Scoped registry package name (e.g. 'username/ocean-depth')"
|
|
8009
|
+
}
|
|
8010
|
+
},
|
|
8011
|
+
additionalProperties: false
|
|
8012
|
+
},
|
|
7994
8013
|
eventWiring: {
|
|
7995
8014
|
type: "array",
|
|
7996
8015
|
description: "Pre-configured event connections between widgets",
|
|
@@ -10421,7 +10440,7 @@ function getAuthStatus$1() {
|
|
|
10421
10440
|
*
|
|
10422
10441
|
* @returns {Promise<Object|null>} User profile or null
|
|
10423
10442
|
*/
|
|
10424
|
-
async function getRegistryProfile$
|
|
10443
|
+
async function getRegistryProfile$2() {
|
|
10425
10444
|
const stored = getStoredToken$1();
|
|
10426
10445
|
if (!stored) return null;
|
|
10427
10446
|
|
|
@@ -10596,7 +10615,7 @@ var registryAuthController$1 = {
|
|
|
10596
10615
|
pollForToken: pollForToken$1,
|
|
10597
10616
|
getStoredToken: getStoredToken$1,
|
|
10598
10617
|
getAuthStatus: getAuthStatus$1,
|
|
10599
|
-
getRegistryProfile: getRegistryProfile$
|
|
10618
|
+
getRegistryProfile: getRegistryProfile$2,
|
|
10600
10619
|
updateRegistryProfile: updateRegistryProfile$1,
|
|
10601
10620
|
getRegistryPackages: getRegistryPackages$1,
|
|
10602
10621
|
updateRegistryPackage: updateRegistryPackage$1,
|
|
@@ -10702,597 +10721,726 @@ var registryApiController$2 = {
|
|
|
10702
10721
|
};
|
|
10703
10722
|
|
|
10704
10723
|
/**
|
|
10705
|
-
*
|
|
10706
|
-
*
|
|
10707
|
-
* Handles export and import of dashboard configuration files.
|
|
10708
|
-
* Runs in the Electron main process.
|
|
10709
|
-
*
|
|
10710
|
-
* Export: serializes a workspace into a .dashboard.json config,
|
|
10711
|
-
* resolving widget dependencies, extracting event wiring from
|
|
10712
|
-
* layout listeners, and aggregating provider requirements.
|
|
10724
|
+
* themeRegistryController.js
|
|
10713
10725
|
*
|
|
10714
|
-
*
|
|
10715
|
-
*
|
|
10716
|
-
*
|
|
10726
|
+
* Handles publishing themes to and installing themes from the Dash registry.
|
|
10727
|
+
* Mirrors dashboardConfigController patterns for ZIP creation, manifest generation,
|
|
10728
|
+
* and registry interaction.
|
|
10717
10729
|
*/
|
|
10718
|
-
|
|
10719
|
-
const { app: app$2, dialog: dialog$1 } = require$$0$1;
|
|
10720
10730
|
const path$2 = require$$1$1;
|
|
10731
|
+
const { app: app$2, dialog: dialog$1 } = require$$0$1;
|
|
10721
10732
|
const AdmZip$1 = require$$3$3;
|
|
10722
|
-
|
|
10723
|
-
const
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
CURRENT_SCHEMA_VERSION,
|
|
10727
|
-
} = dashboardConfigValidator$1;
|
|
10733
|
+
|
|
10734
|
+
const themeController$2 = themeController_1;
|
|
10735
|
+
const registryController$1 = registryController$2;
|
|
10736
|
+
const registryApiController$1 = registryApiController$2;
|
|
10728
10737
|
const {
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
|
|
10732
|
-
buildProviderRequirements,
|
|
10733
|
-
applyEventWiringToLayout,
|
|
10734
|
-
} = dashboardConfigUtils$1;
|
|
10735
|
-
const { searchRegistry, getPackage } = registryController$2;
|
|
10738
|
+
getAuthStatus,
|
|
10739
|
+
getRegistryProfile: getRegistryProfile$1,
|
|
10740
|
+
} = registryAuthController$1;
|
|
10736
10741
|
|
|
10737
|
-
|
|
10738
|
-
|
|
10742
|
+
/**
|
|
10743
|
+
* Sanitize a name for use as a filename (lowercase, hyphens only).
|
|
10744
|
+
*/
|
|
10745
|
+
function sanitizeName(name) {
|
|
10746
|
+
return (name || "theme")
|
|
10747
|
+
.toLowerCase()
|
|
10748
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
10749
|
+
.replace(/^-|-$/g, "");
|
|
10750
|
+
}
|
|
10739
10751
|
|
|
10740
10752
|
/**
|
|
10741
|
-
*
|
|
10753
|
+
* Generate a registry manifest for a theme package.
|
|
10742
10754
|
*
|
|
10743
|
-
* @param {
|
|
10755
|
+
* @param {Object} themeData - The raw theme object
|
|
10756
|
+
* @param {string} themeKey - The theme key/name
|
|
10757
|
+
* @param {Object} options - Publish options { authorName, description, tags, scope }
|
|
10758
|
+
* @returns {Object} Registry manifest
|
|
10759
|
+
*/
|
|
10760
|
+
function generateThemeRegistryManifest(themeData, themeKey, options = {}) {
|
|
10761
|
+
const sanitizedName = sanitizeName(themeKey);
|
|
10762
|
+
const colors = extractColors(themeData);
|
|
10763
|
+
|
|
10764
|
+
return {
|
|
10765
|
+
scope: options.scope || "",
|
|
10766
|
+
name: sanitizedName,
|
|
10767
|
+
displayName: themeKey,
|
|
10768
|
+
author: options.authorName || "",
|
|
10769
|
+
description: options.description || "",
|
|
10770
|
+
version: "1.0.0",
|
|
10771
|
+
type: "theme",
|
|
10772
|
+
category: "general",
|
|
10773
|
+
tags: options.tags || [],
|
|
10774
|
+
icon: "palette",
|
|
10775
|
+
colors,
|
|
10776
|
+
appOrigin: options.appOrigin || "",
|
|
10777
|
+
publishedAt: new Date().toISOString(),
|
|
10778
|
+
};
|
|
10779
|
+
}
|
|
10780
|
+
|
|
10781
|
+
/**
|
|
10782
|
+
* Extract primary/secondary/tertiary/neutral colors from a theme object.
|
|
10783
|
+
* Theme objects store colors in various structures; this normalizes them.
|
|
10784
|
+
*/
|
|
10785
|
+
function extractColors(themeData) {
|
|
10786
|
+
const colors = {
|
|
10787
|
+
primary: "",
|
|
10788
|
+
secondary: "",
|
|
10789
|
+
tertiary: "",
|
|
10790
|
+
neutral: "",
|
|
10791
|
+
};
|
|
10792
|
+
|
|
10793
|
+
if (!themeData) return colors;
|
|
10794
|
+
|
|
10795
|
+
// Direct color fields
|
|
10796
|
+
if (themeData.primary) colors.primary = themeData.primary;
|
|
10797
|
+
if (themeData.secondary) colors.secondary = themeData.secondary;
|
|
10798
|
+
if (themeData.tertiary) colors.tertiary = themeData.tertiary;
|
|
10799
|
+
if (themeData.neutral) colors.neutral = themeData.neutral;
|
|
10800
|
+
|
|
10801
|
+
// Nested under "colors" key
|
|
10802
|
+
if (themeData.colors) {
|
|
10803
|
+
if (themeData.colors.primary) colors.primary = themeData.colors.primary;
|
|
10804
|
+
if (themeData.colors.secondary)
|
|
10805
|
+
colors.secondary = themeData.colors.secondary;
|
|
10806
|
+
if (themeData.colors.tertiary) colors.tertiary = themeData.colors.tertiary;
|
|
10807
|
+
if (themeData.colors.neutral) colors.neutral = themeData.colors.neutral;
|
|
10808
|
+
}
|
|
10809
|
+
|
|
10810
|
+
return colors;
|
|
10811
|
+
}
|
|
10812
|
+
|
|
10813
|
+
/**
|
|
10814
|
+
* Prepare a theme for publishing to the registry.
|
|
10815
|
+
*
|
|
10816
|
+
* Reads the theme from themes.json, generates a manifest, creates a ZIP,
|
|
10817
|
+
* and publishes via the registry API.
|
|
10818
|
+
*
|
|
10819
|
+
* @param {BrowserWindow} win - The sender window
|
|
10744
10820
|
* @param {string} appId - Application identifier
|
|
10745
|
-
* @param {
|
|
10746
|
-
* @param {Object} options -
|
|
10747
|
-
* @
|
|
10748
|
-
* @param {string} options.authorId - Dashboard author ID
|
|
10749
|
-
* @param {Object} widgetRegistry - WidgetRegistry instance (optional)
|
|
10750
|
-
* @returns {Promise<Object>} Result with success flag and file path
|
|
10821
|
+
* @param {string} themeKey - Key of the theme to publish
|
|
10822
|
+
* @param {Object} options - { authorName, description, tags }
|
|
10823
|
+
* @returns {Object} Result with success, manifest, registryResult
|
|
10751
10824
|
*/
|
|
10752
|
-
async function
|
|
10753
|
-
win,
|
|
10754
|
-
appId,
|
|
10755
|
-
workspaceId,
|
|
10756
|
-
options = {},
|
|
10757
|
-
widgetRegistry = null,
|
|
10758
|
-
) {
|
|
10825
|
+
async function prepareThemeForPublish$1(win, appId, themeKey, options = {}) {
|
|
10759
10826
|
try {
|
|
10760
|
-
//
|
|
10761
|
-
const
|
|
10762
|
-
|
|
10763
|
-
appName$1,
|
|
10764
|
-
appId,
|
|
10765
|
-
configFilename,
|
|
10766
|
-
);
|
|
10767
|
-
const workspacesArray = getFileContents$1(filename);
|
|
10768
|
-
const workspace = workspacesArray.find(
|
|
10769
|
-
(w) => w.id === workspaceId || w.id === Number(workspaceId),
|
|
10770
|
-
);
|
|
10771
|
-
|
|
10772
|
-
if (!workspace) {
|
|
10827
|
+
// Read the theme data
|
|
10828
|
+
const themesResult = themeController$2.listThemesForApplication(win, appId);
|
|
10829
|
+
if (themesResult.error) {
|
|
10773
10830
|
return {
|
|
10774
10831
|
success: false,
|
|
10775
|
-
error:
|
|
10832
|
+
error: "Failed to read themes: " + themesResult.message,
|
|
10776
10833
|
};
|
|
10777
10834
|
}
|
|
10778
10835
|
|
|
10779
|
-
const
|
|
10836
|
+
const themeData = themesResult.themes[themeKey];
|
|
10837
|
+
if (!themeData) {
|
|
10838
|
+
return { success: false, error: `Theme "${themeKey}" not found` };
|
|
10839
|
+
}
|
|
10780
10840
|
|
|
10781
|
-
//
|
|
10782
|
-
const
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
|
|
10841
|
+
// Get auth status and profile for scope
|
|
10842
|
+
const auth = getAuthStatus();
|
|
10843
|
+
if (!auth.authenticated) {
|
|
10844
|
+
return {
|
|
10845
|
+
success: false,
|
|
10846
|
+
error: "Not authenticated with registry",
|
|
10847
|
+
authRequired: true,
|
|
10848
|
+
};
|
|
10849
|
+
}
|
|
10850
|
+
const profile = await getRegistryProfile$1();
|
|
10851
|
+
const scope = profile?.username || options.scope || "";
|
|
10852
|
+
if (!scope) {
|
|
10853
|
+
return {
|
|
10854
|
+
success: false,
|
|
10855
|
+
error: "Could not determine registry username",
|
|
10856
|
+
authRequired: true,
|
|
10857
|
+
};
|
|
10858
|
+
}
|
|
10786
10859
|
|
|
10787
|
-
//
|
|
10788
|
-
const
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
...(options.authorName
|
|
10793
|
-
? { author: { name: options.authorName, id: options.authorId || "" } }
|
|
10794
|
-
: {}),
|
|
10795
|
-
shareable: true,
|
|
10796
|
-
tags: options.tags || [],
|
|
10797
|
-
icon: options.icon || "grip",
|
|
10798
|
-
workspace: {
|
|
10799
|
-
id: workspace.id,
|
|
10800
|
-
name: workspace.name,
|
|
10801
|
-
type: workspace.type || "workspace",
|
|
10802
|
-
label: workspace.label || workspace.name,
|
|
10803
|
-
version: workspace.version || 1,
|
|
10804
|
-
layout,
|
|
10805
|
-
menuId: workspace.menuId || 1,
|
|
10806
|
-
},
|
|
10807
|
-
widgets,
|
|
10808
|
-
providers,
|
|
10809
|
-
eventWiring,
|
|
10860
|
+
// Generate manifest
|
|
10861
|
+
const manifest = generateThemeRegistryManifest(themeData, themeKey, {
|
|
10862
|
+
...options,
|
|
10863
|
+
scope,
|
|
10864
|
+
appOrigin: appId,
|
|
10810
10865
|
});
|
|
10811
10866
|
|
|
10812
|
-
//
|
|
10813
|
-
|
|
10814
|
-
|
|
10867
|
+
// Validate colors
|
|
10868
|
+
if (
|
|
10869
|
+
!manifest.colors.primary ||
|
|
10870
|
+
!manifest.colors.secondary ||
|
|
10871
|
+
!manifest.colors.tertiary
|
|
10872
|
+
) {
|
|
10815
10873
|
return {
|
|
10816
10874
|
success: false,
|
|
10817
|
-
error:
|
|
10875
|
+
error:
|
|
10876
|
+
"Theme must have primary, secondary, and tertiary colors defined",
|
|
10818
10877
|
};
|
|
10819
10878
|
}
|
|
10820
10879
|
|
|
10821
|
-
//
|
|
10822
|
-
const sanitizedName = (
|
|
10823
|
-
|
|
10824
|
-
.replace(/\s+/g, "-")
|
|
10825
|
-
.toLowerCase();
|
|
10880
|
+
// Show save dialog
|
|
10881
|
+
const sanitizedName = sanitizeName(themeKey);
|
|
10882
|
+
const defaultFilename = `theme-${sanitizedName}-v${manifest.version}.zip`;
|
|
10826
10883
|
|
|
10827
|
-
const
|
|
10828
|
-
title: "
|
|
10829
|
-
defaultPath:
|
|
10830
|
-
|
|
10831
|
-
`dashboard-${sanitizedName}.zip`,
|
|
10832
|
-
),
|
|
10833
|
-
filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
|
|
10884
|
+
const saveResult = await dialog$1.showSaveDialog(win, {
|
|
10885
|
+
title: "Save Theme Package",
|
|
10886
|
+
defaultPath: defaultFilename,
|
|
10887
|
+
filters: [{ name: "ZIP Files", extensions: ["zip"] }],
|
|
10834
10888
|
});
|
|
10835
10889
|
|
|
10836
|
-
if (canceled || !filePath) {
|
|
10837
|
-
return { success: false,
|
|
10890
|
+
if (saveResult.canceled || !saveResult.filePath) {
|
|
10891
|
+
return { success: false, error: "Save canceled" };
|
|
10838
10892
|
}
|
|
10839
10893
|
|
|
10840
|
-
|
|
10894
|
+
const filePath = saveResult.filePath;
|
|
10895
|
+
|
|
10896
|
+
// Create ZIP with manifest.json + {name}.theme.json
|
|
10841
10897
|
const zip = new AdmZip$1();
|
|
10842
|
-
const configJson = JSON.stringify(dashboardConfig, null, 2);
|
|
10843
10898
|
zip.addFile(
|
|
10844
|
-
|
|
10845
|
-
Buffer.from(
|
|
10899
|
+
"manifest.json",
|
|
10900
|
+
Buffer.from(JSON.stringify(manifest, null, 2), "utf-8"),
|
|
10901
|
+
);
|
|
10902
|
+
zip.addFile(
|
|
10903
|
+
`${sanitizedName}.theme.json`,
|
|
10904
|
+
Buffer.from(JSON.stringify(themeData, null, 2), "utf-8"),
|
|
10846
10905
|
);
|
|
10847
|
-
|
|
10848
10906
|
zip.writeZip(filePath);
|
|
10849
10907
|
|
|
10850
|
-
console.log(
|
|
10851
|
-
|
|
10852
|
-
|
|
10908
|
+
console.log("[ThemeRegistryController] ZIP created at:", filePath);
|
|
10909
|
+
|
|
10910
|
+
// Attempt to publish to registry
|
|
10911
|
+
let registryResult = null;
|
|
10912
|
+
if (auth.authenticated) {
|
|
10913
|
+
registryResult = await registryApiController$1.publishToRegistry(
|
|
10914
|
+
filePath,
|
|
10915
|
+
manifest,
|
|
10916
|
+
);
|
|
10917
|
+
console.log(
|
|
10918
|
+
"[ThemeRegistryController] Registry publish result:",
|
|
10919
|
+
registryResult,
|
|
10920
|
+
);
|
|
10921
|
+
}
|
|
10853
10922
|
|
|
10854
10923
|
return {
|
|
10855
|
-
success: true,
|
|
10856
|
-
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
} catch (error) {
|
|
10860
|
-
console.error(
|
|
10861
|
-
"[DashboardConfigController] Error exporting dashboard:",
|
|
10862
|
-
error,
|
|
10863
|
-
);
|
|
10864
|
-
return {
|
|
10865
|
-
success: false,
|
|
10866
|
-
error: error.message,
|
|
10924
|
+
success: true,
|
|
10925
|
+
manifest,
|
|
10926
|
+
filePath,
|
|
10927
|
+
registryResult,
|
|
10867
10928
|
};
|
|
10929
|
+
} catch (err) {
|
|
10930
|
+
console.error(
|
|
10931
|
+
"[ThemeRegistryController] Error preparing theme for publish:",
|
|
10932
|
+
err,
|
|
10933
|
+
);
|
|
10934
|
+
return { success: false, error: err.message };
|
|
10868
10935
|
}
|
|
10869
10936
|
}
|
|
10870
10937
|
|
|
10871
10938
|
/**
|
|
10872
|
-
*
|
|
10873
|
-
* Opens the file picker, extracts and validates the .dashboard.json,
|
|
10874
|
-
* and returns a preview of the config + the file path for later import.
|
|
10939
|
+
* Install a theme from the registry.
|
|
10875
10940
|
*
|
|
10876
|
-
*
|
|
10877
|
-
*
|
|
10941
|
+
* Looks up the theme package, downloads the ZIP, extracts the .theme.json,
|
|
10942
|
+
* and saves it via themeController.
|
|
10943
|
+
*
|
|
10944
|
+
* @param {BrowserWindow} win - The sender window
|
|
10945
|
+
* @param {string} appId - Application identifier
|
|
10946
|
+
* @param {string} packageName - Registry package name (e.g., "username/ocean-depth")
|
|
10947
|
+
* @returns {Object} Result with success, themeKey, theme
|
|
10878
10948
|
*/
|
|
10879
|
-
async function
|
|
10949
|
+
async function installThemeFromRegistry$1(win, appId, packageName) {
|
|
10880
10950
|
try {
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
|
|
10951
|
+
// Look up the package
|
|
10952
|
+
const pkg = await registryController$1.getPackage(packageName);
|
|
10953
|
+
if (!pkg) {
|
|
10954
|
+
return {
|
|
10955
|
+
success: false,
|
|
10956
|
+
error: `Theme package "${packageName}" not found in registry`,
|
|
10957
|
+
};
|
|
10958
|
+
}
|
|
10886
10959
|
|
|
10887
|
-
|
|
10888
|
-
|
|
10960
|
+
// Resolve download URL
|
|
10961
|
+
let downloadUrl = pkg.downloadUrl;
|
|
10962
|
+
if (!downloadUrl) {
|
|
10963
|
+
return { success: false, error: "Package has no download URL" };
|
|
10889
10964
|
}
|
|
10890
10965
|
|
|
10891
|
-
|
|
10966
|
+
// Resolve template variables
|
|
10967
|
+
downloadUrl = downloadUrl
|
|
10968
|
+
.replace("{version}", pkg.version || "1.0.0")
|
|
10969
|
+
.replace("{name}", pkg.name || "");
|
|
10892
10970
|
|
|
10893
|
-
//
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
|
|
10971
|
+
// Enforce HTTPS
|
|
10972
|
+
if (!downloadUrl.startsWith("https://")) {
|
|
10973
|
+
return { success: false, error: "Download URL must use HTTPS" };
|
|
10974
|
+
}
|
|
10975
|
+
|
|
10976
|
+
console.log(
|
|
10977
|
+
"[ThemeRegistryController] Downloading theme from:",
|
|
10978
|
+
downloadUrl,
|
|
10979
|
+
);
|
|
10980
|
+
|
|
10981
|
+
// Download the ZIP
|
|
10982
|
+
const response = await fetch(downloadUrl);
|
|
10983
|
+
if (!response.ok) {
|
|
10984
|
+
return {
|
|
10985
|
+
success: false,
|
|
10986
|
+
error: `Failed to download theme: ${response.status} ${response.statusText}`,
|
|
10987
|
+
};
|
|
10988
|
+
}
|
|
10989
|
+
|
|
10990
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
10991
|
+
const zipBuffer = Buffer.from(arrayBuffer);
|
|
10898
10992
|
|
|
10993
|
+
// Extract .theme.json from ZIP
|
|
10994
|
+
const zip = new AdmZip$1(zipBuffer);
|
|
10899
10995
|
const entries = zip.getEntries();
|
|
10900
|
-
|
|
10901
|
-
|
|
10996
|
+
|
|
10997
|
+
const themeEntry = entries.find((entry) =>
|
|
10998
|
+
entry.entryName.endsWith(".theme.json"),
|
|
10902
10999
|
);
|
|
10903
11000
|
|
|
10904
|
-
if (!
|
|
11001
|
+
if (!themeEntry) {
|
|
10905
11002
|
return {
|
|
10906
11003
|
success: false,
|
|
10907
|
-
error: "
|
|
11004
|
+
error: "ZIP does not contain a .theme.json file",
|
|
10908
11005
|
};
|
|
10909
11006
|
}
|
|
10910
11007
|
|
|
10911
|
-
|
|
10912
|
-
|
|
11008
|
+
// Validate entry path (security: prevent path traversal)
|
|
11009
|
+
if (
|
|
11010
|
+
themeEntry.entryName.includes("..") ||
|
|
11011
|
+
path$2.isAbsolute(themeEntry.entryName)
|
|
11012
|
+
) {
|
|
11013
|
+
return { success: false, error: "Invalid file path in ZIP" };
|
|
11014
|
+
}
|
|
11015
|
+
|
|
11016
|
+
// Parse theme data
|
|
11017
|
+
const themeJson = themeEntry.getData().toString("utf-8");
|
|
11018
|
+
let themeData;
|
|
10913
11019
|
try {
|
|
10914
|
-
|
|
10915
|
-
} catch (
|
|
11020
|
+
themeData = JSON.parse(themeJson);
|
|
11021
|
+
} catch (parseErr) {
|
|
10916
11022
|
return {
|
|
10917
11023
|
success: false,
|
|
10918
|
-
error:
|
|
11024
|
+
error: "Invalid JSON in theme file: " + parseErr.message,
|
|
10919
11025
|
};
|
|
10920
11026
|
}
|
|
10921
11027
|
|
|
10922
|
-
|
|
10923
|
-
|
|
11028
|
+
// Add registry metadata
|
|
11029
|
+
themeData._registryMeta = {
|
|
11030
|
+
source: "registry",
|
|
11031
|
+
packageName,
|
|
11032
|
+
installedAt: new Date().toISOString(),
|
|
11033
|
+
};
|
|
11034
|
+
|
|
11035
|
+
// Determine theme key from package display name or name
|
|
11036
|
+
const themeKey = pkg.displayName || pkg.name;
|
|
11037
|
+
|
|
11038
|
+
// Save via themeController
|
|
11039
|
+
const saveResult = themeController$2.saveThemeForApplication(
|
|
11040
|
+
win,
|
|
11041
|
+
appId,
|
|
11042
|
+
themeKey,
|
|
11043
|
+
themeData,
|
|
11044
|
+
);
|
|
11045
|
+
|
|
11046
|
+
if (saveResult.error) {
|
|
10924
11047
|
return {
|
|
10925
11048
|
success: false,
|
|
10926
|
-
error:
|
|
11049
|
+
error: "Failed to save theme: " + saveResult.message,
|
|
10927
11050
|
};
|
|
10928
11051
|
}
|
|
10929
11052
|
|
|
10930
|
-
|
|
11053
|
+
console.log("[ThemeRegistryController] Theme installed:", themeKey);
|
|
10931
11054
|
|
|
10932
11055
|
return {
|
|
10933
11056
|
success: true,
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
10937
|
-
description: dashboardConfig.description,
|
|
10938
|
-
author: dashboardConfig.author,
|
|
10939
|
-
workspace: dashboardConfig.workspace,
|
|
10940
|
-
widgets: dashboardConfig.widgets || [],
|
|
10941
|
-
providers: dashboardConfig.providers || [],
|
|
10942
|
-
},
|
|
11057
|
+
themeKey,
|
|
11058
|
+
theme: themeData,
|
|
11059
|
+
themes: saveResult.themes,
|
|
10943
11060
|
};
|
|
10944
|
-
} catch (
|
|
10945
|
-
console.error(
|
|
10946
|
-
|
|
10947
|
-
error,
|
|
10948
|
-
);
|
|
10949
|
-
return { success: false, error: error.message };
|
|
11061
|
+
} catch (err) {
|
|
11062
|
+
console.error("[ThemeRegistryController] Error installing theme:", err);
|
|
11063
|
+
return { success: false, error: err.message };
|
|
10950
11064
|
}
|
|
10951
11065
|
}
|
|
10952
11066
|
|
|
10953
11067
|
/**
|
|
10954
|
-
*
|
|
10955
|
-
*
|
|
10956
|
-
* Steps:
|
|
10957
|
-
* 1. Show native file picker for .zip selection (or use options.filePath)
|
|
10958
|
-
* 2. Extract and validate .dashboard.json
|
|
10959
|
-
* 3. Auto-install missing widgets from registry
|
|
10960
|
-
* 4. Create workspace in workspaces.json
|
|
10961
|
-
* 5. Apply event wiring to layout
|
|
10962
|
-
* 6. Mark imported dashboard shareable: false
|
|
11068
|
+
* Get a preview of theme data for the publish modal.
|
|
10963
11069
|
*
|
|
10964
|
-
* @param {BrowserWindow} win - The main window (for dialog)
|
|
10965
11070
|
* @param {string} appId - Application identifier
|
|
10966
|
-
* @param {
|
|
10967
|
-
* @
|
|
10968
|
-
* @param {string} options.filePath - Skip file picker, use this path directly
|
|
10969
|
-
* @param {string} options.name - Override workspace name
|
|
10970
|
-
* @param {number} options.menuId - Override workspace menuId (folder)
|
|
10971
|
-
* @param {string} options.themeKey - Override workspace themeKey
|
|
10972
|
-
* @returns {Promise<Object>} Result with success, workspace, and import summary
|
|
11071
|
+
* @param {string} themeKey - Theme key
|
|
11072
|
+
* @returns {Object} Preview data with theme name, colors, etc.
|
|
10973
11073
|
*/
|
|
10974
|
-
|
|
10975
|
-
win,
|
|
10976
|
-
appId,
|
|
10977
|
-
widgetRegistry = null,
|
|
10978
|
-
options = {},
|
|
10979
|
-
) {
|
|
11074
|
+
function getThemePublishPreview$1(appId, themeKey) {
|
|
10980
11075
|
try {
|
|
10981
|
-
|
|
10982
|
-
|
|
10983
|
-
if (options.filePath) {
|
|
10984
|
-
// Use the provided file path (from selectDashboardFile)
|
|
10985
|
-
zipPath = options.filePath;
|
|
10986
|
-
} else {
|
|
10987
|
-
// Show file picker
|
|
10988
|
-
const { canceled, filePaths } = await dialog$1.showOpenDialog(win, {
|
|
10989
|
-
title: "Import Dashboard Configuration",
|
|
10990
|
-
filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
|
|
10991
|
-
properties: ["openFile"],
|
|
10992
|
-
});
|
|
10993
|
-
|
|
10994
|
-
if (canceled || !filePaths || !filePaths.length) {
|
|
10995
|
-
return { success: false, canceled: true };
|
|
10996
|
-
}
|
|
10997
|
-
|
|
10998
|
-
zipPath = filePaths[0];
|
|
10999
|
-
}
|
|
11000
|
-
|
|
11001
|
-
// 2. Extract and validate .dashboard.json from ZIP
|
|
11002
|
-
const zip = new AdmZip$1(zipPath);
|
|
11003
|
-
|
|
11004
|
-
// Validate ZIP entries for path traversal
|
|
11005
|
-
const tempDir = path$2.join(app$2.getPath("temp"), "dash-import");
|
|
11006
|
-
const { validateZipEntries } = widgetRegistryExports;
|
|
11007
|
-
validateZipEntries(zip, tempDir);
|
|
11008
|
-
|
|
11009
|
-
// Find the .dashboard.json file
|
|
11010
|
-
const entries = zip.getEntries();
|
|
11011
|
-
const configEntry = entries.find((e) =>
|
|
11012
|
-
e.entryName.endsWith(".dashboard.json"),
|
|
11013
|
-
);
|
|
11014
|
-
|
|
11015
|
-
if (!configEntry) {
|
|
11016
|
-
return {
|
|
11017
|
-
success: false,
|
|
11018
|
-
error: "No .dashboard.json file found in ZIP archive",
|
|
11019
|
-
};
|
|
11020
|
-
}
|
|
11021
|
-
|
|
11022
|
-
const configJson = configEntry.getData().toString("utf-8");
|
|
11023
|
-
let dashboardConfig;
|
|
11024
|
-
try {
|
|
11025
|
-
dashboardConfig = JSON.parse(configJson);
|
|
11026
|
-
} catch (parseError) {
|
|
11076
|
+
const themesResult = themeController$2.listThemesForApplication(null, appId);
|
|
11077
|
+
if (themesResult.error) {
|
|
11027
11078
|
return {
|
|
11028
11079
|
success: false,
|
|
11029
|
-
error:
|
|
11080
|
+
error: "Failed to read themes: " + themesResult.message,
|
|
11030
11081
|
};
|
|
11031
11082
|
}
|
|
11032
11083
|
|
|
11033
|
-
|
|
11034
|
-
|
|
11035
|
-
|
|
11036
|
-
return {
|
|
11037
|
-
success: false,
|
|
11038
|
-
error: `Invalid dashboard config: ${validation.errors.join(", ")}`,
|
|
11039
|
-
};
|
|
11084
|
+
const themeData = themesResult.themes[themeKey];
|
|
11085
|
+
if (!themeData) {
|
|
11086
|
+
return { success: false, error: `Theme "${themeKey}" not found` };
|
|
11040
11087
|
}
|
|
11041
11088
|
|
|
11042
|
-
|
|
11043
|
-
dashboardConfig = applyDefaults(dashboardConfig);
|
|
11089
|
+
const colors = extractColors(themeData);
|
|
11044
11090
|
|
|
11045
|
-
// Delegate to shared import pipeline with overrides
|
|
11046
|
-
return await processDashboardConfig(
|
|
11047
|
-
win,
|
|
11048
|
-
appId,
|
|
11049
|
-
dashboardConfig,
|
|
11050
|
-
widgetRegistry,
|
|
11051
|
-
{
|
|
11052
|
-
name: options.name,
|
|
11053
|
-
menuId: options.menuId,
|
|
11054
|
-
themeKey: options.themeKey,
|
|
11055
|
-
},
|
|
11056
|
-
);
|
|
11057
|
-
} catch (error) {
|
|
11058
|
-
console.error(
|
|
11059
|
-
"[DashboardConfigController] Error importing dashboard:",
|
|
11060
|
-
error,
|
|
11061
|
-
);
|
|
11062
11091
|
return {
|
|
11063
|
-
success:
|
|
11064
|
-
|
|
11092
|
+
success: true,
|
|
11093
|
+
themeName: themeKey,
|
|
11094
|
+
colors,
|
|
11095
|
+
hasRegistryMeta: !!themeData._registryMeta,
|
|
11065
11096
|
};
|
|
11097
|
+
} catch (err) {
|
|
11098
|
+
console.error("[ThemeRegistryController] Error getting preview:", err);
|
|
11099
|
+
return { success: false, error: err.message };
|
|
11066
11100
|
}
|
|
11067
11101
|
}
|
|
11068
11102
|
|
|
11103
|
+
var themeRegistryController$1 = {
|
|
11104
|
+
prepareThemeForPublish: prepareThemeForPublish$1,
|
|
11105
|
+
installThemeFromRegistry: installThemeFromRegistry$1,
|
|
11106
|
+
getThemePublishPreview: getThemePublishPreview$1,
|
|
11107
|
+
generateThemeRegistryManifest,
|
|
11108
|
+
extractColors,
|
|
11109
|
+
};
|
|
11110
|
+
|
|
11069
11111
|
/**
|
|
11070
|
-
*
|
|
11071
|
-
* Used by both importDashboardConfig (ZIP) and installDashboardFromRegistry.
|
|
11112
|
+
* dashboardConfigController.js
|
|
11072
11113
|
*
|
|
11073
|
-
*
|
|
11114
|
+
* Handles export and import of dashboard configuration files.
|
|
11115
|
+
* Runs in the Electron main process.
|
|
11116
|
+
*
|
|
11117
|
+
* Export: serializes a workspace into a .dashboard.json config,
|
|
11118
|
+
* resolving widget dependencies, extracting event wiring from
|
|
11119
|
+
* layout listeners, and aggregating provider requirements.
|
|
11120
|
+
*
|
|
11121
|
+
* Import: validates and processes a .dashboard.json config,
|
|
11122
|
+
* auto-installs missing widgets, creates workspace, and
|
|
11123
|
+
* applies event wiring. (Import is implemented in DASH-13.)
|
|
11124
|
+
*/
|
|
11125
|
+
|
|
11126
|
+
const { app: app$1, dialog } = require$$0$1;
|
|
11127
|
+
const path$1 = require$$1$1;
|
|
11128
|
+
const AdmZip = require$$3$3;
|
|
11129
|
+
const { getFileContents: getFileContents$1 } = file;
|
|
11130
|
+
const {
|
|
11131
|
+
validateDashboardConfig,
|
|
11132
|
+
applyDefaults,
|
|
11133
|
+
CURRENT_SCHEMA_VERSION,
|
|
11134
|
+
} = dashboardConfigValidator$1;
|
|
11135
|
+
const {
|
|
11136
|
+
collectComponentNames,
|
|
11137
|
+
extractEventWiring,
|
|
11138
|
+
buildWidgetDependencies,
|
|
11139
|
+
buildProviderRequirements,
|
|
11140
|
+
applyEventWiringToLayout,
|
|
11141
|
+
} = dashboardConfigUtils$1;
|
|
11142
|
+
const { searchRegistry, getPackage } = registryController$2;
|
|
11143
|
+
const themeController$1 = themeController_1;
|
|
11144
|
+
|
|
11145
|
+
const configFilename = "workspaces.json";
|
|
11146
|
+
const appName$1 = "Dashboard";
|
|
11147
|
+
|
|
11148
|
+
/**
|
|
11149
|
+
* Export a workspace as a .dashboard.json config inside a ZIP file.
|
|
11150
|
+
*
|
|
11151
|
+
* @param {BrowserWindow} win - The main window (for dialog)
|
|
11074
11152
|
* @param {string} appId - Application identifier
|
|
11075
|
-
* @param {
|
|
11076
|
-
* @param {Object}
|
|
11077
|
-
* @param {
|
|
11078
|
-
* @param {string} options.
|
|
11079
|
-
* @
|
|
11153
|
+
* @param {number|string} workspaceId - ID of the workspace to export
|
|
11154
|
+
* @param {Object} options - Export options
|
|
11155
|
+
* @param {string} options.authorName - Dashboard author name
|
|
11156
|
+
* @param {string} options.authorId - Dashboard author ID
|
|
11157
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance (optional)
|
|
11158
|
+
* @returns {Promise<Object>} Result with success flag and file path
|
|
11080
11159
|
*/
|
|
11081
|
-
async function
|
|
11160
|
+
async function exportDashboardConfig$1(
|
|
11082
11161
|
win,
|
|
11083
11162
|
appId,
|
|
11084
|
-
|
|
11085
|
-
widgetRegistry = null,
|
|
11163
|
+
workspaceId,
|
|
11086
11164
|
options = {},
|
|
11165
|
+
widgetRegistry = null,
|
|
11087
11166
|
) {
|
|
11088
|
-
|
|
11167
|
+
try {
|
|
11168
|
+
// 1. Read workspace from workspaces.json
|
|
11169
|
+
const filename = path$1.join(
|
|
11170
|
+
app$1.getPath("userData"),
|
|
11171
|
+
appName$1,
|
|
11172
|
+
appId,
|
|
11173
|
+
configFilename,
|
|
11174
|
+
);
|
|
11175
|
+
const workspacesArray = getFileContents$1(filename);
|
|
11176
|
+
const workspace = workspacesArray.find(
|
|
11177
|
+
(w) => w.id === workspaceId || w.id === Number(workspaceId),
|
|
11178
|
+
);
|
|
11089
11179
|
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
|
|
11180
|
+
if (!workspace) {
|
|
11181
|
+
return {
|
|
11182
|
+
success: false,
|
|
11183
|
+
error: `Workspace not found: ${workspaceId}`,
|
|
11184
|
+
};
|
|
11185
|
+
}
|
|
11096
11186
|
|
|
11097
|
-
|
|
11098
|
-
widgetRegistry &&
|
|
11099
|
-
dashboardConfig.widgets &&
|
|
11100
|
-
dashboardConfig.widgets.length
|
|
11101
|
-
) {
|
|
11102
|
-
const installedWidgets = widgetRegistry.getWidgets();
|
|
11103
|
-
const installedPackages = new Set(installedWidgets.map((w) => w.name));
|
|
11187
|
+
const layout = workspace.layout || [];
|
|
11104
11188
|
|
|
11105
|
-
|
|
11106
|
-
|
|
11189
|
+
// 2. Collect components, extract wiring, resolve deps
|
|
11190
|
+
const componentNames = collectComponentNames(layout);
|
|
11191
|
+
const eventWiring = extractEventWiring(layout);
|
|
11192
|
+
const widgets = buildWidgetDependencies(componentNames, widgetRegistry);
|
|
11193
|
+
const providers = buildProviderRequirements(componentNames, widgetRegistry);
|
|
11107
11194
|
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11195
|
+
// 3. Build the dashboard config
|
|
11196
|
+
const dashboardConfig = applyDefaults({
|
|
11197
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
11198
|
+
name: workspace.name || workspace.label || "Exported Dashboard",
|
|
11199
|
+
description: options.description || "",
|
|
11200
|
+
...(options.authorName
|
|
11201
|
+
? { author: { name: options.authorName, id: options.authorId || "" } }
|
|
11202
|
+
: {}),
|
|
11203
|
+
shareable: true,
|
|
11204
|
+
tags: options.tags || [],
|
|
11205
|
+
icon: options.icon || "grip",
|
|
11206
|
+
workspace: {
|
|
11207
|
+
id: workspace.id,
|
|
11208
|
+
name: workspace.name,
|
|
11209
|
+
type: workspace.type || "workspace",
|
|
11210
|
+
label: workspace.label || workspace.name,
|
|
11211
|
+
version: workspace.version || 1,
|
|
11212
|
+
layout,
|
|
11213
|
+
menuId: workspace.menuId || 1,
|
|
11214
|
+
},
|
|
11215
|
+
widgets,
|
|
11216
|
+
providers,
|
|
11217
|
+
eventWiring,
|
|
11218
|
+
});
|
|
11112
11219
|
|
|
11113
|
-
|
|
11220
|
+
// 4. Bundle theme if workspace has a themeKey
|
|
11221
|
+
if (workspace.themeKey) {
|
|
11114
11222
|
try {
|
|
11115
|
-
const
|
|
11116
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
}
|
|
11223
|
+
const themeResult = themeController$1.listThemesForApplication(
|
|
11224
|
+
win,
|
|
11225
|
+
appId,
|
|
11226
|
+
);
|
|
11227
|
+
const themeData = themeResult.themes?.[workspace.themeKey];
|
|
11228
|
+
if (themeData) {
|
|
11229
|
+
dashboardConfig.theme = {
|
|
11230
|
+
key: workspace.themeKey,
|
|
11231
|
+
data: themeData,
|
|
11232
|
+
};
|
|
11233
|
+
if (themeData._registryMeta?.packageName) {
|
|
11234
|
+
dashboardConfig.theme.registryPackage =
|
|
11235
|
+
themeData._registryMeta.packageName;
|
|
11236
|
+
}
|
|
11129
11237
|
}
|
|
11130
|
-
} catch (
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
|
|
11238
|
+
} catch (themeErr) {
|
|
11239
|
+
console.warn(
|
|
11240
|
+
"[DashboardConfigController] Could not bundle theme:",
|
|
11241
|
+
themeErr.message,
|
|
11242
|
+
);
|
|
11135
11243
|
}
|
|
11136
11244
|
}
|
|
11137
|
-
}
|
|
11138
|
-
|
|
11139
|
-
// 2. Build workspace from config
|
|
11140
|
-
const workspace = { ...dashboardConfig.workspace };
|
|
11141
11245
|
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11246
|
+
// 5. Validate the generated config
|
|
11247
|
+
const validation = validateDashboardConfig(dashboardConfig);
|
|
11248
|
+
if (!validation.valid) {
|
|
11249
|
+
return {
|
|
11250
|
+
success: false,
|
|
11251
|
+
error: `Generated config is invalid: ${validation.errors.join(", ")}`,
|
|
11252
|
+
};
|
|
11253
|
+
}
|
|
11148
11254
|
|
|
11149
|
-
|
|
11150
|
-
|
|
11255
|
+
// 6. Show save dialog
|
|
11256
|
+
const sanitizedName = (workspace.name || "dashboard")
|
|
11257
|
+
.replace(/[^a-zA-Z0-9-_ ]/g, "")
|
|
11258
|
+
.replace(/\s+/g, "-")
|
|
11259
|
+
.toLowerCase();
|
|
11151
11260
|
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11261
|
+
const { canceled, filePath } = await dialog.showSaveDialog(win, {
|
|
11262
|
+
title: "Export Dashboard as ZIP",
|
|
11263
|
+
defaultPath: path$1.join(
|
|
11264
|
+
app$1.getPath("desktop"),
|
|
11265
|
+
`dashboard-${sanitizedName}.zip`,
|
|
11266
|
+
),
|
|
11267
|
+
filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
|
|
11268
|
+
});
|
|
11156
11269
|
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
if (
|
|
11160
|
-
dashboardConfig.eventWiring &&
|
|
11161
|
-
dashboardConfig.eventWiring.length &&
|
|
11162
|
-
workspace.layout
|
|
11163
|
-
) {
|
|
11164
|
-
applyEventWiringToLayout(workspace.layout, dashboardConfig.eventWiring);
|
|
11165
|
-
for (const wire of dashboardConfig.eventWiring) {
|
|
11166
|
-
eventWiringSummary.push(
|
|
11167
|
-
`${wire.source?.widget}.${wire.source?.event} → ${wire.target?.widget}.${wire.target?.handler}`,
|
|
11168
|
-
);
|
|
11270
|
+
if (canceled || !filePath) {
|
|
11271
|
+
return { success: false, canceled: true };
|
|
11169
11272
|
}
|
|
11170
|
-
}
|
|
11171
11273
|
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
|
|
11179
|
-
schemaVersion: dashboardConfig.schemaVersion,
|
|
11180
|
-
registryPackage: options.registryPackage || null,
|
|
11181
|
-
installedVersion: options.installedVersion || null,
|
|
11182
|
-
};
|
|
11274
|
+
// 7. Create ZIP with the config
|
|
11275
|
+
const zip = new AdmZip();
|
|
11276
|
+
const configJson = JSON.stringify(dashboardConfig, null, 2);
|
|
11277
|
+
zip.addFile(
|
|
11278
|
+
`${sanitizedName}.dashboard.json`,
|
|
11279
|
+
Buffer.from(configJson, "utf-8"),
|
|
11280
|
+
);
|
|
11183
11281
|
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
workspace,
|
|
11190
|
-
);
|
|
11282
|
+
zip.writeZip(filePath);
|
|
11283
|
+
|
|
11284
|
+
console.log(
|
|
11285
|
+
`[DashboardConfigController] Exported dashboard to: ${filePath}`,
|
|
11286
|
+
);
|
|
11191
11287
|
|
|
11192
|
-
|
|
11288
|
+
return {
|
|
11289
|
+
success: true,
|
|
11290
|
+
filePath,
|
|
11291
|
+
config: dashboardConfig,
|
|
11292
|
+
};
|
|
11293
|
+
} catch (error) {
|
|
11294
|
+
console.error(
|
|
11295
|
+
"[DashboardConfigController] Error exporting dashboard:",
|
|
11296
|
+
error,
|
|
11297
|
+
);
|
|
11193
11298
|
return {
|
|
11194
11299
|
success: false,
|
|
11195
|
-
error:
|
|
11300
|
+
error: error.message,
|
|
11196
11301
|
};
|
|
11197
11302
|
}
|
|
11198
|
-
|
|
11199
|
-
// Build provider requirements summary
|
|
11200
|
-
const providerSummary = (dashboardConfig.providers || []).map((p) => ({
|
|
11201
|
-
type: p.type,
|
|
11202
|
-
providerClass: p.providerClass,
|
|
11203
|
-
required: p.required,
|
|
11204
|
-
usedBy: p.usedBy,
|
|
11205
|
-
}));
|
|
11206
|
-
|
|
11207
|
-
console.log(
|
|
11208
|
-
`[DashboardConfigController] Imported dashboard "${dashboardConfig.name}" (${source}) as workspace ${workspace.id}`,
|
|
11209
|
-
);
|
|
11210
|
-
|
|
11211
|
-
return {
|
|
11212
|
-
success: true,
|
|
11213
|
-
workspace,
|
|
11214
|
-
summary: {
|
|
11215
|
-
name: dashboardConfig.name,
|
|
11216
|
-
description: dashboardConfig.description || "",
|
|
11217
|
-
author: dashboardConfig.author,
|
|
11218
|
-
widgets: installSummary,
|
|
11219
|
-
eventsWired: eventWiringSummary,
|
|
11220
|
-
providersRequired: providerSummary,
|
|
11221
|
-
},
|
|
11222
|
-
};
|
|
11223
|
-
}
|
|
11303
|
+
}
|
|
11224
11304
|
|
|
11225
11305
|
/**
|
|
11226
|
-
*
|
|
11227
|
-
*
|
|
11228
|
-
*
|
|
11229
|
-
* validates it, and delegates to the shared import pipeline.
|
|
11306
|
+
* Select and preview a dashboard ZIP file without importing it.
|
|
11307
|
+
* Opens the file picker, extracts and validates the .dashboard.json,
|
|
11308
|
+
* and returns a preview of the config + the file path for later import.
|
|
11230
11309
|
*
|
|
11231
|
-
* @param {BrowserWindow} win - The main window
|
|
11232
|
-
* @
|
|
11233
|
-
* @param {string} packageName - Registry package name for the dashboard
|
|
11234
|
-
* @param {Object} widgetRegistry - WidgetRegistry instance
|
|
11235
|
-
* @returns {Promise<Object>} Result with success, workspace, and summary
|
|
11310
|
+
* @param {BrowserWindow} win - The main window (for dialog)
|
|
11311
|
+
* @returns {Promise<Object>} Result with success, filePath, and dashboardConfig preview
|
|
11236
11312
|
*/
|
|
11237
|
-
async function
|
|
11238
|
-
win,
|
|
11239
|
-
appId,
|
|
11240
|
-
packageName,
|
|
11241
|
-
widgetRegistry = null,
|
|
11242
|
-
) {
|
|
11313
|
+
async function selectDashboardFile$1(win) {
|
|
11243
11314
|
try {
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11315
|
+
const { canceled, filePaths } = await dialog.showOpenDialog(win, {
|
|
11316
|
+
title: "Import Dashboard Configuration",
|
|
11317
|
+
filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
|
|
11318
|
+
properties: ["openFile"],
|
|
11319
|
+
});
|
|
11320
|
+
|
|
11321
|
+
if (canceled || !filePaths || !filePaths.length) {
|
|
11322
|
+
return { success: false, canceled: true };
|
|
11323
|
+
}
|
|
11324
|
+
|
|
11325
|
+
const zipPath = filePaths[0];
|
|
11326
|
+
|
|
11327
|
+
// Extract and validate
|
|
11328
|
+
const zip = new AdmZip(zipPath);
|
|
11329
|
+
const tempDir = path$1.join(app$1.getPath("temp"), "dash-import");
|
|
11330
|
+
const { validateZipEntries } = widgetRegistryExports;
|
|
11331
|
+
validateZipEntries(zip, tempDir);
|
|
11332
|
+
|
|
11333
|
+
const entries = zip.getEntries();
|
|
11334
|
+
const configEntry = entries.find((e) =>
|
|
11335
|
+
e.entryName.endsWith(".dashboard.json"),
|
|
11336
|
+
);
|
|
11337
|
+
|
|
11338
|
+
if (!configEntry) {
|
|
11247
11339
|
return {
|
|
11248
11340
|
success: false,
|
|
11249
|
-
error:
|
|
11341
|
+
error: "No .dashboard.json file found in ZIP archive",
|
|
11250
11342
|
};
|
|
11251
11343
|
}
|
|
11252
11344
|
|
|
11253
|
-
|
|
11345
|
+
const configJson = configEntry.getData().toString("utf-8");
|
|
11346
|
+
let dashboardConfig;
|
|
11347
|
+
try {
|
|
11348
|
+
dashboardConfig = JSON.parse(configJson);
|
|
11349
|
+
} catch (parseError) {
|
|
11254
11350
|
return {
|
|
11255
11351
|
success: false,
|
|
11256
|
-
error: `
|
|
11352
|
+
error: `Invalid JSON: ${parseError.message}`,
|
|
11257
11353
|
};
|
|
11258
11354
|
}
|
|
11259
11355
|
|
|
11260
|
-
|
|
11261
|
-
|
|
11262
|
-
let downloadUrl = registryPkg.downloadUrl;
|
|
11263
|
-
downloadUrl = downloadUrl.replace("{version}", version);
|
|
11264
|
-
downloadUrl = downloadUrl.replace("{name}", packageName);
|
|
11265
|
-
|
|
11266
|
-
// Enforce HTTPS
|
|
11267
|
-
const parsedUrl = new URL(downloadUrl);
|
|
11268
|
-
if (parsedUrl.protocol !== "https:") {
|
|
11356
|
+
const validation = validateDashboardConfig(dashboardConfig);
|
|
11357
|
+
if (!validation.valid) {
|
|
11269
11358
|
return {
|
|
11270
11359
|
success: false,
|
|
11271
|
-
error: `
|
|
11360
|
+
error: `Invalid config: ${validation.errors.join(", ")}`,
|
|
11272
11361
|
};
|
|
11273
11362
|
}
|
|
11274
11363
|
|
|
11275
|
-
|
|
11276
|
-
|
|
11364
|
+
dashboardConfig = applyDefaults(dashboardConfig);
|
|
11365
|
+
|
|
11366
|
+
return {
|
|
11367
|
+
success: true,
|
|
11368
|
+
filePath: zipPath,
|
|
11369
|
+
dashboardConfig: {
|
|
11370
|
+
name: dashboardConfig.name,
|
|
11371
|
+
description: dashboardConfig.description,
|
|
11372
|
+
author: dashboardConfig.author,
|
|
11373
|
+
workspace: dashboardConfig.workspace,
|
|
11374
|
+
widgets: dashboardConfig.widgets || [],
|
|
11375
|
+
providers: dashboardConfig.providers || [],
|
|
11376
|
+
},
|
|
11377
|
+
};
|
|
11378
|
+
} catch (error) {
|
|
11379
|
+
console.error(
|
|
11380
|
+
"[DashboardConfigController] Error selecting dashboard file:",
|
|
11381
|
+
error,
|
|
11277
11382
|
);
|
|
11383
|
+
return { success: false, error: error.message };
|
|
11384
|
+
}
|
|
11385
|
+
}
|
|
11278
11386
|
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
|
|
11284
|
-
|
|
11387
|
+
/**
|
|
11388
|
+
* Import a dashboard from a ZIP file containing a .dashboard.json config.
|
|
11389
|
+
*
|
|
11390
|
+
* Steps:
|
|
11391
|
+
* 1. Show native file picker for .zip selection (or use options.filePath)
|
|
11392
|
+
* 2. Extract and validate .dashboard.json
|
|
11393
|
+
* 3. Auto-install missing widgets from registry
|
|
11394
|
+
* 4. Create workspace in workspaces.json
|
|
11395
|
+
* 5. Apply event wiring to layout
|
|
11396
|
+
* 6. Mark imported dashboard shareable: false
|
|
11397
|
+
*
|
|
11398
|
+
* @param {BrowserWindow} win - The main window (for dialog)
|
|
11399
|
+
* @param {string} appId - Application identifier
|
|
11400
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance (needs getWidgets(), downloadWidget())
|
|
11401
|
+
* @param {Object} options - Import options
|
|
11402
|
+
* @param {string} options.filePath - Skip file picker, use this path directly
|
|
11403
|
+
* @param {string} options.name - Override workspace name
|
|
11404
|
+
* @param {number} options.menuId - Override workspace menuId (folder)
|
|
11405
|
+
* @param {string} options.themeKey - Override workspace themeKey
|
|
11406
|
+
* @returns {Promise<Object>} Result with success, workspace, and import summary
|
|
11407
|
+
*/
|
|
11408
|
+
async function importDashboardConfig$1(
|
|
11409
|
+
win,
|
|
11410
|
+
appId,
|
|
11411
|
+
widgetRegistry = null,
|
|
11412
|
+
options = {},
|
|
11413
|
+
) {
|
|
11414
|
+
try {
|
|
11415
|
+
let zipPath;
|
|
11416
|
+
|
|
11417
|
+
if (options.filePath) {
|
|
11418
|
+
// Use the provided file path (from selectDashboardFile)
|
|
11419
|
+
zipPath = options.filePath;
|
|
11420
|
+
} else {
|
|
11421
|
+
// Show file picker
|
|
11422
|
+
const { canceled, filePaths } = await dialog.showOpenDialog(win, {
|
|
11423
|
+
title: "Import Dashboard Configuration",
|
|
11424
|
+
filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
|
|
11425
|
+
properties: ["openFile"],
|
|
11426
|
+
});
|
|
11427
|
+
|
|
11428
|
+
if (canceled || !filePaths || !filePaths.length) {
|
|
11429
|
+
return { success: false, canceled: true };
|
|
11430
|
+
}
|
|
11431
|
+
|
|
11432
|
+
zipPath = filePaths[0];
|
|
11285
11433
|
}
|
|
11286
11434
|
|
|
11287
|
-
|
|
11288
|
-
const zip = new AdmZip
|
|
11435
|
+
// 2. Extract and validate .dashboard.json from ZIP
|
|
11436
|
+
const zip = new AdmZip(zipPath);
|
|
11289
11437
|
|
|
11290
|
-
//
|
|
11291
|
-
const tempDir = path$
|
|
11438
|
+
// Validate ZIP entries for path traversal
|
|
11439
|
+
const tempDir = path$1.join(app$1.getPath("temp"), "dash-import");
|
|
11292
11440
|
const { validateZipEntries } = widgetRegistryExports;
|
|
11293
11441
|
validateZipEntries(zip, tempDir);
|
|
11294
11442
|
|
|
11295
|
-
//
|
|
11443
|
+
// Find the .dashboard.json file
|
|
11296
11444
|
const entries = zip.getEntries();
|
|
11297
11445
|
const configEntry = entries.find((e) =>
|
|
11298
11446
|
e.entryName.endsWith(".dashboard.json"),
|
|
@@ -11301,7 +11449,7 @@ async function installDashboardFromRegistry$1(
|
|
|
11301
11449
|
if (!configEntry) {
|
|
11302
11450
|
return {
|
|
11303
11451
|
success: false,
|
|
11304
|
-
error: "No .dashboard.json file found in
|
|
11452
|
+
error: "No .dashboard.json file found in ZIP archive",
|
|
11305
11453
|
};
|
|
11306
11454
|
}
|
|
11307
11455
|
|
|
@@ -11312,11 +11460,11 @@ async function installDashboardFromRegistry$1(
|
|
|
11312
11460
|
} catch (parseError) {
|
|
11313
11461
|
return {
|
|
11314
11462
|
success: false,
|
|
11315
|
-
error: `Invalid JSON in
|
|
11463
|
+
error: `Invalid JSON in ${configEntry.entryName}: ${parseError.message}`,
|
|
11316
11464
|
};
|
|
11317
11465
|
}
|
|
11318
11466
|
|
|
11319
|
-
//
|
|
11467
|
+
// Validate against schema
|
|
11320
11468
|
const validation = validateDashboardConfig(dashboardConfig);
|
|
11321
11469
|
if (!validation.valid) {
|
|
11322
11470
|
return {
|
|
@@ -11325,23 +11473,24 @@ async function installDashboardFromRegistry$1(
|
|
|
11325
11473
|
};
|
|
11326
11474
|
}
|
|
11327
11475
|
|
|
11476
|
+
// Apply defaults to fill in optional fields
|
|
11328
11477
|
dashboardConfig = applyDefaults(dashboardConfig);
|
|
11329
11478
|
|
|
11330
|
-
//
|
|
11479
|
+
// Delegate to shared import pipeline with overrides
|
|
11331
11480
|
return await processDashboardConfig(
|
|
11332
11481
|
win,
|
|
11333
11482
|
appId,
|
|
11334
11483
|
dashboardConfig,
|
|
11335
11484
|
widgetRegistry,
|
|
11336
11485
|
{
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11486
|
+
name: options.name,
|
|
11487
|
+
menuId: options.menuId,
|
|
11488
|
+
themeKey: options.themeKey,
|
|
11340
11489
|
},
|
|
11341
11490
|
);
|
|
11342
11491
|
} catch (error) {
|
|
11343
11492
|
console.error(
|
|
11344
|
-
"[DashboardConfigController] Error
|
|
11493
|
+
"[DashboardConfigController] Error importing dashboard:",
|
|
11345
11494
|
error,
|
|
11346
11495
|
);
|
|
11347
11496
|
return {
|
|
@@ -11352,401 +11501,429 @@ async function installDashboardFromRegistry$1(
|
|
|
11352
11501
|
}
|
|
11353
11502
|
|
|
11354
11503
|
/**
|
|
11355
|
-
*
|
|
11356
|
-
*
|
|
11357
|
-
*
|
|
11358
|
-
* @param {Array} dashboardWidgets - Widget deps from dashboard config
|
|
11359
|
-
* @param {Object} widgetRegistry - WidgetRegistry instance (needs getWidgets())
|
|
11360
|
-
* @returns {Promise<Object>} Compatibility report
|
|
11361
|
-
*/
|
|
11362
|
-
async function checkCompatibility$1(dashboardWidgets, widgetRegistry = null) {
|
|
11363
|
-
const {
|
|
11364
|
-
checkDashboardCompatibility,
|
|
11365
|
-
} = dashboardConfigUtils$1;
|
|
11366
|
-
const { fetchRegistryIndex } = registryController$2;
|
|
11367
|
-
|
|
11368
|
-
const installedWidgets = widgetRegistry ? widgetRegistry.getWidgets() : [];
|
|
11369
|
-
|
|
11370
|
-
let registryPackages = [];
|
|
11371
|
-
try {
|
|
11372
|
-
const index = await fetchRegistryIndex();
|
|
11373
|
-
registryPackages = index.packages || [];
|
|
11374
|
-
} catch (err) {
|
|
11375
|
-
console.warn(
|
|
11376
|
-
"[DashboardConfigController] Could not fetch registry index for compatibility check:",
|
|
11377
|
-
err.message,
|
|
11378
|
-
);
|
|
11379
|
-
}
|
|
11380
|
-
|
|
11381
|
-
return checkDashboardCompatibility(
|
|
11382
|
-
dashboardWidgets,
|
|
11383
|
-
installedWidgets,
|
|
11384
|
-
registryPackages,
|
|
11385
|
-
);
|
|
11386
|
-
}
|
|
11387
|
-
|
|
11388
|
-
/**
|
|
11389
|
-
* Prepare a dashboard for publishing to the registry.
|
|
11390
|
-
*
|
|
11391
|
-
* Validates that the workspace is shareable, builds the dashboard config,
|
|
11392
|
-
* checks that all widgets exist in the registry, generates a registry
|
|
11393
|
-
* manifest, and creates a ZIP containing both the manifest and
|
|
11394
|
-
* .dashboard.json config.
|
|
11504
|
+
* Shared import pipeline: install widgets, create workspace, wire events.
|
|
11505
|
+
* Used by both importDashboardConfig (ZIP) and installDashboardFromRegistry.
|
|
11395
11506
|
*
|
|
11396
|
-
* @param {BrowserWindow} win - The main window
|
|
11507
|
+
* @param {BrowserWindow} win - The main window
|
|
11397
11508
|
* @param {string} appId - Application identifier
|
|
11398
|
-
* @param {
|
|
11399
|
-
* @param {Object} options - Publishing options
|
|
11400
|
-
* @param {string} options.authorName - Author name
|
|
11401
|
-
* @param {string} options.authorId - Author ID
|
|
11402
|
-
* @param {string} options.description - Dashboard description
|
|
11403
|
-
* @param {string[]} options.tags - Tags
|
|
11404
|
-
* @param {string} options.icon - Icon name
|
|
11405
|
-
* @param {string} options.githubUser - GitHub user/org for registry scope
|
|
11406
|
-
* @param {string} options.category - Registry category
|
|
11509
|
+
* @param {Object} dashboardConfig - Validated dashboard config object
|
|
11407
11510
|
* @param {Object} widgetRegistry - WidgetRegistry instance
|
|
11408
|
-
* @
|
|
11511
|
+
* @param {Object} options - Additional options
|
|
11512
|
+
* @param {string} options.source - Source label ("zip" or "registry")
|
|
11513
|
+
* @returns {Promise<Object>} Result with success, workspace, and summary
|
|
11409
11514
|
*/
|
|
11410
|
-
async function
|
|
11515
|
+
async function processDashboardConfig(
|
|
11411
11516
|
win,
|
|
11412
11517
|
appId,
|
|
11413
|
-
|
|
11414
|
-
options = {},
|
|
11518
|
+
dashboardConfig,
|
|
11415
11519
|
widgetRegistry = null,
|
|
11520
|
+
options = {},
|
|
11416
11521
|
) {
|
|
11417
|
-
|
|
11418
|
-
const {
|
|
11419
|
-
generateRegistryManifest,
|
|
11420
|
-
} = dashboardConfigUtils$1;
|
|
11421
|
-
|
|
11422
|
-
// 1. Read workspace
|
|
11423
|
-
const filename = path$2.join(
|
|
11424
|
-
app$2.getPath("userData"),
|
|
11425
|
-
appName$1,
|
|
11426
|
-
appId,
|
|
11427
|
-
configFilename,
|
|
11428
|
-
);
|
|
11429
|
-
const workspacesArray = getFileContents$1(filename);
|
|
11430
|
-
const workspace = workspacesArray.find(
|
|
11431
|
-
(w) => w.id === workspaceId || w.id === Number(workspaceId),
|
|
11432
|
-
);
|
|
11433
|
-
|
|
11434
|
-
if (!workspace) {
|
|
11435
|
-
return {
|
|
11436
|
-
success: false,
|
|
11437
|
-
error: `Workspace not found: ${workspaceId}`,
|
|
11438
|
-
};
|
|
11439
|
-
}
|
|
11440
|
-
|
|
11441
|
-
// 2. Check shareable flag — imported dashboards cannot be published
|
|
11442
|
-
if (
|
|
11443
|
-
workspace._dashboardConfig &&
|
|
11444
|
-
workspace._dashboardConfig.shareable === false
|
|
11445
|
-
) {
|
|
11446
|
-
return {
|
|
11447
|
-
success: false,
|
|
11448
|
-
error:
|
|
11449
|
-
"This dashboard was imported and cannot be published. Only dashboards you created can be shared.",
|
|
11450
|
-
};
|
|
11451
|
-
}
|
|
11452
|
-
|
|
11453
|
-
const layout = workspace.layout || [];
|
|
11522
|
+
const source = options.source || "zip";
|
|
11454
11523
|
|
|
11455
|
-
|
|
11456
|
-
|
|
11457
|
-
|
|
11458
|
-
|
|
11459
|
-
|
|
11524
|
+
// 1. Auto-install missing widgets from registry
|
|
11525
|
+
const installSummary = {
|
|
11526
|
+
installed: [],
|
|
11527
|
+
alreadyInstalled: [],
|
|
11528
|
+
failed: [],
|
|
11529
|
+
};
|
|
11460
11530
|
|
|
11461
|
-
|
|
11462
|
-
|
|
11463
|
-
|
|
11464
|
-
|
|
11465
|
-
|
|
11466
|
-
|
|
11467
|
-
|
|
11468
|
-
shareable: true,
|
|
11469
|
-
tags: options.tags || [],
|
|
11470
|
-
icon: options.icon || "grip",
|
|
11471
|
-
workspace: {
|
|
11472
|
-
id: workspace.id,
|
|
11473
|
-
name: workspace.name,
|
|
11474
|
-
type: workspace.type || "workspace",
|
|
11475
|
-
label: workspace.label || workspace.name,
|
|
11476
|
-
version: workspace.version || 1,
|
|
11477
|
-
layout,
|
|
11478
|
-
menuId: workspace.menuId || 1,
|
|
11479
|
-
},
|
|
11480
|
-
widgets,
|
|
11481
|
-
providers,
|
|
11482
|
-
eventWiring,
|
|
11483
|
-
});
|
|
11531
|
+
if (
|
|
11532
|
+
widgetRegistry &&
|
|
11533
|
+
dashboardConfig.widgets &&
|
|
11534
|
+
dashboardConfig.widgets.length
|
|
11535
|
+
) {
|
|
11536
|
+
const installedWidgets = widgetRegistry.getWidgets();
|
|
11537
|
+
const installedPackages = new Set(installedWidgets.map((w) => w.name));
|
|
11484
11538
|
|
|
11485
|
-
|
|
11486
|
-
|
|
11487
|
-
if (!validation.valid) {
|
|
11488
|
-
return {
|
|
11489
|
-
success: false,
|
|
11490
|
-
error: `Generated config is invalid: ${validation.errors.join(", ")}`,
|
|
11491
|
-
};
|
|
11492
|
-
}
|
|
11539
|
+
for (const widgetDep of dashboardConfig.widgets) {
|
|
11540
|
+
const packageName = widgetDep.package;
|
|
11493
11541
|
|
|
11494
|
-
|
|
11495
|
-
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
try {
|
|
11499
|
-
const index = await fetchRegistryIndex();
|
|
11500
|
-
registryPackages = index.packages || [];
|
|
11501
|
-
} catch (err) {
|
|
11502
|
-
console.warn(
|
|
11503
|
-
`[DashboardConfigController] Unable to verify registry: ${err.message}`,
|
|
11504
|
-
);
|
|
11505
|
-
registryCheckFailed = true;
|
|
11506
|
-
}
|
|
11542
|
+
if (installedPackages.has(packageName)) {
|
|
11543
|
+
installSummary.alreadyInstalled.push(packageName);
|
|
11544
|
+
continue;
|
|
11545
|
+
}
|
|
11507
11546
|
|
|
11508
|
-
|
|
11509
|
-
|
|
11510
|
-
|
|
11511
|
-
|
|
11512
|
-
|
|
11513
|
-
|
|
11514
|
-
|
|
11515
|
-
|
|
11516
|
-
|
|
11517
|
-
|
|
11518
|
-
|
|
11519
|
-
|
|
11547
|
+
// Try to find the widget in the registry and install it
|
|
11548
|
+
try {
|
|
11549
|
+
const registryPkg = await getPackage(packageName);
|
|
11550
|
+
if (registryPkg && registryPkg.downloadUrl) {
|
|
11551
|
+
await widgetRegistry.downloadWidget(
|
|
11552
|
+
packageName,
|
|
11553
|
+
registryPkg.downloadUrl,
|
|
11554
|
+
registryPkg.dashConfigUrl || null,
|
|
11555
|
+
);
|
|
11556
|
+
installSummary.installed.push(packageName);
|
|
11557
|
+
installedPackages.add(packageName);
|
|
11558
|
+
} else {
|
|
11559
|
+
installSummary.failed.push({
|
|
11560
|
+
package: packageName,
|
|
11561
|
+
reason: "Not found in registry",
|
|
11562
|
+
});
|
|
11520
11563
|
}
|
|
11564
|
+
} catch (installError) {
|
|
11565
|
+
installSummary.failed.push({
|
|
11566
|
+
package: packageName,
|
|
11567
|
+
reason: installError.message,
|
|
11568
|
+
});
|
|
11521
11569
|
}
|
|
11522
|
-
missingFromRegistry = Object.entries(grouped).map(
|
|
11523
|
-
([pkg, widgetNames]) => ({ package: pkg, widgets: widgetNames }),
|
|
11524
|
-
);
|
|
11525
|
-
}
|
|
11526
|
-
|
|
11527
|
-
// 6. Generate registry manifest
|
|
11528
|
-
const manifest = generateRegistryManifest(dashboardConfig, {
|
|
11529
|
-
githubUser: options.githubUser || options.authorId || "",
|
|
11530
|
-
category: options.category || "general",
|
|
11531
|
-
repository: options.repository || "",
|
|
11532
|
-
});
|
|
11533
|
-
|
|
11534
|
-
// 7. Show save dialog for the publish package
|
|
11535
|
-
const sanitizedName = manifest.name;
|
|
11536
|
-
const { canceled, filePath } = await dialog$1.showSaveDialog(win, {
|
|
11537
|
-
title: "Save Dashboard Package for Registry",
|
|
11538
|
-
defaultPath: path$2.join(
|
|
11539
|
-
app$2.getPath("desktop"),
|
|
11540
|
-
`dashboard-${sanitizedName}-v${manifest.version}.zip`,
|
|
11541
|
-
),
|
|
11542
|
-
filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
|
|
11543
|
-
});
|
|
11544
|
-
|
|
11545
|
-
if (canceled || !filePath) {
|
|
11546
|
-
return { success: false, canceled: true };
|
|
11547
11570
|
}
|
|
11571
|
+
}
|
|
11548
11572
|
|
|
11549
|
-
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
Buffer.from(JSON.stringify(manifest, null, 2), "utf-8"),
|
|
11554
|
-
);
|
|
11555
|
-
zip.addFile(
|
|
11556
|
-
`${sanitizedName}.dashboard.json`,
|
|
11557
|
-
Buffer.from(JSON.stringify(dashboardConfig, null, 2), "utf-8"),
|
|
11558
|
-
);
|
|
11559
|
-
zip.writeZip(filePath);
|
|
11560
|
-
|
|
11561
|
-
console.log(
|
|
11562
|
-
`[DashboardConfigController] Prepared publish package: ${filePath}`,
|
|
11563
|
-
);
|
|
11564
|
-
|
|
11565
|
-
// 9. Attempt to publish to registry if authenticated
|
|
11566
|
-
let registrySubmission = null;
|
|
11573
|
+
// 2. Install bundled theme if present
|
|
11574
|
+
let themeInstalled = null;
|
|
11575
|
+
if (dashboardConfig.theme) {
|
|
11576
|
+
const bundledTheme = dashboardConfig.theme;
|
|
11567
11577
|
try {
|
|
11568
|
-
const
|
|
11569
|
-
const
|
|
11570
|
-
const
|
|
11571
|
-
|
|
11572
|
-
if (
|
|
11573
|
-
|
|
11574
|
-
|
|
11575
|
-
if (
|
|
11578
|
+
const themeResult = themeController$1.listThemesForApplication(win, appId);
|
|
11579
|
+
const existingThemes = themeResult.themes || {};
|
|
11580
|
+
const themeKey = bundledTheme.key;
|
|
11581
|
+
|
|
11582
|
+
if (bundledTheme.data && themeKey && !existingThemes[themeKey]) {
|
|
11583
|
+
// Embed registry origin metadata if present
|
|
11584
|
+
const themeData = { ...bundledTheme.data };
|
|
11585
|
+
if (bundledTheme.registryPackage) {
|
|
11586
|
+
themeData._registryMeta = {
|
|
11587
|
+
source: "dashboard-import",
|
|
11588
|
+
packageName: bundledTheme.registryPackage,
|
|
11589
|
+
installedAt: new Date().toISOString(),
|
|
11590
|
+
};
|
|
11591
|
+
}
|
|
11592
|
+
themeController$1.saveThemeForApplication(
|
|
11593
|
+
win,
|
|
11594
|
+
appId,
|
|
11595
|
+
themeKey,
|
|
11596
|
+
themeData,
|
|
11597
|
+
);
|
|
11598
|
+
themeInstalled = themeKey;
|
|
11599
|
+
console.log(
|
|
11600
|
+
`[DashboardConfigController] Installed bundled theme: ${themeKey}`,
|
|
11601
|
+
);
|
|
11602
|
+
} else if (
|
|
11603
|
+
!bundledTheme.data &&
|
|
11604
|
+
bundledTheme.registryPackage &&
|
|
11605
|
+
themeKey &&
|
|
11606
|
+
!existingThemes[themeKey]
|
|
11607
|
+
) {
|
|
11608
|
+
// Fallback: try to install from registry by package name
|
|
11609
|
+
try {
|
|
11610
|
+
const {
|
|
11611
|
+
installThemeFromRegistry,
|
|
11612
|
+
} = themeRegistryController$1;
|
|
11613
|
+
await installThemeFromRegistry(
|
|
11614
|
+
win,
|
|
11615
|
+
appId,
|
|
11616
|
+
bundledTheme.registryPackage,
|
|
11617
|
+
);
|
|
11618
|
+
themeInstalled = themeKey;
|
|
11576
11619
|
console.log(
|
|
11577
|
-
`[DashboardConfigController]
|
|
11620
|
+
`[DashboardConfigController] Installed theme from registry: ${bundledTheme.registryPackage}`,
|
|
11578
11621
|
);
|
|
11579
|
-
}
|
|
11622
|
+
} catch (registryErr) {
|
|
11580
11623
|
console.warn(
|
|
11581
|
-
`[DashboardConfigController]
|
|
11624
|
+
`[DashboardConfigController] Could not install theme from registry: ${registryErr.message}`,
|
|
11582
11625
|
);
|
|
11583
11626
|
}
|
|
11584
|
-
} else {
|
|
11585
|
-
registrySubmission = { success: false, authRequired: true };
|
|
11586
11627
|
}
|
|
11587
|
-
} catch (
|
|
11628
|
+
} catch (themeErr) {
|
|
11588
11629
|
console.warn(
|
|
11589
|
-
`[DashboardConfigController]
|
|
11630
|
+
`[DashboardConfigController] Could not install bundled theme: ${themeErr.message}`,
|
|
11590
11631
|
);
|
|
11591
|
-
registrySubmission = { success: false, error: err.message };
|
|
11592
11632
|
}
|
|
11593
|
-
|
|
11594
|
-
return {
|
|
11595
|
-
success: true,
|
|
11596
|
-
filePath,
|
|
11597
|
-
manifest,
|
|
11598
|
-
config: dashboardConfig,
|
|
11599
|
-
warnings:
|
|
11600
|
-
missingFromRegistry.length > 0 ? missingFromRegistry : undefined,
|
|
11601
|
-
registryCheckFailed: registryCheckFailed || undefined,
|
|
11602
|
-
registrySubmission,
|
|
11603
|
-
};
|
|
11604
|
-
} catch (error) {
|
|
11605
|
-
console.error(
|
|
11606
|
-
"[DashboardConfigController] Error preparing dashboard for publish:",
|
|
11607
|
-
error,
|
|
11608
|
-
);
|
|
11609
|
-
return {
|
|
11610
|
-
success: false,
|
|
11611
|
-
error: error.message,
|
|
11612
|
-
};
|
|
11613
11633
|
}
|
|
11614
|
-
}
|
|
11615
11634
|
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
* Combines the structured preview data with a compatibility check.
|
|
11619
|
-
*
|
|
11620
|
-
* @param {string} packageName - Registry package name
|
|
11621
|
-
* @param {Object} widgetRegistry - WidgetRegistry instance
|
|
11622
|
-
* @returns {Promise<Object>} Preview data with compatibility report
|
|
11623
|
-
*/
|
|
11624
|
-
async function getDashboardPreview$1(packageName, widgetRegistry = null) {
|
|
11625
|
-
const {
|
|
11626
|
-
buildDashboardPreview,
|
|
11627
|
-
checkDashboardCompatibility,
|
|
11628
|
-
} = dashboardConfigUtils$1;
|
|
11629
|
-
const { getPackage, fetchRegistryIndex } = registryController$2;
|
|
11635
|
+
// 3. Build workspace from config
|
|
11636
|
+
const workspace = { ...dashboardConfig.workspace };
|
|
11630
11637
|
|
|
11631
|
-
|
|
11632
|
-
if (!pkg) {
|
|
11638
|
+
if (!workspace || !workspace.layout) {
|
|
11633
11639
|
return {
|
|
11634
11640
|
success: false,
|
|
11635
|
-
error:
|
|
11641
|
+
error: "Dashboard config has no workspace data",
|
|
11636
11642
|
};
|
|
11637
11643
|
}
|
|
11638
11644
|
|
|
11639
|
-
|
|
11645
|
+
// Generate a unique ID for the imported workspace
|
|
11646
|
+
workspace.id = Date.now();
|
|
11640
11647
|
|
|
11641
|
-
//
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
const index = await fetchRegistryIndex();
|
|
11646
|
-
registryPackages = index.packages || [];
|
|
11647
|
-
} catch (err) {
|
|
11648
|
-
// Non-fatal — preview still works without compatibility
|
|
11649
|
-
}
|
|
11648
|
+
// Apply name/menuId/themeKey overrides if provided
|
|
11649
|
+
if (options.name) workspace.name = options.name;
|
|
11650
|
+
if (options.menuId !== undefined) workspace.menuId = options.menuId;
|
|
11651
|
+
if (options.themeKey !== undefined) workspace.themeKey = options.themeKey;
|
|
11650
11652
|
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11653
|
+
// Set themeKey from bundled theme if it was installed and no override given
|
|
11654
|
+
if (themeInstalled && options.themeKey === undefined) {
|
|
11655
|
+
workspace.themeKey = themeInstalled;
|
|
11656
|
+
}
|
|
11657
|
+
|
|
11658
|
+
// 4. Apply event wiring to layout
|
|
11659
|
+
const eventWiringSummary = [];
|
|
11660
|
+
if (
|
|
11661
|
+
dashboardConfig.eventWiring &&
|
|
11662
|
+
dashboardConfig.eventWiring.length &&
|
|
11663
|
+
workspace.layout
|
|
11664
|
+
) {
|
|
11665
|
+
applyEventWiringToLayout(workspace.layout, dashboardConfig.eventWiring);
|
|
11666
|
+
for (const wire of dashboardConfig.eventWiring) {
|
|
11667
|
+
eventWiringSummary.push(
|
|
11668
|
+
`${wire.source?.widget}.${wire.source?.event} → ${wire.target?.widget}.${wire.target?.handler}`,
|
|
11669
|
+
);
|
|
11670
|
+
}
|
|
11671
|
+
}
|
|
11672
|
+
|
|
11673
|
+
// 5. Mark as not shareable (imported dashboards cannot be re-published)
|
|
11674
|
+
workspace._dashboardConfig = {
|
|
11675
|
+
shareable: false,
|
|
11676
|
+
source,
|
|
11677
|
+
importedFrom: dashboardConfig.name,
|
|
11678
|
+
importedAt: new Date().toISOString(),
|
|
11679
|
+
originalAuthor: dashboardConfig.author,
|
|
11680
|
+
schemaVersion: dashboardConfig.schemaVersion,
|
|
11681
|
+
registryPackage: options.registryPackage || null,
|
|
11682
|
+
installedVersion: options.installedVersion || null,
|
|
11683
|
+
};
|
|
11684
|
+
|
|
11685
|
+
// Save workspace to workspaces.json
|
|
11686
|
+
const workspaceController = workspaceController_1;
|
|
11687
|
+
const saveResult = workspaceController.saveWorkspaceForApplication(
|
|
11688
|
+
win,
|
|
11689
|
+
appId,
|
|
11690
|
+
workspace,
|
|
11691
|
+
);
|
|
11692
|
+
|
|
11693
|
+
if (saveResult.error) {
|
|
11694
|
+
return {
|
|
11695
|
+
success: false,
|
|
11696
|
+
error: `Failed to save workspace: ${saveResult.message}`,
|
|
11697
|
+
};
|
|
11698
|
+
}
|
|
11699
|
+
|
|
11700
|
+
// Build provider requirements summary
|
|
11701
|
+
const providerSummary = (dashboardConfig.providers || []).map((p) => ({
|
|
11702
|
+
type: p.type,
|
|
11703
|
+
providerClass: p.providerClass,
|
|
11704
|
+
required: p.required,
|
|
11705
|
+
usedBy: p.usedBy,
|
|
11706
|
+
}));
|
|
11707
|
+
|
|
11708
|
+
console.log(
|
|
11709
|
+
`[DashboardConfigController] Imported dashboard "${dashboardConfig.name}" (${source}) as workspace ${workspace.id}`,
|
|
11710
|
+
);
|
|
11656
11711
|
|
|
11657
11712
|
return {
|
|
11658
11713
|
success: true,
|
|
11659
|
-
|
|
11660
|
-
|
|
11714
|
+
workspace,
|
|
11715
|
+
summary: {
|
|
11716
|
+
name: dashboardConfig.name,
|
|
11717
|
+
description: dashboardConfig.description || "",
|
|
11718
|
+
author: dashboardConfig.author,
|
|
11719
|
+
widgets: installSummary,
|
|
11720
|
+
eventsWired: eventWiringSummary,
|
|
11721
|
+
providersRequired: providerSummary,
|
|
11722
|
+
themeInstalled: themeInstalled || null,
|
|
11723
|
+
},
|
|
11661
11724
|
};
|
|
11662
11725
|
}
|
|
11663
11726
|
|
|
11664
11727
|
/**
|
|
11665
|
-
*
|
|
11666
|
-
*
|
|
11667
|
-
*
|
|
11728
|
+
* Install a dashboard from the registry by package name.
|
|
11729
|
+
*
|
|
11730
|
+
* Fetches the dashboard ZIP from the registry, extracts the .dashboard.json,
|
|
11731
|
+
* validates it, and delegates to the shared import pipeline.
|
|
11668
11732
|
*
|
|
11733
|
+
* @param {BrowserWindow} win - The main window
|
|
11669
11734
|
* @param {string} appId - Application identifier
|
|
11670
|
-
* @
|
|
11735
|
+
* @param {string} packageName - Registry package name for the dashboard
|
|
11736
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance
|
|
11737
|
+
* @returns {Promise<Object>} Result with success, workspace, and summary
|
|
11671
11738
|
*/
|
|
11672
|
-
async function
|
|
11673
|
-
|
|
11674
|
-
|
|
11675
|
-
|
|
11739
|
+
async function installDashboardFromRegistry$1(
|
|
11740
|
+
win,
|
|
11741
|
+
appId,
|
|
11742
|
+
packageName,
|
|
11743
|
+
widgetRegistry = null,
|
|
11744
|
+
) {
|
|
11676
11745
|
try {
|
|
11677
|
-
|
|
11678
|
-
|
|
11679
|
-
|
|
11680
|
-
|
|
11681
|
-
|
|
11746
|
+
// 1. Look up the dashboard package in the registry
|
|
11747
|
+
const registryPkg = await getPackage(packageName);
|
|
11748
|
+
if (!registryPkg) {
|
|
11749
|
+
return {
|
|
11750
|
+
success: false,
|
|
11751
|
+
error: `Dashboard package not found in registry: ${packageName}`,
|
|
11752
|
+
};
|
|
11753
|
+
}
|
|
11754
|
+
|
|
11755
|
+
if (!registryPkg.downloadUrl) {
|
|
11756
|
+
return {
|
|
11757
|
+
success: false,
|
|
11758
|
+
error: `Dashboard package has no download URL: ${packageName}`,
|
|
11759
|
+
};
|
|
11760
|
+
}
|
|
11761
|
+
|
|
11762
|
+
// 2. Resolve the download URL and fetch the ZIP
|
|
11763
|
+
const version = registryPkg.version || "1.0.0";
|
|
11764
|
+
let downloadUrl = registryPkg.downloadUrl;
|
|
11765
|
+
downloadUrl = downloadUrl.replace("{version}", version);
|
|
11766
|
+
downloadUrl = downloadUrl.replace("{name}", packageName);
|
|
11767
|
+
|
|
11768
|
+
// Enforce HTTPS
|
|
11769
|
+
const parsedUrl = new URL(downloadUrl);
|
|
11770
|
+
if (parsedUrl.protocol !== "https:") {
|
|
11771
|
+
return {
|
|
11772
|
+
success: false,
|
|
11773
|
+
error: `Dashboard downloads must use HTTPS. Refusing: ${downloadUrl}`,
|
|
11774
|
+
};
|
|
11775
|
+
}
|
|
11776
|
+
|
|
11777
|
+
console.log(
|
|
11778
|
+
`[DashboardConfigController] Fetching dashboard from: ${downloadUrl}`,
|
|
11682
11779
|
);
|
|
11683
|
-
const workspaces = getFileContents$1(filename) || [];
|
|
11684
11780
|
|
|
11685
|
-
const
|
|
11686
|
-
|
|
11781
|
+
const response = await fetch(downloadUrl);
|
|
11782
|
+
if (!response.ok) {
|
|
11783
|
+
return {
|
|
11784
|
+
success: false,
|
|
11785
|
+
error: `Failed to download dashboard: ${response.status} ${response.statusText}`,
|
|
11786
|
+
};
|
|
11787
|
+
}
|
|
11687
11788
|
|
|
11688
|
-
const
|
|
11789
|
+
const buffer = await response.arrayBuffer();
|
|
11790
|
+
const zip = new AdmZip(Buffer.from(buffer));
|
|
11689
11791
|
|
|
11690
|
-
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
|
|
11792
|
+
// 3. Validate ZIP entries
|
|
11793
|
+
const tempDir = path$1.join(app$1.getPath("temp"), "dash-registry-import");
|
|
11794
|
+
const { validateZipEntries } = widgetRegistryExports;
|
|
11795
|
+
validateZipEntries(zip, tempDir);
|
|
11796
|
+
|
|
11797
|
+
// 4. Find and parse .dashboard.json
|
|
11798
|
+
const entries = zip.getEntries();
|
|
11799
|
+
const configEntry = entries.find((e) =>
|
|
11800
|
+
e.entryName.endsWith(".dashboard.json"),
|
|
11801
|
+
);
|
|
11802
|
+
|
|
11803
|
+
if (!configEntry) {
|
|
11804
|
+
return {
|
|
11805
|
+
success: false,
|
|
11806
|
+
error: "No .dashboard.json file found in downloaded archive",
|
|
11807
|
+
};
|
|
11808
|
+
}
|
|
11809
|
+
|
|
11810
|
+
const configJson = configEntry.getData().toString("utf-8");
|
|
11811
|
+
let dashboardConfig;
|
|
11812
|
+
try {
|
|
11813
|
+
dashboardConfig = JSON.parse(configJson);
|
|
11814
|
+
} catch (parseError) {
|
|
11815
|
+
return {
|
|
11816
|
+
success: false,
|
|
11817
|
+
error: `Invalid JSON in dashboard config: ${parseError.message}`,
|
|
11818
|
+
};
|
|
11819
|
+
}
|
|
11820
|
+
|
|
11821
|
+
// 5. Validate against schema
|
|
11822
|
+
const validation = validateDashboardConfig(dashboardConfig);
|
|
11823
|
+
if (!validation.valid) {
|
|
11824
|
+
return {
|
|
11825
|
+
success: false,
|
|
11826
|
+
error: `Invalid dashboard config: ${validation.errors.join(", ")}`,
|
|
11827
|
+
};
|
|
11828
|
+
}
|
|
11829
|
+
|
|
11830
|
+
dashboardConfig = applyDefaults(dashboardConfig);
|
|
11831
|
+
|
|
11832
|
+
// 6. Delegate to shared import pipeline
|
|
11833
|
+
return await processDashboardConfig(
|
|
11834
|
+
win,
|
|
11835
|
+
appId,
|
|
11836
|
+
dashboardConfig,
|
|
11837
|
+
widgetRegistry,
|
|
11838
|
+
{
|
|
11839
|
+
source: "registry",
|
|
11840
|
+
registryPackage: packageName,
|
|
11841
|
+
installedVersion: registryPkg.version || null,
|
|
11842
|
+
},
|
|
11843
|
+
);
|
|
11697
11844
|
} catch (error) {
|
|
11698
11845
|
console.error(
|
|
11699
|
-
"[DashboardConfigController] Error
|
|
11846
|
+
"[DashboardConfigController] Error installing dashboard from registry:",
|
|
11700
11847
|
error,
|
|
11701
11848
|
);
|
|
11702
11849
|
return {
|
|
11703
11850
|
success: false,
|
|
11704
11851
|
error: error.message,
|
|
11705
|
-
updates: [],
|
|
11706
11852
|
};
|
|
11707
11853
|
}
|
|
11708
11854
|
}
|
|
11709
11855
|
|
|
11710
11856
|
/**
|
|
11711
|
-
*
|
|
11712
|
-
*
|
|
11857
|
+
* Check compatibility of a dashboard's widget dependencies against
|
|
11858
|
+
* installed widgets and registry availability.
|
|
11713
11859
|
*
|
|
11714
|
-
* @param {
|
|
11715
|
-
* @param {
|
|
11716
|
-
* @returns {Object}
|
|
11860
|
+
* @param {Array} dashboardWidgets - Widget deps from dashboard config
|
|
11861
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance (needs getWidgets())
|
|
11862
|
+
* @returns {Promise<Object>} Compatibility report
|
|
11717
11863
|
*/
|
|
11718
|
-
function
|
|
11864
|
+
async function checkCompatibility$1(dashboardWidgets, widgetRegistry = null) {
|
|
11719
11865
|
const {
|
|
11720
|
-
|
|
11866
|
+
checkDashboardCompatibility,
|
|
11721
11867
|
} = dashboardConfigUtils$1;
|
|
11722
|
-
const {
|
|
11868
|
+
const { fetchRegistryIndex } = registryController$2;
|
|
11723
11869
|
|
|
11724
|
-
|
|
11870
|
+
const installedWidgets = widgetRegistry ? widgetRegistry.getWidgets() : [];
|
|
11871
|
+
|
|
11872
|
+
let registryPackages = [];
|
|
11725
11873
|
try {
|
|
11726
|
-
|
|
11874
|
+
const index = await fetchRegistryIndex();
|
|
11875
|
+
registryPackages = index.packages || [];
|
|
11727
11876
|
} catch (err) {
|
|
11728
11877
|
console.warn(
|
|
11729
|
-
"[DashboardConfigController] Could not
|
|
11878
|
+
"[DashboardConfigController] Could not fetch registry index for compatibility check:",
|
|
11730
11879
|
err.message,
|
|
11731
11880
|
);
|
|
11732
11881
|
}
|
|
11733
11882
|
|
|
11734
|
-
return
|
|
11883
|
+
return checkDashboardCompatibility(
|
|
11884
|
+
dashboardWidgets,
|
|
11885
|
+
installedWidgets,
|
|
11886
|
+
registryPackages,
|
|
11887
|
+
);
|
|
11735
11888
|
}
|
|
11736
11889
|
|
|
11737
11890
|
/**
|
|
11738
|
-
*
|
|
11739
|
-
*
|
|
11891
|
+
* Prepare a dashboard for publishing to the registry.
|
|
11892
|
+
*
|
|
11893
|
+
* Validates that the workspace is shareable, builds the dashboard config,
|
|
11894
|
+
* checks that all widgets exist in the registry, generates a registry
|
|
11895
|
+
* manifest, and creates a ZIP containing both the manifest and
|
|
11896
|
+
* .dashboard.json config.
|
|
11740
11897
|
*
|
|
11898
|
+
* @param {BrowserWindow} win - The main window (for save dialog)
|
|
11741
11899
|
* @param {string} appId - Application identifier
|
|
11742
|
-
* @param {number|string} workspaceId -
|
|
11743
|
-
* @param {Object}
|
|
11744
|
-
* @
|
|
11900
|
+
* @param {number|string} workspaceId - ID of the workspace to publish
|
|
11901
|
+
* @param {Object} options - Publishing options
|
|
11902
|
+
* @param {string} options.authorName - Author name
|
|
11903
|
+
* @param {string} options.authorId - Author ID
|
|
11904
|
+
* @param {string} options.description - Dashboard description
|
|
11905
|
+
* @param {string[]} options.tags - Tags
|
|
11906
|
+
* @param {string} options.icon - Icon name
|
|
11907
|
+
* @param {string} options.githubUser - GitHub user/org for registry scope
|
|
11908
|
+
* @param {string} options.category - Registry category
|
|
11909
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance
|
|
11910
|
+
* @returns {Promise<Object>} Result with success, manifest, and filePath
|
|
11745
11911
|
*/
|
|
11746
|
-
function
|
|
11912
|
+
async function prepareDashboardForPublish$1(
|
|
11913
|
+
win,
|
|
11914
|
+
appId,
|
|
11915
|
+
workspaceId,
|
|
11916
|
+
options = {},
|
|
11917
|
+
widgetRegistry = null,
|
|
11918
|
+
) {
|
|
11747
11919
|
try {
|
|
11748
|
-
const
|
|
11749
|
-
|
|
11920
|
+
const {
|
|
11921
|
+
generateRegistryManifest,
|
|
11922
|
+
} = dashboardConfigUtils$1;
|
|
11923
|
+
|
|
11924
|
+
// 1. Read workspace
|
|
11925
|
+
const filename = path$1.join(
|
|
11926
|
+
app$1.getPath("userData"),
|
|
11750
11927
|
appName$1,
|
|
11751
11928
|
appId,
|
|
11752
11929
|
configFilename,
|
|
@@ -11757,77 +11934,428 @@ function getDashboardPublishPreview$1(appId, workspaceId, widgetRegistry = null)
|
|
|
11757
11934
|
);
|
|
11758
11935
|
|
|
11759
11936
|
if (!workspace) {
|
|
11760
|
-
return {
|
|
11937
|
+
return {
|
|
11938
|
+
success: false,
|
|
11939
|
+
error: `Workspace not found: ${workspaceId}`,
|
|
11940
|
+
};
|
|
11941
|
+
}
|
|
11942
|
+
|
|
11943
|
+
// 2. Check shareable flag — imported dashboards cannot be published
|
|
11944
|
+
if (
|
|
11945
|
+
workspace._dashboardConfig &&
|
|
11946
|
+
workspace._dashboardConfig.shareable === false
|
|
11947
|
+
) {
|
|
11948
|
+
return {
|
|
11949
|
+
success: false,
|
|
11950
|
+
error:
|
|
11951
|
+
"This dashboard was imported and cannot be published. Only dashboards you created can be shared.",
|
|
11952
|
+
};
|
|
11761
11953
|
}
|
|
11762
11954
|
|
|
11763
11955
|
const layout = workspace.layout || [];
|
|
11956
|
+
|
|
11957
|
+
// 3. Build the dashboard config (reuse export logic)
|
|
11764
11958
|
const componentNames = collectComponentNames(layout);
|
|
11959
|
+
const eventWiring = extractEventWiring(layout);
|
|
11765
11960
|
const widgets = buildWidgetDependencies(componentNames, widgetRegistry);
|
|
11961
|
+
const providers = buildProviderRequirements(componentNames, widgetRegistry);
|
|
11962
|
+
|
|
11963
|
+
const dashboardConfig = applyDefaults({
|
|
11964
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
11965
|
+
name: workspace.name || workspace.label || "Dashboard",
|
|
11966
|
+
description: options.description || "",
|
|
11967
|
+
...(options.authorName
|
|
11968
|
+
? { author: { name: options.authorName, id: options.authorId || "" } }
|
|
11969
|
+
: {}),
|
|
11970
|
+
shareable: true,
|
|
11971
|
+
tags: options.tags || [],
|
|
11972
|
+
icon: options.icon || "grip",
|
|
11973
|
+
workspace: {
|
|
11974
|
+
id: workspace.id,
|
|
11975
|
+
name: workspace.name,
|
|
11976
|
+
type: workspace.type || "workspace",
|
|
11977
|
+
label: workspace.label || workspace.name,
|
|
11978
|
+
version: workspace.version || 1,
|
|
11979
|
+
layout,
|
|
11980
|
+
menuId: workspace.menuId || 1,
|
|
11981
|
+
},
|
|
11982
|
+
widgets,
|
|
11983
|
+
providers,
|
|
11984
|
+
eventWiring,
|
|
11985
|
+
});
|
|
11986
|
+
|
|
11987
|
+
// 4. Bundle theme if workspace has a themeKey
|
|
11988
|
+
if (workspace.themeKey) {
|
|
11989
|
+
try {
|
|
11990
|
+
const themeResult = themeController$1.listThemesForApplication(
|
|
11991
|
+
win,
|
|
11992
|
+
appId,
|
|
11993
|
+
);
|
|
11994
|
+
const themeData = themeResult.themes?.[workspace.themeKey];
|
|
11995
|
+
if (themeData) {
|
|
11996
|
+
dashboardConfig.theme = {
|
|
11997
|
+
key: workspace.themeKey,
|
|
11998
|
+
data: themeData,
|
|
11999
|
+
};
|
|
12000
|
+
if (themeData._registryMeta?.packageName) {
|
|
12001
|
+
dashboardConfig.theme.registryPackage =
|
|
12002
|
+
themeData._registryMeta.packageName;
|
|
12003
|
+
}
|
|
12004
|
+
}
|
|
12005
|
+
} catch (themeErr) {
|
|
12006
|
+
console.warn(
|
|
12007
|
+
"[DashboardConfigController] Could not bundle theme for publish:",
|
|
12008
|
+
themeErr.message,
|
|
12009
|
+
);
|
|
12010
|
+
}
|
|
12011
|
+
}
|
|
12012
|
+
|
|
12013
|
+
// 5. Validate the config
|
|
12014
|
+
const validation = validateDashboardConfig(dashboardConfig);
|
|
12015
|
+
if (!validation.valid) {
|
|
12016
|
+
return {
|
|
12017
|
+
success: false,
|
|
12018
|
+
error: `Generated config is invalid: ${validation.errors.join(", ")}`,
|
|
12019
|
+
};
|
|
12020
|
+
}
|
|
12021
|
+
|
|
12022
|
+
// 6. Check which widgets exist in the registry (soft warning, not blocking)
|
|
12023
|
+
const { fetchRegistryIndex } = registryController$2;
|
|
12024
|
+
let registryPackages = [];
|
|
12025
|
+
let registryCheckFailed = false;
|
|
12026
|
+
try {
|
|
12027
|
+
const index = await fetchRegistryIndex();
|
|
12028
|
+
registryPackages = index.packages || [];
|
|
12029
|
+
} catch (err) {
|
|
12030
|
+
console.warn(
|
|
12031
|
+
`[DashboardConfigController] Unable to verify registry: ${err.message}`,
|
|
12032
|
+
);
|
|
12033
|
+
registryCheckFailed = true;
|
|
12034
|
+
}
|
|
12035
|
+
|
|
12036
|
+
let missingFromRegistry = [];
|
|
12037
|
+
if (!registryCheckFailed) {
|
|
12038
|
+
const registryNames = new Set(registryPackages.map((p) => p.name));
|
|
12039
|
+
const missingWidgets = widgets.filter(
|
|
12040
|
+
(w) => w.required !== false && !registryNames.has(w.package),
|
|
12041
|
+
);
|
|
12042
|
+
const grouped = {};
|
|
12043
|
+
for (const w of missingWidgets) {
|
|
12044
|
+
if (!grouped[w.package]) grouped[w.package] = [];
|
|
12045
|
+
const widgetName = w.id.includes(".") ? w.id.split(".")[1] : w.id;
|
|
12046
|
+
if (!grouped[w.package].includes(widgetName)) {
|
|
12047
|
+
grouped[w.package].push(widgetName);
|
|
12048
|
+
}
|
|
12049
|
+
}
|
|
12050
|
+
missingFromRegistry = Object.entries(grouped).map(
|
|
12051
|
+
([pkg, widgetNames]) => ({ package: pkg, widgets: widgetNames }),
|
|
12052
|
+
);
|
|
12053
|
+
}
|
|
12054
|
+
|
|
12055
|
+
// 7. Generate registry manifest
|
|
12056
|
+
const manifest = generateRegistryManifest(dashboardConfig, {
|
|
12057
|
+
githubUser: options.githubUser || options.authorId || "",
|
|
12058
|
+
category: options.category || "general",
|
|
12059
|
+
repository: options.repository || "",
|
|
12060
|
+
});
|
|
12061
|
+
|
|
12062
|
+
// 8. Show save dialog for the publish package
|
|
12063
|
+
const sanitizedName = manifest.name;
|
|
12064
|
+
const { canceled, filePath } = await dialog.showSaveDialog(win, {
|
|
12065
|
+
title: "Save Dashboard Package for Registry",
|
|
12066
|
+
defaultPath: path$1.join(
|
|
12067
|
+
app$1.getPath("desktop"),
|
|
12068
|
+
`dashboard-${sanitizedName}-v${manifest.version}.zip`,
|
|
12069
|
+
),
|
|
12070
|
+
filters: [{ name: "ZIP Archive", extensions: ["zip"] }],
|
|
12071
|
+
});
|
|
12072
|
+
|
|
12073
|
+
if (canceled || !filePath) {
|
|
12074
|
+
return { success: false, canceled: true };
|
|
12075
|
+
}
|
|
12076
|
+
|
|
12077
|
+
// 9. Create ZIP with manifest and dashboard config
|
|
12078
|
+
const zip = new AdmZip();
|
|
12079
|
+
zip.addFile(
|
|
12080
|
+
"manifest.json",
|
|
12081
|
+
Buffer.from(JSON.stringify(manifest, null, 2), "utf-8"),
|
|
12082
|
+
);
|
|
12083
|
+
zip.addFile(
|
|
12084
|
+
`${sanitizedName}.dashboard.json`,
|
|
12085
|
+
Buffer.from(JSON.stringify(dashboardConfig, null, 2), "utf-8"),
|
|
12086
|
+
);
|
|
12087
|
+
zip.writeZip(filePath);
|
|
12088
|
+
|
|
12089
|
+
console.log(
|
|
12090
|
+
`[DashboardConfigController] Prepared publish package: ${filePath}`,
|
|
12091
|
+
);
|
|
12092
|
+
|
|
12093
|
+
// 10. Attempt to publish to registry if authenticated
|
|
12094
|
+
let registrySubmission = null;
|
|
12095
|
+
try {
|
|
12096
|
+
const { getAuthStatus } = registryAuthController$1;
|
|
12097
|
+
const { publishToRegistry } = registryApiController$2;
|
|
12098
|
+
const authStatus = getAuthStatus();
|
|
12099
|
+
|
|
12100
|
+
if (authStatus.authenticated) {
|
|
12101
|
+
console.log("[DashboardConfigController] Publishing to registry...");
|
|
12102
|
+
registrySubmission = await publishToRegistry(filePath, manifest);
|
|
12103
|
+
if (registrySubmission.success) {
|
|
12104
|
+
console.log(
|
|
12105
|
+
`[DashboardConfigController] Published to registry: ${registrySubmission.registryUrl}`,
|
|
12106
|
+
);
|
|
12107
|
+
} else {
|
|
12108
|
+
console.warn(
|
|
12109
|
+
`[DashboardConfigController] Registry publish failed: ${registrySubmission.error}`,
|
|
12110
|
+
);
|
|
12111
|
+
}
|
|
12112
|
+
} else {
|
|
12113
|
+
registrySubmission = { success: false, authRequired: true };
|
|
12114
|
+
}
|
|
12115
|
+
} catch (err) {
|
|
12116
|
+
console.warn(
|
|
12117
|
+
`[DashboardConfigController] Registry publish error: ${err.message}`,
|
|
12118
|
+
);
|
|
12119
|
+
registrySubmission = { success: false, error: err.message };
|
|
12120
|
+
}
|
|
11766
12121
|
|
|
11767
12122
|
return {
|
|
11768
12123
|
success: true,
|
|
11769
|
-
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
12124
|
+
filePath,
|
|
12125
|
+
manifest,
|
|
12126
|
+
config: dashboardConfig,
|
|
12127
|
+
warnings:
|
|
12128
|
+
missingFromRegistry.length > 0 ? missingFromRegistry : undefined,
|
|
12129
|
+
registryCheckFailed: registryCheckFailed || undefined,
|
|
12130
|
+
registrySubmission,
|
|
11773
12131
|
};
|
|
11774
12132
|
} catch (error) {
|
|
11775
12133
|
console.error(
|
|
11776
|
-
"[DashboardConfigController] Error
|
|
12134
|
+
"[DashboardConfigController] Error preparing dashboard for publish:",
|
|
11777
12135
|
error,
|
|
11778
12136
|
);
|
|
11779
|
-
return {
|
|
12137
|
+
return {
|
|
12138
|
+
success: false,
|
|
12139
|
+
error: error.message,
|
|
12140
|
+
};
|
|
11780
12141
|
}
|
|
11781
12142
|
}
|
|
11782
12143
|
|
|
11783
|
-
var dashboardConfigController$1 = {
|
|
11784
|
-
exportDashboardConfig: exportDashboardConfig$1,
|
|
11785
|
-
selectDashboardFile: selectDashboardFile$1,
|
|
11786
|
-
importDashboardConfig: importDashboardConfig$1,
|
|
11787
|
-
installDashboardFromRegistry: installDashboardFromRegistry$1,
|
|
11788
|
-
checkCompatibility: checkCompatibility$1,
|
|
11789
|
-
prepareDashboardForPublish: prepareDashboardForPublish$1,
|
|
11790
|
-
getDashboardPreview: getDashboardPreview$1,
|
|
11791
|
-
checkDashboardUpdatesForApp: checkDashboardUpdatesForApp$1,
|
|
11792
|
-
getProviderSetupManifest: getProviderSetupManifest$1,
|
|
11793
|
-
getDashboardPublishPreview: getDashboardPublishPreview$1,
|
|
11794
|
-
};
|
|
11795
|
-
|
|
11796
12144
|
/**
|
|
11797
|
-
*
|
|
12145
|
+
* Get a full preview of a dashboard package from the registry.
|
|
12146
|
+
* Combines the structured preview data with a compatibility check.
|
|
11798
12147
|
*
|
|
11799
|
-
*
|
|
11800
|
-
*
|
|
11801
|
-
*
|
|
12148
|
+
* @param {string} packageName - Registry package name
|
|
12149
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance
|
|
12150
|
+
* @returns {Promise<Object>} Preview data with compatibility report
|
|
11802
12151
|
*/
|
|
12152
|
+
async function getDashboardPreview$1(packageName, widgetRegistry = null) {
|
|
12153
|
+
const {
|
|
12154
|
+
buildDashboardPreview,
|
|
12155
|
+
checkDashboardCompatibility,
|
|
12156
|
+
} = dashboardConfigUtils$1;
|
|
12157
|
+
const { getPackage, fetchRegistryIndex } = registryController$2;
|
|
11803
12158
|
|
|
11804
|
-
const
|
|
11805
|
-
|
|
11806
|
-
|
|
11807
|
-
|
|
12159
|
+
const pkg = await getPackage(packageName);
|
|
12160
|
+
if (!pkg) {
|
|
12161
|
+
return {
|
|
12162
|
+
success: false,
|
|
12163
|
+
error: `Dashboard package not found: ${packageName}`,
|
|
12164
|
+
};
|
|
12165
|
+
}
|
|
11808
12166
|
|
|
11809
|
-
|
|
11810
|
-
// Sliding window: max 10 notifications per 60s per widget
|
|
11811
|
-
const RATE_LIMIT_WINDOW_MS = 60_000;
|
|
11812
|
-
const RATE_LIMIT_MAX = 10;
|
|
11813
|
-
const rateBuckets = new Map(); // widgetId -> [timestamp, ...]
|
|
12167
|
+
const preview = buildDashboardPreview(pkg);
|
|
11814
12168
|
|
|
11815
|
-
//
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
12169
|
+
// Get compatibility report
|
|
12170
|
+
const installedWidgets = widgetRegistry ? widgetRegistry.getWidgets() : [];
|
|
12171
|
+
let registryPackages = [];
|
|
12172
|
+
try {
|
|
12173
|
+
const index = await fetchRegistryIndex();
|
|
12174
|
+
registryPackages = index.packages || [];
|
|
12175
|
+
} catch (err) {
|
|
12176
|
+
// Non-fatal — preview still works without compatibility
|
|
12177
|
+
}
|
|
11819
12178
|
|
|
11820
|
-
|
|
11821
|
-
|
|
11822
|
-
|
|
12179
|
+
const compatibility = checkDashboardCompatibility(
|
|
12180
|
+
pkg.widgets || [],
|
|
12181
|
+
installedWidgets,
|
|
12182
|
+
registryPackages,
|
|
12183
|
+
);
|
|
11823
12184
|
|
|
11824
|
-
|
|
11825
|
-
|
|
11826
|
-
|
|
11827
|
-
|
|
11828
|
-
|
|
11829
|
-
|
|
11830
|
-
|
|
12185
|
+
return {
|
|
12186
|
+
success: true,
|
|
12187
|
+
preview,
|
|
12188
|
+
compatibility,
|
|
12189
|
+
};
|
|
12190
|
+
}
|
|
12191
|
+
|
|
12192
|
+
/**
|
|
12193
|
+
* Check installed dashboards for available updates.
|
|
12194
|
+
* Reads workspaces, finds those installed from the registry,
|
|
12195
|
+
* and compares versions against the current registry index.
|
|
12196
|
+
*
|
|
12197
|
+
* @param {string} appId - Application identifier
|
|
12198
|
+
* @returns {Promise<Object>} Result with updates array
|
|
12199
|
+
*/
|
|
12200
|
+
async function checkDashboardUpdatesForApp$1(appId) {
|
|
12201
|
+
const { checkDashboardUpdates } = dashboardConfigUtils$1;
|
|
12202
|
+
const { fetchRegistryIndex } = registryController$2;
|
|
12203
|
+
|
|
12204
|
+
try {
|
|
12205
|
+
const filename = path$1.join(
|
|
12206
|
+
app$1.getPath("userData"),
|
|
12207
|
+
appName$1,
|
|
12208
|
+
appId,
|
|
12209
|
+
configFilename,
|
|
12210
|
+
);
|
|
12211
|
+
const workspaces = getFileContents$1(filename) || [];
|
|
12212
|
+
|
|
12213
|
+
const index = await fetchRegistryIndex();
|
|
12214
|
+
const registryPackages = index.packages || [];
|
|
12215
|
+
|
|
12216
|
+
const updates = checkDashboardUpdates(workspaces, registryPackages);
|
|
12217
|
+
|
|
12218
|
+
return {
|
|
12219
|
+
success: true,
|
|
12220
|
+
updates,
|
|
12221
|
+
totalInstalled: workspaces.filter(
|
|
12222
|
+
(w) => w._dashboardConfig?.registryPackage,
|
|
12223
|
+
).length,
|
|
12224
|
+
};
|
|
12225
|
+
} catch (error) {
|
|
12226
|
+
console.error(
|
|
12227
|
+
"[DashboardConfigController] Error checking dashboard updates:",
|
|
12228
|
+
error,
|
|
12229
|
+
);
|
|
12230
|
+
return {
|
|
12231
|
+
success: false,
|
|
12232
|
+
error: error.message,
|
|
12233
|
+
updates: [],
|
|
12234
|
+
};
|
|
12235
|
+
}
|
|
12236
|
+
}
|
|
12237
|
+
|
|
12238
|
+
/**
|
|
12239
|
+
* Get a provider setup manifest for a dashboard's requirements.
|
|
12240
|
+
* Compares required providers against the user's configured providers.
|
|
12241
|
+
*
|
|
12242
|
+
* @param {string} appId - Application identifier
|
|
12243
|
+
* @param {Array} requiredProviders - Provider requirements from dashboard config
|
|
12244
|
+
* @returns {Object} Setup manifest with per-provider status
|
|
12245
|
+
*/
|
|
12246
|
+
function getProviderSetupManifest$1(appId, requiredProviders = []) {
|
|
12247
|
+
const {
|
|
12248
|
+
buildProviderSetupManifest,
|
|
12249
|
+
} = dashboardConfigUtils$1;
|
|
12250
|
+
const { listProviders } = requireProviderController();
|
|
12251
|
+
|
|
12252
|
+
let configuredProviders = [];
|
|
12253
|
+
try {
|
|
12254
|
+
configuredProviders = listProviders(null, appId) || [];
|
|
12255
|
+
} catch (err) {
|
|
12256
|
+
console.warn(
|
|
12257
|
+
"[DashboardConfigController] Could not list providers:",
|
|
12258
|
+
err.message,
|
|
12259
|
+
);
|
|
12260
|
+
}
|
|
12261
|
+
|
|
12262
|
+
return buildProviderSetupManifest(requiredProviders, configuredProviders);
|
|
12263
|
+
}
|
|
12264
|
+
|
|
12265
|
+
/**
|
|
12266
|
+
* Get a publish preview for a dashboard workspace.
|
|
12267
|
+
* Returns widget/layout info without creating a ZIP or uploading.
|
|
12268
|
+
*
|
|
12269
|
+
* @param {string} appId - Application identifier
|
|
12270
|
+
* @param {number|string} workspaceId - Workspace to preview
|
|
12271
|
+
* @param {Object} widgetRegistry - WidgetRegistry instance (optional)
|
|
12272
|
+
* @returns {Object} Preview with dashboardName, widgetCount, widgets, componentNames
|
|
12273
|
+
*/
|
|
12274
|
+
function getDashboardPublishPreview$1(appId, workspaceId, widgetRegistry = null) {
|
|
12275
|
+
try {
|
|
12276
|
+
const filename = path$1.join(
|
|
12277
|
+
app$1.getPath("userData"),
|
|
12278
|
+
appName$1,
|
|
12279
|
+
appId,
|
|
12280
|
+
configFilename,
|
|
12281
|
+
);
|
|
12282
|
+
const workspacesArray = getFileContents$1(filename);
|
|
12283
|
+
const workspace = workspacesArray.find(
|
|
12284
|
+
(w) => w.id === workspaceId || w.id === Number(workspaceId),
|
|
12285
|
+
);
|
|
12286
|
+
|
|
12287
|
+
if (!workspace) {
|
|
12288
|
+
return { success: false, error: `Workspace not found: ${workspaceId}` };
|
|
12289
|
+
}
|
|
12290
|
+
|
|
12291
|
+
const layout = workspace.layout || [];
|
|
12292
|
+
const componentNames = collectComponentNames(layout);
|
|
12293
|
+
const widgets = buildWidgetDependencies(componentNames, widgetRegistry);
|
|
12294
|
+
|
|
12295
|
+
return {
|
|
12296
|
+
success: true,
|
|
12297
|
+
dashboardName: workspace.name || workspace.label || "Dashboard",
|
|
12298
|
+
widgetCount: componentNames.length,
|
|
12299
|
+
widgets: widgets.map((w) => ({ name: w.name, package: w.package })),
|
|
12300
|
+
componentNames: [...componentNames],
|
|
12301
|
+
};
|
|
12302
|
+
} catch (error) {
|
|
12303
|
+
console.error(
|
|
12304
|
+
"[DashboardConfigController] Error getting publish preview:",
|
|
12305
|
+
error,
|
|
12306
|
+
);
|
|
12307
|
+
return { success: false, error: error.message };
|
|
12308
|
+
}
|
|
12309
|
+
}
|
|
12310
|
+
|
|
12311
|
+
var dashboardConfigController$1 = {
|
|
12312
|
+
exportDashboardConfig: exportDashboardConfig$1,
|
|
12313
|
+
selectDashboardFile: selectDashboardFile$1,
|
|
12314
|
+
importDashboardConfig: importDashboardConfig$1,
|
|
12315
|
+
installDashboardFromRegistry: installDashboardFromRegistry$1,
|
|
12316
|
+
checkCompatibility: checkCompatibility$1,
|
|
12317
|
+
prepareDashboardForPublish: prepareDashboardForPublish$1,
|
|
12318
|
+
getDashboardPreview: getDashboardPreview$1,
|
|
12319
|
+
checkDashboardUpdatesForApp: checkDashboardUpdatesForApp$1,
|
|
12320
|
+
getProviderSetupManifest: getProviderSetupManifest$1,
|
|
12321
|
+
getDashboardPublishPreview: getDashboardPublishPreview$1,
|
|
12322
|
+
};
|
|
12323
|
+
|
|
12324
|
+
/**
|
|
12325
|
+
* notificationController.js
|
|
12326
|
+
*
|
|
12327
|
+
* Main process controller for OS-level notifications.
|
|
12328
|
+
* Manages preferences (electron-store), rate limiting, deduplication,
|
|
12329
|
+
* and dispatching native Notification instances.
|
|
12330
|
+
*/
|
|
12331
|
+
|
|
12332
|
+
const { Notification } = require$$0$1;
|
|
12333
|
+
const Store = require$$1;
|
|
12334
|
+
|
|
12335
|
+
const store$1 = new Store({ name: "dash-notifications" });
|
|
12336
|
+
|
|
12337
|
+
// --- Rate limiting ---
|
|
12338
|
+
// Sliding window: max 10 notifications per 60s per widget
|
|
12339
|
+
const RATE_LIMIT_WINDOW_MS = 60_000;
|
|
12340
|
+
const RATE_LIMIT_MAX = 10;
|
|
12341
|
+
const rateBuckets = new Map(); // widgetId -> [timestamp, ...]
|
|
12342
|
+
|
|
12343
|
+
// --- Deduplication ---
|
|
12344
|
+
// Same (widgetName, type, title, body) within 5s is dropped
|
|
12345
|
+
const DEDUP_WINDOW_MS = 5_000;
|
|
12346
|
+
const recentNotifications = new Map(); // dedup key -> timestamp
|
|
12347
|
+
|
|
12348
|
+
function getDedupKey(payload) {
|
|
12349
|
+
return `${payload.widgetName}:${payload.type}:${payload.title}:${payload.body}`;
|
|
12350
|
+
}
|
|
12351
|
+
|
|
12352
|
+
function isRateLimited(widgetId) {
|
|
12353
|
+
const now = Date.now();
|
|
12354
|
+
let timestamps = rateBuckets.get(widgetId) || [];
|
|
12355
|
+
// Prune old entries
|
|
12356
|
+
timestamps = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
|
|
12357
|
+
rateBuckets.set(widgetId, timestamps);
|
|
12358
|
+
return timestamps.length >= RATE_LIMIT_MAX;
|
|
11831
12359
|
}
|
|
11832
12360
|
|
|
11833
12361
|
function recordNotification(widgetId) {
|
|
@@ -12038,348 +12566,6 @@ const notificationController$2 = {
|
|
|
12038
12566
|
|
|
12039
12567
|
var notificationController_1 = notificationController$2;
|
|
12040
12568
|
|
|
12041
|
-
/**
|
|
12042
|
-
* themeRegistryController.js
|
|
12043
|
-
*
|
|
12044
|
-
* Handles publishing themes to and installing themes from the Dash registry.
|
|
12045
|
-
* Mirrors dashboardConfigController patterns for ZIP creation, manifest generation,
|
|
12046
|
-
* and registry interaction.
|
|
12047
|
-
*/
|
|
12048
|
-
const path$1 = require$$1$1;
|
|
12049
|
-
const { app: app$1, dialog } = require$$0$1;
|
|
12050
|
-
const AdmZip = require$$3$3;
|
|
12051
|
-
|
|
12052
|
-
const themeController$1 = themeController_1;
|
|
12053
|
-
const registryController$1 = registryController$2;
|
|
12054
|
-
const registryApiController$1 = registryApiController$2;
|
|
12055
|
-
const { getAuthStatus } = registryAuthController$1;
|
|
12056
|
-
|
|
12057
|
-
/**
|
|
12058
|
-
* Sanitize a name for use as a filename (lowercase, hyphens only).
|
|
12059
|
-
*/
|
|
12060
|
-
function sanitizeName(name) {
|
|
12061
|
-
return (name || "theme")
|
|
12062
|
-
.toLowerCase()
|
|
12063
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
12064
|
-
.replace(/^-|-$/g, "");
|
|
12065
|
-
}
|
|
12066
|
-
|
|
12067
|
-
/**
|
|
12068
|
-
* Generate a registry manifest for a theme package.
|
|
12069
|
-
*
|
|
12070
|
-
* @param {Object} themeData - The raw theme object
|
|
12071
|
-
* @param {string} themeKey - The theme key/name
|
|
12072
|
-
* @param {Object} options - Publish options { authorName, description, tags, scope }
|
|
12073
|
-
* @returns {Object} Registry manifest
|
|
12074
|
-
*/
|
|
12075
|
-
function generateThemeRegistryManifest(themeData, themeKey, options = {}) {
|
|
12076
|
-
const sanitizedName = sanitizeName(themeKey);
|
|
12077
|
-
const colors = extractColors(themeData);
|
|
12078
|
-
|
|
12079
|
-
return {
|
|
12080
|
-
scope: options.scope || "",
|
|
12081
|
-
name: sanitizedName,
|
|
12082
|
-
displayName: themeKey,
|
|
12083
|
-
author: options.authorName || "",
|
|
12084
|
-
description: options.description || "",
|
|
12085
|
-
version: "1.0.0",
|
|
12086
|
-
type: "theme",
|
|
12087
|
-
category: "general",
|
|
12088
|
-
tags: options.tags || [],
|
|
12089
|
-
icon: "palette",
|
|
12090
|
-
colors,
|
|
12091
|
-
appOrigin: options.appOrigin || "",
|
|
12092
|
-
publishedAt: new Date().toISOString(),
|
|
12093
|
-
};
|
|
12094
|
-
}
|
|
12095
|
-
|
|
12096
|
-
/**
|
|
12097
|
-
* Extract primary/secondary/tertiary/neutral colors from a theme object.
|
|
12098
|
-
* Theme objects store colors in various structures; this normalizes them.
|
|
12099
|
-
*/
|
|
12100
|
-
function extractColors(themeData) {
|
|
12101
|
-
const colors = {
|
|
12102
|
-
primary: "",
|
|
12103
|
-
secondary: "",
|
|
12104
|
-
tertiary: "",
|
|
12105
|
-
neutral: "",
|
|
12106
|
-
};
|
|
12107
|
-
|
|
12108
|
-
if (!themeData) return colors;
|
|
12109
|
-
|
|
12110
|
-
// Direct color fields
|
|
12111
|
-
if (themeData.primary) colors.primary = themeData.primary;
|
|
12112
|
-
if (themeData.secondary) colors.secondary = themeData.secondary;
|
|
12113
|
-
if (themeData.tertiary) colors.tertiary = themeData.tertiary;
|
|
12114
|
-
if (themeData.neutral) colors.neutral = themeData.neutral;
|
|
12115
|
-
|
|
12116
|
-
// Nested under "colors" key
|
|
12117
|
-
if (themeData.colors) {
|
|
12118
|
-
if (themeData.colors.primary) colors.primary = themeData.colors.primary;
|
|
12119
|
-
if (themeData.colors.secondary)
|
|
12120
|
-
colors.secondary = themeData.colors.secondary;
|
|
12121
|
-
if (themeData.colors.tertiary) colors.tertiary = themeData.colors.tertiary;
|
|
12122
|
-
if (themeData.colors.neutral) colors.neutral = themeData.colors.neutral;
|
|
12123
|
-
}
|
|
12124
|
-
|
|
12125
|
-
return colors;
|
|
12126
|
-
}
|
|
12127
|
-
|
|
12128
|
-
/**
|
|
12129
|
-
* Prepare a theme for publishing to the registry.
|
|
12130
|
-
*
|
|
12131
|
-
* Reads the theme from themes.json, generates a manifest, creates a ZIP,
|
|
12132
|
-
* and publishes via the registry API.
|
|
12133
|
-
*
|
|
12134
|
-
* @param {BrowserWindow} win - The sender window
|
|
12135
|
-
* @param {string} appId - Application identifier
|
|
12136
|
-
* @param {string} themeKey - Key of the theme to publish
|
|
12137
|
-
* @param {Object} options - { authorName, description, tags }
|
|
12138
|
-
* @returns {Object} Result with success, manifest, registryResult
|
|
12139
|
-
*/
|
|
12140
|
-
async function prepareThemeForPublish$1(win, appId, themeKey, options = {}) {
|
|
12141
|
-
try {
|
|
12142
|
-
// Read the theme data
|
|
12143
|
-
const themesResult = themeController$1.listThemesForApplication(win, appId);
|
|
12144
|
-
if (themesResult.error) {
|
|
12145
|
-
return { success: false, error: "Failed to read themes: " + themesResult.message };
|
|
12146
|
-
}
|
|
12147
|
-
|
|
12148
|
-
const themeData = themesResult.themes[themeKey];
|
|
12149
|
-
if (!themeData) {
|
|
12150
|
-
return { success: false, error: `Theme "${themeKey}" not found` };
|
|
12151
|
-
}
|
|
12152
|
-
|
|
12153
|
-
// Get auth status for scope
|
|
12154
|
-
const auth = getAuthStatus();
|
|
12155
|
-
const scope = auth.profile?.username || options.scope || "";
|
|
12156
|
-
if (!scope) {
|
|
12157
|
-
return {
|
|
12158
|
-
success: false,
|
|
12159
|
-
error: "Not authenticated with registry",
|
|
12160
|
-
authRequired: true,
|
|
12161
|
-
};
|
|
12162
|
-
}
|
|
12163
|
-
|
|
12164
|
-
// Generate manifest
|
|
12165
|
-
const manifest = generateThemeRegistryManifest(themeData, themeKey, {
|
|
12166
|
-
...options,
|
|
12167
|
-
scope,
|
|
12168
|
-
appOrigin: appId,
|
|
12169
|
-
});
|
|
12170
|
-
|
|
12171
|
-
// Validate colors
|
|
12172
|
-
if (!manifest.colors.primary || !manifest.colors.secondary || !manifest.colors.tertiary) {
|
|
12173
|
-
return {
|
|
12174
|
-
success: false,
|
|
12175
|
-
error: "Theme must have primary, secondary, and tertiary colors defined",
|
|
12176
|
-
};
|
|
12177
|
-
}
|
|
12178
|
-
|
|
12179
|
-
// Show save dialog
|
|
12180
|
-
const sanitizedName = sanitizeName(themeKey);
|
|
12181
|
-
const defaultFilename = `theme-${sanitizedName}-v${manifest.version}.zip`;
|
|
12182
|
-
|
|
12183
|
-
const saveResult = await dialog.showSaveDialog(win, {
|
|
12184
|
-
title: "Save Theme Package",
|
|
12185
|
-
defaultPath: defaultFilename,
|
|
12186
|
-
filters: [{ name: "ZIP Files", extensions: ["zip"] }],
|
|
12187
|
-
});
|
|
12188
|
-
|
|
12189
|
-
if (saveResult.canceled || !saveResult.filePath) {
|
|
12190
|
-
return { success: false, error: "Save canceled" };
|
|
12191
|
-
}
|
|
12192
|
-
|
|
12193
|
-
const filePath = saveResult.filePath;
|
|
12194
|
-
|
|
12195
|
-
// Create ZIP with manifest.json + {name}.theme.json
|
|
12196
|
-
const zip = new AdmZip();
|
|
12197
|
-
zip.addFile(
|
|
12198
|
-
"manifest.json",
|
|
12199
|
-
Buffer.from(JSON.stringify(manifest, null, 2), "utf-8"),
|
|
12200
|
-
);
|
|
12201
|
-
zip.addFile(
|
|
12202
|
-
`${sanitizedName}.theme.json`,
|
|
12203
|
-
Buffer.from(JSON.stringify(themeData, null, 2), "utf-8"),
|
|
12204
|
-
);
|
|
12205
|
-
zip.writeZip(filePath);
|
|
12206
|
-
|
|
12207
|
-
console.log("[ThemeRegistryController] ZIP created at:", filePath);
|
|
12208
|
-
|
|
12209
|
-
// Attempt to publish to registry
|
|
12210
|
-
let registryResult = null;
|
|
12211
|
-
if (auth.authenticated) {
|
|
12212
|
-
registryResult = await registryApiController$1.publishToRegistry(
|
|
12213
|
-
filePath,
|
|
12214
|
-
manifest,
|
|
12215
|
-
);
|
|
12216
|
-
console.log("[ThemeRegistryController] Registry publish result:", registryResult);
|
|
12217
|
-
}
|
|
12218
|
-
|
|
12219
|
-
return {
|
|
12220
|
-
success: true,
|
|
12221
|
-
manifest,
|
|
12222
|
-
filePath,
|
|
12223
|
-
registryResult,
|
|
12224
|
-
};
|
|
12225
|
-
} catch (err) {
|
|
12226
|
-
console.error("[ThemeRegistryController] Error preparing theme for publish:", err);
|
|
12227
|
-
return { success: false, error: err.message };
|
|
12228
|
-
}
|
|
12229
|
-
}
|
|
12230
|
-
|
|
12231
|
-
/**
|
|
12232
|
-
* Install a theme from the registry.
|
|
12233
|
-
*
|
|
12234
|
-
* Looks up the theme package, downloads the ZIP, extracts the .theme.json,
|
|
12235
|
-
* and saves it via themeController.
|
|
12236
|
-
*
|
|
12237
|
-
* @param {BrowserWindow} win - The sender window
|
|
12238
|
-
* @param {string} appId - Application identifier
|
|
12239
|
-
* @param {string} packageName - Registry package name (e.g., "username/ocean-depth")
|
|
12240
|
-
* @returns {Object} Result with success, themeKey, theme
|
|
12241
|
-
*/
|
|
12242
|
-
async function installThemeFromRegistry$1(win, appId, packageName) {
|
|
12243
|
-
try {
|
|
12244
|
-
// Look up the package
|
|
12245
|
-
const pkg = await registryController$1.getPackage(packageName);
|
|
12246
|
-
if (!pkg) {
|
|
12247
|
-
return { success: false, error: `Theme package "${packageName}" not found in registry` };
|
|
12248
|
-
}
|
|
12249
|
-
|
|
12250
|
-
// Resolve download URL
|
|
12251
|
-
let downloadUrl = pkg.downloadUrl;
|
|
12252
|
-
if (!downloadUrl) {
|
|
12253
|
-
return { success: false, error: "Package has no download URL" };
|
|
12254
|
-
}
|
|
12255
|
-
|
|
12256
|
-
// Resolve template variables
|
|
12257
|
-
downloadUrl = downloadUrl
|
|
12258
|
-
.replace("{version}", pkg.version || "1.0.0")
|
|
12259
|
-
.replace("{name}", pkg.name || "");
|
|
12260
|
-
|
|
12261
|
-
// Enforce HTTPS
|
|
12262
|
-
if (!downloadUrl.startsWith("https://")) {
|
|
12263
|
-
return { success: false, error: "Download URL must use HTTPS" };
|
|
12264
|
-
}
|
|
12265
|
-
|
|
12266
|
-
console.log("[ThemeRegistryController] Downloading theme from:", downloadUrl);
|
|
12267
|
-
|
|
12268
|
-
// Download the ZIP
|
|
12269
|
-
const response = await fetch(downloadUrl);
|
|
12270
|
-
if (!response.ok) {
|
|
12271
|
-
return {
|
|
12272
|
-
success: false,
|
|
12273
|
-
error: `Failed to download theme: ${response.status} ${response.statusText}`,
|
|
12274
|
-
};
|
|
12275
|
-
}
|
|
12276
|
-
|
|
12277
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
12278
|
-
const zipBuffer = Buffer.from(arrayBuffer);
|
|
12279
|
-
|
|
12280
|
-
// Extract .theme.json from ZIP
|
|
12281
|
-
const zip = new AdmZip(zipBuffer);
|
|
12282
|
-
const entries = zip.getEntries();
|
|
12283
|
-
|
|
12284
|
-
const themeEntry = entries.find((entry) =>
|
|
12285
|
-
entry.entryName.endsWith(".theme.json"),
|
|
12286
|
-
);
|
|
12287
|
-
|
|
12288
|
-
if (!themeEntry) {
|
|
12289
|
-
return { success: false, error: "ZIP does not contain a .theme.json file" };
|
|
12290
|
-
}
|
|
12291
|
-
|
|
12292
|
-
// Validate entry path (security: prevent path traversal)
|
|
12293
|
-
if (themeEntry.entryName.includes("..") || path$1.isAbsolute(themeEntry.entryName)) {
|
|
12294
|
-
return { success: false, error: "Invalid file path in ZIP" };
|
|
12295
|
-
}
|
|
12296
|
-
|
|
12297
|
-
// Parse theme data
|
|
12298
|
-
const themeJson = themeEntry.getData().toString("utf-8");
|
|
12299
|
-
let themeData;
|
|
12300
|
-
try {
|
|
12301
|
-
themeData = JSON.parse(themeJson);
|
|
12302
|
-
} catch (parseErr) {
|
|
12303
|
-
return { success: false, error: "Invalid JSON in theme file: " + parseErr.message };
|
|
12304
|
-
}
|
|
12305
|
-
|
|
12306
|
-
// Add registry metadata
|
|
12307
|
-
themeData._registryMeta = {
|
|
12308
|
-
source: "registry",
|
|
12309
|
-
packageName,
|
|
12310
|
-
installedAt: new Date().toISOString(),
|
|
12311
|
-
};
|
|
12312
|
-
|
|
12313
|
-
// Determine theme key from package display name or name
|
|
12314
|
-
const themeKey = pkg.displayName || pkg.name;
|
|
12315
|
-
|
|
12316
|
-
// Save via themeController
|
|
12317
|
-
const saveResult = themeController$1.saveThemeForApplication(
|
|
12318
|
-
win,
|
|
12319
|
-
appId,
|
|
12320
|
-
themeKey,
|
|
12321
|
-
themeData,
|
|
12322
|
-
);
|
|
12323
|
-
|
|
12324
|
-
if (saveResult.error) {
|
|
12325
|
-
return { success: false, error: "Failed to save theme: " + saveResult.message };
|
|
12326
|
-
}
|
|
12327
|
-
|
|
12328
|
-
console.log("[ThemeRegistryController] Theme installed:", themeKey);
|
|
12329
|
-
|
|
12330
|
-
return {
|
|
12331
|
-
success: true,
|
|
12332
|
-
themeKey,
|
|
12333
|
-
theme: themeData,
|
|
12334
|
-
themes: saveResult.themes,
|
|
12335
|
-
};
|
|
12336
|
-
} catch (err) {
|
|
12337
|
-
console.error("[ThemeRegistryController] Error installing theme:", err);
|
|
12338
|
-
return { success: false, error: err.message };
|
|
12339
|
-
}
|
|
12340
|
-
}
|
|
12341
|
-
|
|
12342
|
-
/**
|
|
12343
|
-
* Get a preview of theme data for the publish modal.
|
|
12344
|
-
*
|
|
12345
|
-
* @param {string} appId - Application identifier
|
|
12346
|
-
* @param {string} themeKey - Theme key
|
|
12347
|
-
* @returns {Object} Preview data with theme name, colors, etc.
|
|
12348
|
-
*/
|
|
12349
|
-
function getThemePublishPreview$1(appId, themeKey) {
|
|
12350
|
-
try {
|
|
12351
|
-
const themesResult = themeController$1.listThemesForApplication(null, appId);
|
|
12352
|
-
if (themesResult.error) {
|
|
12353
|
-
return { success: false, error: "Failed to read themes: " + themesResult.message };
|
|
12354
|
-
}
|
|
12355
|
-
|
|
12356
|
-
const themeData = themesResult.themes[themeKey];
|
|
12357
|
-
if (!themeData) {
|
|
12358
|
-
return { success: false, error: `Theme "${themeKey}" not found` };
|
|
12359
|
-
}
|
|
12360
|
-
|
|
12361
|
-
const colors = extractColors(themeData);
|
|
12362
|
-
|
|
12363
|
-
return {
|
|
12364
|
-
success: true,
|
|
12365
|
-
themeName: themeKey,
|
|
12366
|
-
colors,
|
|
12367
|
-
hasRegistryMeta: !!themeData._registryMeta,
|
|
12368
|
-
};
|
|
12369
|
-
} catch (err) {
|
|
12370
|
-
console.error("[ThemeRegistryController] Error getting preview:", err);
|
|
12371
|
-
return { success: false, error: err.message };
|
|
12372
|
-
}
|
|
12373
|
-
}
|
|
12374
|
-
|
|
12375
|
-
var themeRegistryController$1 = {
|
|
12376
|
-
prepareThemeForPublish: prepareThemeForPublish$1,
|
|
12377
|
-
installThemeFromRegistry: installThemeFromRegistry$1,
|
|
12378
|
-
getThemePublishPreview: getThemePublishPreview$1,
|
|
12379
|
-
generateThemeRegistryManifest,
|
|
12380
|
-
extractColors,
|
|
12381
|
-
};
|
|
12382
|
-
|
|
12383
12569
|
/**
|
|
12384
12570
|
* clientFactories.js
|
|
12385
12571
|
*
|