@growthub/cli 0.9.10 → 0.9.11
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/README.md +1 -1
- package/assets/worker-kits/creative-strategist-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-agency-portal-starter-v1/kit.json +4 -1
- package/assets/worker-kits/growthub-ai-website-cloner-v1/kit.json +6 -3
- package/assets/worker-kits/growthub-creative-video-pipeline-v1/kit.json +4 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +4 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integration-entities/route.js +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +284 -15
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +5 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +4 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +507 -34
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/growthub-connection-normalizer.js +12 -16
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/index.js +61 -11
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/domain/integrations.js +31 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +54 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-email-marketing-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-geo-seo-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-hyperframes-studio-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-marketing-skills-v1/kit.json +6 -3
- package/assets/worker-kits/growthub-open-higgsfield-studio-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-open-montage-studio-v1/kit.json +6 -3
- package/assets/worker-kits/growthub-postiz-social-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-twenty-crm-v1/kit.json +6 -3
- package/assets/worker-kits/growthub-video-use-studio-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-zernio-social-v1/kit.json +5 -2
- package/dist/index.js +1750 -433
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
5
5
|
import {
|
|
6
6
|
BarChart3,
|
|
7
7
|
Bolt,
|
|
8
|
+
Check,
|
|
8
9
|
ChevronDown,
|
|
9
10
|
Code2,
|
|
10
11
|
Columns3,
|
|
@@ -33,7 +34,6 @@ import {
|
|
|
33
34
|
Settings,
|
|
34
35
|
Sigma,
|
|
35
36
|
SlidersHorizontal,
|
|
36
|
-
Sparkles,
|
|
37
37
|
Table2,
|
|
38
38
|
Trash2,
|
|
39
39
|
Type,
|
|
@@ -63,6 +63,25 @@ const DEFAULT_FILTER_OP = "and";
|
|
|
63
63
|
const DEFAULT_FILTER_OPERATOR = "contains";
|
|
64
64
|
const DEFAULT_SORT_DIRECTION = "asc";
|
|
65
65
|
const SUB_PANEL_ROOT = "root";
|
|
66
|
+
const MANAGED_INTEGRATION_SOURCE_TYPE = "managed-integrations";
|
|
67
|
+
const CUSTOM_API_SOURCE_TYPE = "custom-api-webhooks";
|
|
68
|
+
|
|
69
|
+
const SOURCE_TYPE_OBJECTS = [
|
|
70
|
+
{
|
|
71
|
+
id: MANAGED_INTEGRATION_SOURCE_TYPE,
|
|
72
|
+
label: "Managed Integrations",
|
|
73
|
+
authority: "Growthub Bridge",
|
|
74
|
+
description: "Bridge or BYO adapters resolve metadata server-side."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: CUSTOM_API_SOURCE_TYPE,
|
|
78
|
+
label: "Custom APIs/Webhooks",
|
|
79
|
+
authority: "Custom endpoint",
|
|
80
|
+
description: "Reference a governed endpoint object without storing credentials in widget config."
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const ENTITY_REFERENCE_FIELD_IDS = ["id", "entityId"];
|
|
66
85
|
|
|
67
86
|
const CHART_TYPE_LABELS = {
|
|
68
87
|
"bar-vertical": "Vertical Bar",
|
|
@@ -622,11 +641,28 @@ function getChartStyle(widget) {
|
|
|
622
641
|
|
|
623
642
|
function summarizeSource(widget) {
|
|
624
643
|
const binding = widget?.config?.binding;
|
|
625
|
-
if (binding?.mode === "integration"
|
|
644
|
+
if (binding?.mode === "integration") {
|
|
645
|
+
const source = binding.source || "Integration";
|
|
646
|
+
if (binding.entityLabel) return `${source} · ${binding.entityLabel}`;
|
|
647
|
+
if (binding.entityId) return `${source} · ${binding.entityId}`;
|
|
648
|
+
return source;
|
|
649
|
+
}
|
|
626
650
|
if (widget?.config?.source) return widget.config.source;
|
|
627
651
|
return "Static";
|
|
628
652
|
}
|
|
629
653
|
|
|
654
|
+
function summarizeSourceType(binding) {
|
|
655
|
+
if (binding?.sourceType === CUSTOM_API_SOURCE_TYPE) return "Custom APIs/Webhooks";
|
|
656
|
+
if (binding?.mode === "integration" || binding?.sourceType === MANAGED_INTEGRATION_SOURCE_TYPE) return "Managed Integrations";
|
|
657
|
+
return "Static data";
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function resolveBindingSourceType(binding) {
|
|
661
|
+
if (binding?.sourceType) return binding.sourceType;
|
|
662
|
+
if (binding?.mode === "integration") return MANAGED_INTEGRATION_SOURCE_TYPE;
|
|
663
|
+
return "static";
|
|
664
|
+
}
|
|
665
|
+
|
|
630
666
|
function summarizeFields(widget) {
|
|
631
667
|
const total = getColumnList(widget).length;
|
|
632
668
|
const hidden = getHiddenColumnSet(widget).size;
|
|
@@ -655,6 +691,142 @@ function describeIntegrationLane(integration) {
|
|
|
655
691
|
return integration?.lane === "data-source" ? "Data Sources" : "Workspace Tools";
|
|
656
692
|
}
|
|
657
693
|
|
|
694
|
+
function flattenIntegrationSettings(integrationSettings) {
|
|
695
|
+
const grouped = integrationSettings?.integrations || integrationSettings || {};
|
|
696
|
+
const runtime = [
|
|
697
|
+
...(Array.isArray(grouped.dataSources) ? grouped.dataSources : []),
|
|
698
|
+
...(Array.isArray(grouped.workspaceIntegrations) ? grouped.workspaceIntegrations : [])
|
|
699
|
+
];
|
|
700
|
+
const byId = new Map();
|
|
701
|
+
for (const item of [...governedWorkspaceIntegrationCatalog, ...runtime]) {
|
|
702
|
+
if (item?.id) byId.set(item.id, { ...byId.get(item.id), ...item });
|
|
703
|
+
}
|
|
704
|
+
return Array.from(byId.values());
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function getFilterFieldOptions(widget, entities = []) {
|
|
708
|
+
const fields = new Set(getColumnList(widget));
|
|
709
|
+
const binding = widget?.config?.binding || {};
|
|
710
|
+
if (binding.mode === "integration") {
|
|
711
|
+
["id", "label", "secondaryLabel", "entityType", "provider", "lane", "status"].forEach((field) => fields.add(field));
|
|
712
|
+
fields.add("provider");
|
|
713
|
+
fields.add("lane");
|
|
714
|
+
for (const entity of entities) {
|
|
715
|
+
if (entity?.metadata && typeof entity.metadata === "object") {
|
|
716
|
+
Object.keys(entity.metadata).forEach((field) => fields.add(field));
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (binding.sourceType === CUSTOM_API_SOURCE_TYPE) {
|
|
721
|
+
const customFields = Array.isArray(binding.fields) ? binding.fields : ["entityId", "status", "createdAt"];
|
|
722
|
+
customFields.forEach((field) => fields.add(field));
|
|
723
|
+
}
|
|
724
|
+
return Array.from(fields).filter(Boolean);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function getEntityFieldValue(entity, fieldId) {
|
|
728
|
+
if (!entity || !fieldId) return "";
|
|
729
|
+
if (fieldId === "id" || fieldId === "entityId") return entity.id || "";
|
|
730
|
+
if (fieldId === "label" || fieldId === "name") return entity.label || "";
|
|
731
|
+
if (fieldId === "secondaryLabel") return entity.secondaryLabel || "";
|
|
732
|
+
if (fieldId === "entityType") return entity.entityType || "";
|
|
733
|
+
if (fieldId === "provider") return entity.provider || "";
|
|
734
|
+
if (fieldId === "lane") return entity.lane || "";
|
|
735
|
+
if (fieldId === "status") return entity.status || "";
|
|
736
|
+
if (entity.metadata && typeof entity.metadata === "object" && entity.metadata[fieldId] !== undefined) {
|
|
737
|
+
return String(entity.metadata[fieldId]);
|
|
738
|
+
}
|
|
739
|
+
return "";
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function getEntityFieldChoices(entities) {
|
|
743
|
+
const fields = new Map();
|
|
744
|
+
const add = (id, label) => {
|
|
745
|
+
if (id && !fields.has(id)) fields.set(id, { id, label });
|
|
746
|
+
};
|
|
747
|
+
add("id", "Stable ID");
|
|
748
|
+
add("label", "Primary label");
|
|
749
|
+
add("secondaryLabel", "Secondary label");
|
|
750
|
+
add("entityType", "Entity type");
|
|
751
|
+
add("provider", "Provider");
|
|
752
|
+
add("lane", "Lane");
|
|
753
|
+
add("status", "Status");
|
|
754
|
+
for (const entity of entities) {
|
|
755
|
+
if (entity?.metadata && typeof entity.metadata === "object") {
|
|
756
|
+
Object.keys(entity.metadata).forEach((field) => add(field, field));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return Array.from(fields.values());
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function getFilterFieldChoices(widget, entities = []) {
|
|
763
|
+
const binding = widget?.config?.binding || {};
|
|
764
|
+
if (binding.mode === "integration" && entities.length) return getEntityFieldChoices(entities);
|
|
765
|
+
return getFilterFieldOptions(widget, entities).map((id) => ({ id, label: id }));
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function getEntityValueChoices(entities, fieldId) {
|
|
769
|
+
const seen = new Map();
|
|
770
|
+
for (const entity of entities) {
|
|
771
|
+
const value = getEntityFieldValue(entity, fieldId);
|
|
772
|
+
if (!value || seen.has(value)) continue;
|
|
773
|
+
const label = fieldId === "id" || fieldId === "entityId"
|
|
774
|
+
? `${entity.label || value} · ${value}`
|
|
775
|
+
: value;
|
|
776
|
+
seen.set(value, { value, label, entity });
|
|
777
|
+
}
|
|
778
|
+
return Array.from(seen.values());
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function findEntityByFieldValue(entities, fieldId, value) {
|
|
782
|
+
if (!value) return null;
|
|
783
|
+
return entities.find((entity) => getEntityFieldValue(entity, fieldId) === value) || null;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function updateWidgetEntityBinding(widget, entity) {
|
|
787
|
+
const binding = widget.config?.binding || {};
|
|
788
|
+
const existingFilter = widget.config?.filter;
|
|
789
|
+
const existingClauses = Array.isArray(existingFilter?.clauses) ? existingFilter.clauses : [];
|
|
790
|
+
|
|
791
|
+
if (!entity) {
|
|
792
|
+
const { entityId, entityType, entityLabel, ...restBinding } = binding;
|
|
793
|
+
const cleanedClauses = existingClauses.filter(
|
|
794
|
+
(clause) => !(ENTITY_REFERENCE_FIELD_IDS.includes(clause.fieldId) && clause.operator === "eq")
|
|
795
|
+
);
|
|
796
|
+
return {
|
|
797
|
+
...widget.config,
|
|
798
|
+
binding: restBinding,
|
|
799
|
+
filter: { op: existingFilter?.op || DEFAULT_FILTER_OP, clauses: cleanedClauses }
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const entityClause = { fieldId: "id", operator: "eq", value: entity.id };
|
|
804
|
+
const otherClauses = existingClauses.filter(
|
|
805
|
+
(clause) => !(ENTITY_REFERENCE_FIELD_IDS.includes(clause.fieldId) && clause.operator === "eq")
|
|
806
|
+
);
|
|
807
|
+
const nextBinding = {
|
|
808
|
+
...binding,
|
|
809
|
+
entityId: entity.id,
|
|
810
|
+
entityLabel: entity.label
|
|
811
|
+
};
|
|
812
|
+
if (entity.entityType) nextBinding.entityType = entity.entityType;
|
|
813
|
+
else delete nextBinding.entityType;
|
|
814
|
+
return {
|
|
815
|
+
...widget.config,
|
|
816
|
+
source: binding.source || entity.label,
|
|
817
|
+
binding: nextBinding,
|
|
818
|
+
filter: { op: existingFilter?.op || DEFAULT_FILTER_OP, clauses: [entityClause, ...otherClauses] }
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function resolveChartColor(style, branding) {
|
|
823
|
+
if (style?.colors === "manual" && style.manualColor) return style.manualColor;
|
|
824
|
+
if (style?.colors === "brand-local") return branding?.accent || "#3f68ff";
|
|
825
|
+
if (style?.colors === "brand-bridge") return branding?.bridgeAccent || branding?.accent || "#3f68ff";
|
|
826
|
+
if (style?.colors === "accent") return "#38bdf8";
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
|
|
658
830
|
const NORMALIZED_TEMPLATES = DASHBOARD_TEMPLATES.map((template) => ({
|
|
659
831
|
...normalizeWorkspaceTemplate(template),
|
|
660
832
|
widgets: template.widgets
|
|
@@ -809,11 +981,110 @@ function SubPanelHeader({ title, breadcrumb, onBack }) {
|
|
|
809
981
|
</div>;
|
|
810
982
|
}
|
|
811
983
|
|
|
984
|
+
/**
|
|
985
|
+
* EntityBadge — chip showing the selected entity on the source panel and root inspector.
|
|
986
|
+
* Displays primary label + muted secondary label (stable ID). onClear is optional.
|
|
987
|
+
*/
|
|
988
|
+
function EntityBadge({ entity, onClear }) {
|
|
989
|
+
const initials = entity.entityType
|
|
990
|
+
? entity.entityType[0].toUpperCase()
|
|
991
|
+
: (entity.label?.[0] || "•").toUpperCase();
|
|
992
|
+
return <div className="workspace-entity-badge">
|
|
993
|
+
<span className="workspace-entity-badge-icon" aria-hidden="true">{initials}</span>
|
|
994
|
+
<span className="workspace-entity-badge-meta">
|
|
995
|
+
<strong title={entity.label}>{entity.label}</strong>
|
|
996
|
+
{entity.secondaryLabel ? <em title={entity.secondaryLabel}>{entity.secondaryLabel}</em> : null}
|
|
997
|
+
</span>
|
|
998
|
+
{onClear ? <button
|
|
999
|
+
type="button"
|
|
1000
|
+
className="workspace-entity-badge-clear"
|
|
1001
|
+
aria-label={`Clear selected entity ${entity.label}`}
|
|
1002
|
+
onClick={onClear}
|
|
1003
|
+
>
|
|
1004
|
+
<X size={11} />
|
|
1005
|
+
</button> : null}
|
|
1006
|
+
</div>;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function UniversalSourceInfoCard() {
|
|
1010
|
+
return <p className="workspace-source-info-card">
|
|
1011
|
+
Universal source objects support managed integrations and custom APIs/webhooks through normalized metadata and stable saved references.
|
|
1012
|
+
</p>;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* EntitySelector — compact dropdown for picking a normalized source object after
|
|
1017
|
+
* an integration is selected from the SourceSubPanel.
|
|
1018
|
+
*
|
|
1019
|
+
* Governed invariant: only the object `id` is persisted. The `label` is
|
|
1020
|
+
* display-only and may be refreshed from adapter metadata at any time.
|
|
1021
|
+
* The browser never holds source credentials or executes source queries.
|
|
1022
|
+
*/
|
|
1023
|
+
function EntitySelector({ integration, entities, selectedEntityId, selectedEntityLabel, selectedEntityType, onSelect, loading }) {
|
|
1024
|
+
const selected = entities.find((e) => e.id === selectedEntityId)
|
|
1025
|
+
|| (selectedEntityId ? {
|
|
1026
|
+
id: selectedEntityId,
|
|
1027
|
+
label: selectedEntityLabel || selectedEntityId,
|
|
1028
|
+
secondaryLabel: selectedEntityId,
|
|
1029
|
+
entityType: selectedEntityType
|
|
1030
|
+
} : null);
|
|
1031
|
+
|
|
1032
|
+
const clearSelected = () => {
|
|
1033
|
+
if (!selectedEntityId || window.confirm("Remove the selected source object from this widget?")) {
|
|
1034
|
+
onSelect(null);
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
return <div className="workspace-entity-selector">
|
|
1039
|
+
<p className="workspace-panel-label">Source object</p>
|
|
1040
|
+
{selected ? <EntityBadge entity={selected} onClear={clearSelected} /> : null}
|
|
1041
|
+
{loading ? <p className="workspace-entity-empty">Loading source objects…</p> : null}
|
|
1042
|
+
{!loading && !entities.length ? <p className="workspace-entity-empty">
|
|
1043
|
+
No source objects returned. Configure a server-side API/webhook object resolver for this integration.
|
|
1044
|
+
</p> : null}
|
|
1045
|
+
{!loading && entities.length ? <label className="workspace-entity-dropdown">
|
|
1046
|
+
<span>Select source object</span>
|
|
1047
|
+
<select
|
|
1048
|
+
aria-label="Select source object"
|
|
1049
|
+
value={selectedEntityId || ""}
|
|
1050
|
+
onChange={(event) => {
|
|
1051
|
+
const entity = entities.find((item) => item.id === event.target.value);
|
|
1052
|
+
onSelect(entity || null);
|
|
1053
|
+
}}
|
|
1054
|
+
>
|
|
1055
|
+
<option value="">Choose an object</option>
|
|
1056
|
+
{entities.map((entity) => <option key={entity.id} value={entity.id}>
|
|
1057
|
+
{entity.label}{entity.secondaryLabel ? ` · ${entity.secondaryLabel}` : ""}
|
|
1058
|
+
</option>)}
|
|
1059
|
+
</select>
|
|
1060
|
+
</label> : null}
|
|
1061
|
+
</div>;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
812
1064
|
function SourceSubPanel({ widget, integrations, onChange, onBack }) {
|
|
813
1065
|
const binding = widget.config?.binding || {};
|
|
814
1066
|
const currentMode = binding.mode || (widget.kind === "view" ? "manual" : "json");
|
|
815
|
-
|
|
816
|
-
|
|
1067
|
+
const activeSourceType = resolveBindingSourceType(binding);
|
|
1068
|
+
const [query, setQuery] = useState("");
|
|
1069
|
+
const [laneFilter, setLaneFilter] = useState("all");
|
|
1070
|
+
const hasConnectedSource = Boolean(
|
|
1071
|
+
binding.integrationId ||
|
|
1072
|
+
binding.endpointRef ||
|
|
1073
|
+
binding.sourceType === MANAGED_INTEGRATION_SOURCE_TYPE ||
|
|
1074
|
+
binding.sourceType === CUSTOM_API_SOURCE_TYPE
|
|
1075
|
+
);
|
|
1076
|
+
const confirmSourceChange = useCallback((nextLabel) => {
|
|
1077
|
+
if (!hasConnectedSource) return true;
|
|
1078
|
+
const currentLabel = summarizeSource(widget);
|
|
1079
|
+
return window.confirm(`Change source from ${currentLabel} to ${nextLabel}? This updates the widget binding and can clear source-object filters.`);
|
|
1080
|
+
}, [hasConnectedSource, widget]);
|
|
1081
|
+
|
|
1082
|
+
const activeIntegration = useMemo(() => {
|
|
1083
|
+
if (currentMode !== "integration" || !binding.integrationId) return null;
|
|
1084
|
+
const list = Array.isArray(integrations) ? integrations : [];
|
|
1085
|
+
return list.find((item) => item.id === binding.integrationId) || null;
|
|
1086
|
+
}, [currentMode, binding.integrationId, integrations]);
|
|
1087
|
+
|
|
817
1088
|
const groups = useMemo(() => {
|
|
818
1089
|
const list = Array.isArray(integrations) ? integrations : [];
|
|
819
1090
|
const filtered = list.filter((item) => {
|
|
@@ -826,8 +1097,10 @@ function SourceSubPanel({ widget, integrations, onChange, onBack }) {
|
|
|
826
1097
|
"workspace-integration": filtered.filter((item) => item.lane === "workspace-integration")
|
|
827
1098
|
};
|
|
828
1099
|
}, [integrations, laneFilter, query]);
|
|
829
|
-
|
|
830
|
-
|
|
1100
|
+
|
|
1101
|
+
const selectStatic = useCallback(() => {
|
|
1102
|
+
if (!confirmSourceChange("Static rows")) return;
|
|
1103
|
+
if (widget.kind === "chart") {
|
|
831
1104
|
onChange({ ...widget.config, binding: SAMPLE_DATA_BINDINGS.reportingJson });
|
|
832
1105
|
} else {
|
|
833
1106
|
onChange({
|
|
@@ -836,27 +1109,102 @@ function SourceSubPanel({ widget, integrations, onChange, onBack }) {
|
|
|
836
1109
|
binding: SAMPLE_DATA_BINDINGS.companiesManual
|
|
837
1110
|
});
|
|
838
1111
|
}
|
|
839
|
-
|
|
840
|
-
|
|
1112
|
+
}, [confirmSourceChange, onChange, widget.config, widget.kind]);
|
|
1113
|
+
|
|
1114
|
+
const selectCustomApi = useCallback(() => {
|
|
1115
|
+
if (!confirmSourceChange("Custom APIs/Webhooks")) return;
|
|
1116
|
+
onChange({
|
|
1117
|
+
...widget.config,
|
|
1118
|
+
source: "Custom APIs/Webhooks",
|
|
1119
|
+
binding: {
|
|
1120
|
+
...binding,
|
|
1121
|
+
mode: "json",
|
|
1122
|
+
source: "Custom APIs/Webhooks",
|
|
1123
|
+
sourceType: CUSTOM_API_SOURCE_TYPE,
|
|
1124
|
+
sourceAuthority: "custom-api",
|
|
1125
|
+
endpointRef: binding.endpointRef || "",
|
|
1126
|
+
fields: Array.isArray(binding.fields) ? binding.fields : ["entityId", "status", "createdAt"]
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
}, [binding, confirmSourceChange, onChange, widget.config]);
|
|
1130
|
+
|
|
1131
|
+
const updateCustomFields = useCallback((value) => {
|
|
1132
|
+
const fields = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1133
|
+
onChange({
|
|
1134
|
+
...widget.config,
|
|
1135
|
+
binding: {
|
|
1136
|
+
...binding,
|
|
1137
|
+
mode: "json",
|
|
1138
|
+
source: "Custom APIs/Webhooks",
|
|
1139
|
+
sourceType: CUSTOM_API_SOURCE_TYPE,
|
|
1140
|
+
sourceAuthority: "custom-api",
|
|
1141
|
+
fields
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
}, [binding, onChange, widget.config]);
|
|
1145
|
+
|
|
1146
|
+
const updateEndpointRef = useCallback((value) => {
|
|
841
1147
|
onChange({
|
|
842
1148
|
...widget.config,
|
|
1149
|
+
binding: {
|
|
1150
|
+
...binding,
|
|
1151
|
+
mode: "json",
|
|
1152
|
+
source: "Custom APIs/Webhooks",
|
|
1153
|
+
sourceType: CUSTOM_API_SOURCE_TYPE,
|
|
1154
|
+
sourceAuthority: "custom-api",
|
|
1155
|
+
endpointRef: value
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
}, [binding, onChange, widget.config]);
|
|
1159
|
+
|
|
1160
|
+
const selectIntegration = useCallback((integration) => {
|
|
1161
|
+
if (binding.integrationId && binding.integrationId !== integration.id && !confirmSourceChange(integration.label)) return;
|
|
1162
|
+
onChange({
|
|
1163
|
+
...widget.config,
|
|
843
1164
|
source: integration.label,
|
|
844
1165
|
binding: {
|
|
845
1166
|
mode: "integration",
|
|
846
1167
|
source: integration.label,
|
|
1168
|
+
sourceType: MANAGED_INTEGRATION_SOURCE_TYPE,
|
|
1169
|
+
sourceAuthority: "growthub-bridge",
|
|
847
1170
|
integrationId: integration.id,
|
|
848
|
-
lane: integration.lane
|
|
1171
|
+
lane: integration.lane,
|
|
1172
|
+
provider: integration.provider
|
|
849
1173
|
}
|
|
850
1174
|
});
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1175
|
+
}, [binding.integrationId, confirmSourceChange, onChange, widget.config]);
|
|
1176
|
+
|
|
1177
|
+
return <section className="workspace-widget-subpanel">
|
|
1178
|
+
<SubPanelHeader title="Source" breadcrumb={widget.title} onBack={onBack} />
|
|
1179
|
+
<UniversalSourceInfoCard />
|
|
1180
|
+
<p className="workspace-panel-label">Source type</p>
|
|
1181
|
+
<div className="workspace-source-object-list">
|
|
1182
|
+
{SOURCE_TYPE_OBJECTS.map((sourceType) => {
|
|
1183
|
+
const isActive = activeSourceType === sourceType.id;
|
|
1184
|
+
return <button
|
|
1185
|
+
key={sourceType.id}
|
|
1186
|
+
type="button"
|
|
1187
|
+
className={`workspace-source-object-row${isActive ? " active" : ""}`}
|
|
1188
|
+
onClick={sourceType.id === CUSTOM_API_SOURCE_TYPE ? selectCustomApi : undefined}
|
|
1189
|
+
disabled={sourceType.id === MANAGED_INTEGRATION_SOURCE_TYPE}
|
|
1190
|
+
>
|
|
1191
|
+
<span className="workspace-source-object-icon" aria-hidden="true">
|
|
1192
|
+
{sourceType.id === MANAGED_INTEGRATION_SOURCE_TYPE ? <Database size={15} /> : <LinkIcon size={15} />}
|
|
1193
|
+
</span>
|
|
1194
|
+
<span className="workspace-source-meta">
|
|
1195
|
+
<strong>{sourceType.label}</strong>
|
|
1196
|
+
<em>{sourceType.authority} · {sourceType.description}</em>
|
|
1197
|
+
</span>
|
|
1198
|
+
{isActive ? <span className="workspace-source-tick" aria-hidden="true"><Check size={16} strokeWidth={2.4} /></span> : null}
|
|
1199
|
+
</button>;
|
|
1200
|
+
})}
|
|
1201
|
+
</div>
|
|
854
1202
|
<div className="workspace-source-controls">
|
|
855
1203
|
<label>
|
|
856
1204
|
<Search size={14} aria-hidden="true" />
|
|
857
1205
|
<input
|
|
858
1206
|
aria-label="Search sources"
|
|
859
|
-
placeholder="Search
|
|
1207
|
+
placeholder="Search connectors"
|
|
860
1208
|
value={query}
|
|
861
1209
|
onChange={(event) => setQuery(event.target.value)}
|
|
862
1210
|
/>
|
|
@@ -868,7 +1216,7 @@ function SourceSubPanel({ widget, integrations, onChange, onBack }) {
|
|
|
868
1216
|
value={laneFilter}
|
|
869
1217
|
onChange={(event) => setLaneFilter(event.target.value)}
|
|
870
1218
|
>
|
|
871
|
-
<option value="all">All
|
|
1219
|
+
<option value="all">All connector lanes</option>
|
|
872
1220
|
<option value="data-source">Data sources</option>
|
|
873
1221
|
<option value="workspace-integration">Workspace tools</option>
|
|
874
1222
|
</select>
|
|
@@ -879,15 +1227,15 @@ function SourceSubPanel({ widget, integrations, onChange, onBack }) {
|
|
|
879
1227
|
<div className="workspace-source-list">
|
|
880
1228
|
<button
|
|
881
1229
|
type="button"
|
|
882
|
-
className={`workspace-source-row${
|
|
1230
|
+
className={`workspace-source-row${activeSourceType === "static" ? " active" : ""}`}
|
|
883
1231
|
onClick={selectStatic}
|
|
884
1232
|
>
|
|
885
1233
|
<span className="workspace-source-icon" aria-hidden="true"><Grid2X2 size={15} /></span>
|
|
886
1234
|
<span className="workspace-source-meta">
|
|
887
1235
|
<strong>Static rows</strong>
|
|
888
|
-
<em>Inline
|
|
1236
|
+
<em>Inline JSON, CSV, or manual rows remain supported.</em>
|
|
889
1237
|
</span>
|
|
890
|
-
{
|
|
1238
|
+
{activeSourceType === "static" ? <span className="workspace-source-tick" aria-hidden="true"><Check size={16} strokeWidth={2.4} /></span> : null}
|
|
891
1239
|
</button>
|
|
892
1240
|
</div>
|
|
893
1241
|
{Object.entries(groups).map(([lane, items]) => items.length ? <div key={lane}>
|
|
@@ -907,13 +1255,36 @@ function SourceSubPanel({ widget, integrations, onChange, onBack }) {
|
|
|
907
1255
|
<strong>{integration.label}</strong>
|
|
908
1256
|
<em>{describeIntegrationLane(integration)} · {connected ? "connected" : "needs connection"}</em>
|
|
909
1257
|
</span>
|
|
910
|
-
{isActive ? <span className="workspace-source-tick" aria-hidden="true"><
|
|
1258
|
+
{isActive ? <span className="workspace-source-tick" aria-hidden="true"><Check size={16} strokeWidth={2.4} /></span> : null}
|
|
911
1259
|
</button>;
|
|
912
1260
|
})}
|
|
913
1261
|
</div>
|
|
914
1262
|
</div> : null)}
|
|
1263
|
+
{activeSourceType === CUSTOM_API_SOURCE_TYPE ? <div className="workspace-custom-source-config">
|
|
1264
|
+
<label>
|
|
1265
|
+
<span>Endpoint reference</span>
|
|
1266
|
+
<input
|
|
1267
|
+
value={binding.endpointRef || ""}
|
|
1268
|
+
placeholder="api.clients.primary"
|
|
1269
|
+
onChange={(event) => updateEndpointRef(event.target.value)}
|
|
1270
|
+
/>
|
|
1271
|
+
</label>
|
|
1272
|
+
<label>
|
|
1273
|
+
<span>Available fields</span>
|
|
1274
|
+
<input
|
|
1275
|
+
value={(Array.isArray(binding.fields) ? binding.fields : []).join(", ")}
|
|
1276
|
+
placeholder="entityId, status, createdAt"
|
|
1277
|
+
onChange={(event) => updateCustomFields(event.target.value)}
|
|
1278
|
+
/>
|
|
1279
|
+
</label>
|
|
1280
|
+
</div> : null}
|
|
1281
|
+
{currentMode === "integration" && binding.integrationId ? <div className="workspace-active-source-state">
|
|
1282
|
+
<span>Active source</span>
|
|
1283
|
+
<strong>{activeIntegration?.label || binding.source || binding.integrationId}</strong>
|
|
1284
|
+
<code>{binding.integrationId}</code>
|
|
1285
|
+
</div> : null}
|
|
915
1286
|
<p className="workspace-panel-hint">
|
|
916
|
-
Selecting a source writes a binding reference only. The browser
|
|
1287
|
+
Selecting a source writes a binding reference only. The browser only calls local workspace routes and never stores source credentials.
|
|
917
1288
|
</p>
|
|
918
1289
|
</section>;
|
|
919
1290
|
}
|
|
@@ -1052,24 +1423,85 @@ function SortSubPanel({ widget, onChange, onBack }) {
|
|
|
1052
1423
|
</section>;
|
|
1053
1424
|
}
|
|
1054
1425
|
|
|
1055
|
-
function FilterSubPanel({ widget, onChange, onBack }) {
|
|
1426
|
+
function FilterSubPanel({ widget, integrations, onChange, onBack }) {
|
|
1427
|
+
const binding = widget.config?.binding || {};
|
|
1056
1428
|
const filter = getFilterConfig(widget);
|
|
1057
|
-
const
|
|
1429
|
+
const [entities, setEntities] = useState([]);
|
|
1430
|
+
const [entitiesLoading, setEntitiesLoading] = useState(false);
|
|
1431
|
+
const fieldChoices = getFilterFieldChoices(widget, entities);
|
|
1432
|
+
const columns = fieldChoices.map((field) => field.id);
|
|
1058
1433
|
const setFilter = (next) => onChange({ ...widget.config, filter: next });
|
|
1059
1434
|
const setOp = (op) => setFilter({ ...filter, op });
|
|
1435
|
+
const activeIntegration = useMemo(() => {
|
|
1436
|
+
if (binding.mode !== "integration" || !binding.integrationId) return null;
|
|
1437
|
+
const list = Array.isArray(integrations) ? integrations : [];
|
|
1438
|
+
return list.find((item) => item.id === binding.integrationId) || null;
|
|
1439
|
+
}, [binding.integrationId, binding.mode, integrations]);
|
|
1440
|
+
|
|
1441
|
+
useEffect(() => {
|
|
1442
|
+
if (!binding.integrationId || binding.mode !== "integration") {
|
|
1443
|
+
setEntities([]);
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
let cancelled = false;
|
|
1447
|
+
setEntitiesLoading(true);
|
|
1448
|
+
fetch(`/api/workspace/integration-entities?integrationId=${encodeURIComponent(binding.integrationId)}`, { cache: "no-store" })
|
|
1449
|
+
.then((res) => res.ok ? res.json() : { entities: [] })
|
|
1450
|
+
.then((data) => {
|
|
1451
|
+
if (!cancelled) {
|
|
1452
|
+
setEntities(Array.isArray(data.entities) ? data.entities : []);
|
|
1453
|
+
setEntitiesLoading(false);
|
|
1454
|
+
}
|
|
1455
|
+
})
|
|
1456
|
+
.catch(() => {
|
|
1457
|
+
if (!cancelled) {
|
|
1458
|
+
setEntities([]);
|
|
1459
|
+
setEntitiesLoading(false);
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
return () => { cancelled = true; };
|
|
1463
|
+
}, [binding.integrationId, binding.mode]);
|
|
1464
|
+
|
|
1060
1465
|
const addClause = () => {
|
|
1061
|
-
const fieldId = columns[0] || "";
|
|
1466
|
+
const fieldId = binding.mode === "integration" && entities.length ? "id" : (columns[0] || "");
|
|
1062
1467
|
if (!fieldId) return;
|
|
1063
|
-
setFilter({ ...filter, clauses: [...filter.clauses, { fieldId, operator:
|
|
1468
|
+
setFilter({ ...filter, clauses: [...filter.clauses, { fieldId, operator: "eq", value: "" }] });
|
|
1064
1469
|
};
|
|
1470
|
+
const updateField = (index, fieldId) => {
|
|
1471
|
+
setFilter({
|
|
1472
|
+
...filter,
|
|
1473
|
+
clauses: filter.clauses.map((clause, idx) => idx === index ? { ...clause, fieldId, value: "" } : clause)
|
|
1474
|
+
});
|
|
1475
|
+
};
|
|
1476
|
+
const selectEntity = useCallback((entity) => {
|
|
1477
|
+
onChange(updateWidgetEntityBinding(widget, entity));
|
|
1478
|
+
}, [onChange, widget]);
|
|
1065
1479
|
const updateClause = (index, patch) => {
|
|
1066
1480
|
setFilter({ ...filter, clauses: filter.clauses.map((clause, idx) => idx === index ? { ...clause, ...patch } : clause) });
|
|
1067
1481
|
};
|
|
1068
1482
|
const removeClause = (index) => {
|
|
1069
1483
|
setFilter({ ...filter, clauses: filter.clauses.filter((_, idx) => idx !== index) });
|
|
1070
1484
|
};
|
|
1071
|
-
|
|
1072
|
-
|
|
1485
|
+
return <section className="workspace-widget-subpanel">
|
|
1486
|
+
<SubPanelHeader title="Filter" breadcrumb={widget.title} onBack={onBack} />
|
|
1487
|
+
<UniversalSourceInfoCard />
|
|
1488
|
+
<div className="workspace-filter-source-state">
|
|
1489
|
+
<span>{summarizeSourceType(binding)}</span>
|
|
1490
|
+
<strong>{summarizeSource(widget)}</strong>
|
|
1491
|
+
</div>
|
|
1492
|
+
{binding.mode === "integration" && binding.integrationId ? <EntitySelector
|
|
1493
|
+
integration={activeIntegration}
|
|
1494
|
+
entities={entities}
|
|
1495
|
+
selectedEntityId={binding.entityId || null}
|
|
1496
|
+
selectedEntityLabel={binding.entityLabel || null}
|
|
1497
|
+
selectedEntityType={binding.entityType || null}
|
|
1498
|
+
onSelect={selectEntity}
|
|
1499
|
+
loading={entitiesLoading}
|
|
1500
|
+
/> : null}
|
|
1501
|
+
{binding.sourceType === CUSTOM_API_SOURCE_TYPE ? <div className="workspace-filter-source-state">
|
|
1502
|
+
<span>Custom endpoint</span>
|
|
1503
|
+
<code>{binding.endpointRef || "No endpoint reference set"}</code>
|
|
1504
|
+
</div> : null}
|
|
1073
1505
|
<div className="workspace-filter-op-toggle" role="radiogroup" aria-label="Filter conjunction">
|
|
1074
1506
|
{KNOWN_FILTER_CONJUNCTIONS.map((op) => <button
|
|
1075
1507
|
key={op}
|
|
@@ -1084,14 +1516,15 @@ function FilterSubPanel({ widget, onChange, onBack }) {
|
|
|
1084
1516
|
{filter.clauses.length === 0 ? <p className="workspace-panel-hint">No filter clauses.</p> : null}
|
|
1085
1517
|
{filter.clauses.map((clause, index) => {
|
|
1086
1518
|
const valueless = clause.operator === "isEmpty" || clause.operator === "isNotEmpty";
|
|
1519
|
+
const valueChoices = binding.mode === "integration" ? getEntityValueChoices(entities, clause.fieldId) : [];
|
|
1087
1520
|
return <div key={index} className="workspace-filter-clause">
|
|
1088
1521
|
<select
|
|
1089
1522
|
aria-label={`Filter ${index + 1} field`}
|
|
1090
1523
|
value={clause.fieldId}
|
|
1091
|
-
onChange={(event) =>
|
|
1524
|
+
onChange={(event) => updateField(index, event.target.value)}
|
|
1092
1525
|
>
|
|
1093
1526
|
{!columns.includes(clause.fieldId) && clause.fieldId ? <option value={clause.fieldId}>{clause.fieldId}</option> : null}
|
|
1094
|
-
{
|
|
1527
|
+
{fieldChoices.map((field) => <option key={field.id} value={field.id}>{field.label}</option>)}
|
|
1095
1528
|
</select>
|
|
1096
1529
|
<select
|
|
1097
1530
|
aria-label={`Filter ${index + 1} operator`}
|
|
@@ -1100,7 +1533,20 @@ function FilterSubPanel({ widget, onChange, onBack }) {
|
|
|
1100
1533
|
>
|
|
1101
1534
|
{KNOWN_FILTER_OPERATORS.map((op) => <option key={op} value={op}>{FILTER_OPERATOR_LABELS[op] || op}</option>)}
|
|
1102
1535
|
</select>
|
|
1103
|
-
{!valueless ? <
|
|
1536
|
+
{!valueless && binding.mode === "integration" && valueChoices.length ? <select
|
|
1537
|
+
aria-label={`Filter ${index + 1} value`}
|
|
1538
|
+
value={clause.value ?? ""}
|
|
1539
|
+
onChange={(event) => {
|
|
1540
|
+
const entity = findEntityByFieldValue(entities, clause.fieldId, event.target.value);
|
|
1541
|
+
updateClause(index, { value: event.target.value });
|
|
1542
|
+
if (entity && (clause.fieldId === "id" || clause.fieldId === "entityId")) {
|
|
1543
|
+
onChange(updateWidgetEntityBinding(widget, entity));
|
|
1544
|
+
}
|
|
1545
|
+
}}
|
|
1546
|
+
>
|
|
1547
|
+
<option value="">Select value</option>
|
|
1548
|
+
{valueChoices.map((choice) => <option key={choice.value} value={choice.value}>{choice.label}</option>)}
|
|
1549
|
+
</select> : !valueless ? <input
|
|
1104
1550
|
aria-label={`Filter ${index + 1} value`}
|
|
1105
1551
|
value={clause.value ?? ""}
|
|
1106
1552
|
placeholder="value"
|
|
@@ -1119,11 +1565,12 @@ function FilterSubPanel({ widget, onChange, onBack }) {
|
|
|
1119
1565
|
</section>;
|
|
1120
1566
|
}
|
|
1121
1567
|
|
|
1122
|
-
function ChartConfigPanel({ widget, onChange, onSubPage }) {
|
|
1568
|
+
function ChartConfigPanel({ widget, branding, onChange, onSubPage }) {
|
|
1123
1569
|
const chartType = getChartType(widget) === "line" ? DEFAULT_CHART_TYPE : getChartType(widget);
|
|
1124
1570
|
const xAxis = getChartAxis(widget, "xAxis");
|
|
1125
1571
|
const yAxis = getChartAxis(widget, "yAxis");
|
|
1126
1572
|
const style = getChartStyle(widget);
|
|
1573
|
+
const activeColor = resolveChartColor(style, branding) || "#d9e4ff";
|
|
1127
1574
|
const setChartType = (type) => onChange({ ...widget.config, chartType: type });
|
|
1128
1575
|
const setXAxis = (patch) => onChange({ ...widget.config, xAxis: { ...xAxis, ...patch } });
|
|
1129
1576
|
const setYAxis = (patch) => onChange({ ...widget.config, yAxis: { ...yAxis, ...patch } });
|
|
@@ -1148,11 +1595,17 @@ function ChartConfigPanel({ widget, onChange, onSubPage }) {
|
|
|
1148
1595
|
})}
|
|
1149
1596
|
</div>
|
|
1150
1597
|
<button type="button" className="workspace-settings-row" onClick={() => onSubPage("source")}>
|
|
1151
|
-
<span>Source</span><code>{summarizeSource(widget)}</code>
|
|
1598
|
+
<span>Source</span><code>{summarizeSourceType(widget.config?.binding)} · {summarizeSource(widget)}</code>
|
|
1152
1599
|
</button>
|
|
1153
1600
|
<button type="button" className="workspace-settings-row" onClick={() => onSubPage("filter")}>
|
|
1154
1601
|
<span>Filter</span><code>{summarizeFilter(widget)}</code>
|
|
1155
1602
|
</button>
|
|
1603
|
+
{widget.config?.binding?.entityId ? <EntityBadge entity={{
|
|
1604
|
+
id: widget.config.binding.entityId,
|
|
1605
|
+
label: widget.config.binding.entityLabel || widget.config.binding.entityId,
|
|
1606
|
+
secondaryLabel: widget.config.binding.entityId,
|
|
1607
|
+
entityType: widget.config.binding.entityType
|
|
1608
|
+
}} /> : null}
|
|
1156
1609
|
<p className="workspace-panel-label">X axis</p>
|
|
1157
1610
|
<label>
|
|
1158
1611
|
<span>Data on display</span>
|
|
@@ -1225,9 +1678,16 @@ function ChartConfigPanel({ widget, onChange, onSubPage }) {
|
|
|
1225
1678
|
<select value={style.colors || "auto"} onChange={(event) => setStyle({ colors: event.target.value })}>
|
|
1226
1679
|
<option value="auto">Auto</option>
|
|
1227
1680
|
<option value="accent">Accent</option>
|
|
1681
|
+
<option value="brand-local">Local brand kit</option>
|
|
1682
|
+
<option value="brand-bridge">Bridge brand kit</option>
|
|
1228
1683
|
<option value="manual">Manual</option>
|
|
1229
1684
|
</select>
|
|
1230
1685
|
</label>
|
|
1686
|
+
<div className="workspace-color-preview-row">
|
|
1687
|
+
<span>Active color</span>
|
|
1688
|
+
<em style={{ background: activeColor }} />
|
|
1689
|
+
<code>{activeColor}</code>
|
|
1690
|
+
</div>
|
|
1231
1691
|
{style.colors === "manual" ? <div className="workspace-color-picker-row">
|
|
1232
1692
|
<label>
|
|
1233
1693
|
<span>Manual color</span>
|
|
@@ -1414,7 +1874,7 @@ function IframePreviewModal({ widget, onClose }) {
|
|
|
1414
1874
|
</div>;
|
|
1415
1875
|
}
|
|
1416
1876
|
|
|
1417
|
-
function WidgetPreview({ widget, selected, onSelect, onMoveStart, onRemove, onResizeStart, onExpandIframe }) {
|
|
1877
|
+
function WidgetPreview({ widget, branding, selected, onSelect, onMoveStart, onRemove, onResizeStart, onExpandIframe }) {
|
|
1418
1878
|
const fallbackColumns = widget.config?.columns?.length ? widget.config.columns : ["Name", "Domain Name"];
|
|
1419
1879
|
const visibleColumns = widget.kind === "view" ? getVisibleColumns(widget) : fallbackColumns;
|
|
1420
1880
|
const viewColumns = visibleColumns.length ? visibleColumns : fallbackColumns;
|
|
@@ -1423,7 +1883,11 @@ function WidgetPreview({ widget, selected, onSelect, onMoveStart, onRemove, onRe
|
|
|
1423
1883
|
const chartType = widget.kind === "chart" ? (getChartType(widget) === "line" ? DEFAULT_CHART_TYPE : getChartType(widget)) : null;
|
|
1424
1884
|
const dataLabels = widget.kind === "chart" ? Boolean(widget.config?.style?.dataLabels) : false;
|
|
1425
1885
|
const chartStyle = widget.kind === "chart" ? getChartStyle(widget) : {};
|
|
1426
|
-
const chartColor = chartStyle
|
|
1886
|
+
const chartColor = resolveChartColor(chartStyle, branding);
|
|
1887
|
+
const selectedSourceObject = widget.config?.binding?.entityId ? {
|
|
1888
|
+
id: widget.config.binding.entityId,
|
|
1889
|
+
label: widget.config.binding.entityLabel || widget.config.binding.entityId
|
|
1890
|
+
} : null;
|
|
1427
1891
|
return <article
|
|
1428
1892
|
className={`workspace-widget-preview${selected ? " selected" : ""}`}
|
|
1429
1893
|
onClick={onSelect}
|
|
@@ -1439,6 +1903,10 @@ function WidgetPreview({ widget, selected, onSelect, onMoveStart, onRemove, onRe
|
|
|
1439
1903
|
onPointerDown={(event) => onMoveStart(event)}
|
|
1440
1904
|
>::</span>
|
|
1441
1905
|
<strong>{widget.title}</strong>
|
|
1906
|
+
{selectedSourceObject ? <span
|
|
1907
|
+
className="workspace-widget-source-chip"
|
|
1908
|
+
title={`${selectedSourceObject.label} · ${selectedSourceObject.id}`}
|
|
1909
|
+
>{selectedSourceObject.label}</span> : null}
|
|
1442
1910
|
<button
|
|
1443
1911
|
aria-label={`Remove ${widget.title}`}
|
|
1444
1912
|
onClick={(event) => {
|
|
@@ -1651,7 +2119,7 @@ function WorkspaceManagementPanel({ config, persistence, adapterConfig, onClose
|
|
|
1651
2119
|
</div>;
|
|
1652
2120
|
}
|
|
1653
2121
|
|
|
1654
|
-
function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, persistence }) {
|
|
2122
|
+
function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, integrationSettings, persistence }) {
|
|
1655
2123
|
const [config, setConfig] = useState(() => {
|
|
1656
2124
|
const dashboards = Array.isArray(initialConfig.dashboards) && initialConfig.dashboards.length
|
|
1657
2125
|
? initialConfig.dashboards.map((dashboard, index) =>
|
|
@@ -1706,6 +2174,8 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, pe
|
|
|
1706
2174
|
const importInputRef = useRef(null);
|
|
1707
2175
|
const addSlot = dragPreview || selectedPosition;
|
|
1708
2176
|
const selectedWidget = activeWidgets.find((widget) => widget.id === selectedWidgetId) || null;
|
|
2177
|
+
const availableIntegrations = useMemo(() => flattenIntegrationSettings(integrationSettings), [integrationSettings]);
|
|
2178
|
+
const branding = config.branding || {};
|
|
1709
2179
|
const occupiedCells = useMemo(() => {
|
|
1710
2180
|
const cells = new Set();
|
|
1711
2181
|
for (const widget of activeWidgets) {
|
|
@@ -2769,6 +3239,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, pe
|
|
|
2769
3239
|
</button>
|
|
2770
3240
|
{activeWidgets.map((widget) => <WidgetPreview
|
|
2771
3241
|
key={widget.id}
|
|
3242
|
+
branding={branding}
|
|
2772
3243
|
onMoveStart={(event) => beginMoveDrag(widget, event)}
|
|
2773
3244
|
onRemove={() => removeSelectedWidget(widget.id)}
|
|
2774
3245
|
onResizeStart={(corner, event) => beginResizeDrag(widget, corner, event)}
|
|
@@ -2820,7 +3291,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, pe
|
|
|
2820
3291
|
</div> : null}
|
|
2821
3292
|
{selectedWidget && inspectorPath === "source" ? <SourceSubPanel
|
|
2822
3293
|
widget={selectedWidget}
|
|
2823
|
-
integrations={
|
|
3294
|
+
integrations={availableIntegrations}
|
|
2824
3295
|
onChange={replaceSelectedWidgetConfig}
|
|
2825
3296
|
onBack={() => setInspectorPath(SUB_PANEL_ROOT)}
|
|
2826
3297
|
/> : null}
|
|
@@ -2836,6 +3307,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, pe
|
|
|
2836
3307
|
/> : null}
|
|
2837
3308
|
{selectedWidget && inspectorPath === "filter" ? <FilterSubPanel
|
|
2838
3309
|
widget={selectedWidget}
|
|
3310
|
+
integrations={availableIntegrations}
|
|
2839
3311
|
onChange={replaceSelectedWidgetConfig}
|
|
2840
3312
|
onBack={() => setInspectorPath(SUB_PANEL_ROOT)}
|
|
2841
3313
|
/> : null}
|
|
@@ -2846,6 +3318,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, pe
|
|
|
2846
3318
|
</label>
|
|
2847
3319
|
{selectedWidget.kind === "chart" ? <ChartConfigPanel
|
|
2848
3320
|
widget={selectedWidget}
|
|
3321
|
+
branding={branding}
|
|
2849
3322
|
onChange={replaceSelectedWidgetConfig}
|
|
2850
3323
|
onSubPage={(name) => setInspectorPath(name)}
|
|
2851
3324
|
/> : null}
|