Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 268 additions & 66 deletions apps/studio/src/components/RightPanel/FieldPicker.tsx

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion apps/studio/src/hooks/useAutoSave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { notifyChange, requestSave as sendToHost } from '../embed/message-router
import { setEmbedSessionSnapshot } from '../lib/embed/sessionSnapshot';
import { resolveEditorServiceConfig } from '../lib/embedded/service-config';
import { sanitizeDataSourcesForHostSave } from '../lib/embedded/hostDataSourcePolicy';
import { augmentPlatformDataSourcesForNodes } from '../lib/platformDatasourceBindings';

// =============================================================================
// Hook Options
Expand Down Expand Up @@ -72,9 +73,13 @@ export function useAutoSave(options: UseAutoSaveOptions) {
// 场景2: 嵌入物模型 - 保存到宿主平台
if (shouldSaveToHost()) {
setEmbedSessionSnapshot(project.meta.id, project, 'host-save');
const augmentedDataSources = augmentPlatformDataSourcesForNodes(
project.dataSources as Parameters<typeof augmentPlatformDataSourcesForNodes>[0],
project.nodes as Array<Record<string, unknown>>,
);
const dataSources = sanitizeDataSourcesForHostSave(
project.nodes,
project.dataSources,
augmentedDataSources,
resolveEditorServiceConfig().context,
);
const payload: SavePayload = {
Expand Down
4 changes: 3 additions & 1 deletion apps/studio/src/i18n/locales/en/editor.json
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,9 @@
"transformDocsData": "`data` — full data source snapshot",
"fieldValueRaw": "Raw:",
"fieldValueTransformed": "Result:",
"fieldValueCurrent": "Current:"
"fieldValueCurrent": "Current:",
"previewExpand": "Expand",
"previewCollapse": "Collapse"
},
"upload": {
"onlyImages": "Only image files are supported",
Expand Down
4 changes: 3 additions & 1 deletion apps/studio/src/i18n/locales/zh/editor.json
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,9 @@
"transformDocsData": "`data` — 完整的数据源快照",
"fieldValueRaw": "原始:",
"fieldValueTransformed": "变换:",
"fieldValueCurrent": "当前值:"
"fieldValueCurrent": "当前值:",
"previewExpand": "展开",
"previewCollapse": "收起"
},
"upload": {
"onlyImages": "只支持图片文件",
Expand Down
17 changes: 15 additions & 2 deletions apps/studio/src/lib/platformDatasourceBindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('platformDatasourceBindings', () => {
expect((nextConfigs[0]?.config as any)?.bufferSize).toBe(24);
});

it('does not create missing platform data sources from bindings alone', () => {
it('creates missing platform data sources from node bindings', () => {
const nextConfigs = augmentPlatformDataSourcesForNodes(
[],
[
Expand All @@ -103,6 +103,19 @@ describe('platformDatasourceBindings', () => {
],
);

expect(nextConfigs).toEqual([]);
expect(nextConfigs).toEqual([
{
id: '__platform_dev-3__',
name: 'Device dev-3',
type: 'PLATFORM_FIELD',
config: {
source: 'platform',
fieldMappings: {},
deviceId: 'dev-3',
requestedFields: ['cpu_usage'],
bufferSize: DEFAULT_HISTORY_BUFFER_SIZE,
},
},
]);
});
});
27 changes: 20 additions & 7 deletions apps/studio/src/lib/platformDatasourceBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ function isPlatformDataSourceId(dataSourceId: string): boolean {
return /^__platform_(.+)__$/.test(dataSourceId);
}

function getPlatformDeviceId(dataSourceId: string): string | null {
const match = /^__platform_(.+)__$/.exec(dataSourceId);
return match?.[1] ?? null;
}

function getFieldRoot(fieldPath?: string): string | null {
if (!fieldPath) return null;
const [root] = fieldPath.split(/[.[\]]/).filter(Boolean);
Expand Down Expand Up @@ -115,24 +120,32 @@ export function augmentPlatformDataSourcesForNodes(

requirements.forEach((requirement, dataSourceId) => {
const baseDataSource = nextById.get(dataSourceId);
if (!baseDataSource) return;
const deviceId = getPlatformDeviceId(dataSourceId);
if (!baseDataSource && !deviceId) return;

const baseConfig = (baseDataSource.config ?? {}) as PlatformFieldConfig;
const baseConfig = (baseDataSource?.config ?? {}) as PlatformFieldConfig;
const existingBufferSize = normalizeBufferSize(baseConfig.bufferSize);
const nextBufferSize = requirement.needsHistory
? Math.max(existingBufferSize, inferredBufferSize)
: existingBufferSize;

nextById.set(dataSourceId, {
...baseDataSource,
...(baseDataSource ?? {
id: dataSourceId,
name: `Device ${deviceId}`,
}),
type: 'PLATFORM_FIELD',
config: {
source: 'platform',
fieldMappings: {},
...baseConfig,
...(deviceId ? { deviceId } : {}),
requestedFields: Array.from(
new Set([...(baseConfig.requestedFields ?? []), ...requirement.requestedFields]),
),
bufferSize: requirement.needsHistory
? Math.max(existingBufferSize, inferredBufferSize)
: existingBufferSize,
bufferSize: nextBufferSize,
},
});
} as DataSource);
});

return Array.from(nextById.values());
Expand Down
73 changes: 73 additions & 0 deletions apps/studio/src/lib/stores/platformDeviceStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { afterEach, describe, expect, it } from 'vitest';
import { platformDeviceStore } from './platformDeviceStore';

afterEach(() => {
platformDeviceStore.clearDevices();
});

describe('platformDeviceStore', () => {
it('preserves loaded field metadata when groups are refreshed', () => {
platformDeviceStore.setDevices([
{
deviceId: 'dev-1',
deviceName: 'Device 1',
groupId: 'group-a',
fields: [{ id: 'supplyPressure', name: 'Supply pressure', type: 'number' }],
},
]);

platformDeviceStore.setGroups([{ groupId: 'group-b', groupName: 'Group B' }]);

expect(platformDeviceStore.getDevices()[0]?.fields).toEqual([
{ id: 'supplyPressure', name: 'Supply pressure', type: 'number' },
]);
});

it('preserves loaded field metadata when device summaries are refreshed', () => {
platformDeviceStore.setDevices([
{
deviceId: 'dev-1',
deviceName: 'Device 1',
groupId: 'group-a',
deviceConfigId: 'config-1',
fields: [{ id: 'returnTemp', name: 'Return temperature', type: 'number' }],
},
]);

platformDeviceStore.setDevices([
{
deviceId: 'dev-1',
deviceName: 'Device 1 renamed',
groupId: 'group-a',
},
]);

const [device] = platformDeviceStore.getDevices();
expect(device?.deviceName).toBe('Device 1 renamed');
expect(device?.deviceConfigId).toBe('config-1');
expect(device?.fields).toEqual([
{ id: 'returnTemp', name: 'Return temperature', type: 'number' },
]);
});

it('preserves loaded field metadata when a group page is refreshed', () => {
platformDeviceStore.setDevicesForGroup('group-a', [
{
deviceId: 'dev-1',
deviceName: 'Device 1',
fields: [{ id: 'totalHeat', name: 'Total heat', type: 'number' }],
},
]);

platformDeviceStore.setDevicesForGroup('group-a', [
{
deviceId: 'dev-1',
deviceName: 'Device 1',
},
]);

expect(platformDeviceStore.getDevices()[0]?.fields).toEqual([
{ id: 'totalHeat', name: 'Total heat', type: 'number' },
]);
});
});
63 changes: 52 additions & 11 deletions apps/studio/src/lib/stores/platformDeviceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,37 @@ function normalizeDevices(devices: PlatformDevice[]): PlatformDevice[] {
});
}

function hasFields(device: PlatformDevice | undefined): boolean {
return Array.isArray(device?.fields) && device.fields.length > 0;
}

function mergeDeviceMetadata(
nextDevice: PlatformDevice,
existingDevice: PlatformDevice | undefined,
): PlatformDevice {
if (!existingDevice) return nextDevice;

return {
...existingDevice,
...nextDevice,
templateId: nextDevice.templateId ?? existingDevice.templateId,
deviceConfigId: nextDevice.deviceConfigId ?? existingDevice.deviceConfigId,
deviceConfigName: nextDevice.deviceConfigName ?? existingDevice.deviceConfigName,
presets: Array.isArray(nextDevice.presets) ? nextDevice.presets : existingDevice.presets,
fields: hasFields(nextDevice) ? nextDevice.fields : existingDevice.fields,
};
}

function mergeDevicesWithExisting(
devices: PlatformDevice[],
existingDevices: PlatformDevice[],
): PlatformDevice[] {
const existingById = new Map(existingDevices.map((device) => [device.deviceId, device]));
return normalizeDevices(devices).map((device) =>
mergeDeviceMetadata(device, existingById.get(device.deviceId)),
);
}

function deriveGroupsFromDevices(devices: PlatformDevice[]): PlatformDeviceGroup[] {
const groups: PlatformDeviceGroup[] = devices.map((device) => {
const groupId = normalizeGroupId(device.groupId, device.groupName);
Expand All @@ -132,20 +163,29 @@ export const usePlatformDeviceStore = create<PlatformDeviceState>((set) => ({
devices: [],

setGroups: (groups) =>
set({
groups: normalizeGroups(groups),
devices: [],
loadedGroupIds: [],
set((state) => {
const normalizedGroups = normalizeGroups(groups);
const validGroupIds = new Set(normalizedGroups.map((group) => group.groupId));
const retainedLoadedGroupIds = state.loadedGroupIds.filter((groupId) =>
validGroupIds.has(normalizeGroupId(groupId)),
);

return {
groups: normalizedGroups,
devices: state.devices,
loadedGroupIds: retainedLoadedGroupIds,
};
}),

setDevices: (devices) => {
const normalizedDevices = normalizeDevices(devices);
const derivedGroups = deriveGroupsFromDevices(normalizedDevices);
set((state) => {
const mergedDevices = mergeDevicesWithExisting(normalizedDevices, state.devices);
const mergedGroups = normalizeGroups([...state.groups, ...derivedGroups]);
return {
groups: mergedGroups,
devices: normalizedDevices,
devices: mergedDevices,
loadedGroupIds: Array.from(
new Set([...state.loadedGroupIds, ...derivedGroups.map((group) => group.groupId)]),
),
Expand All @@ -155,14 +195,15 @@ export const usePlatformDeviceStore = create<PlatformDeviceState>((set) => ({

setDevicesForGroup: (groupId, devices) => {
const normalizedGroupId = normalizeGroupId(groupId);
const normalizedDevices = normalizeDevices(
devices.map((device) => ({
...device,
groupId: device.groupId || normalizedGroupId,
})),
);

set((state) => {
const normalizedDevices = mergeDevicesWithExisting(
devices.map((device) => ({
...device,
groupId: device.groupId || normalizedGroupId,
})),
state.devices,
);
const retainedDevices = state.devices.filter(
(device) => normalizeGroupId(device.groupId, device.groupName) !== normalizedGroupId,
);
Expand Down
2 changes: 1 addition & 1 deletion packages/widgets/chart/echarts-line/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ describe('chart/echarts-line widget', () => {

expect(latestOption?.xAxis?.type).toBe('time');
expect(latestOption?.series).toHaveLength(2);
expect(latestOption?.series?.[0]?.encode).toEqual({ x: 0, y: 1, tooltip: [1] });
expect(latestOption?.series?.[0]?.encode).toEqual({ x: 0, y: 1 });
expect(latestOption?.series?.[0]?.data).toEqual([
{ name: '2026-03-19T09:00:00.000Z', value: [1773910800000, 47.2] },
{ name: '2026-03-19T10:00:00.000Z', value: [1773914400000, 47.8] },
Expand Down
3 changes: 2 additions & 1 deletion packages/widgets/chart/echarts-line/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,8 @@ function buildOption(
return {
type: 'line',
name,
encode: isTimeSeries ? { x: 0, y: 1, tooltip: [1] } : undefined,
// Only x + y: adding tooltip: [1] duplicates the Y dimension in axis tooltips (same series shown twice).
encode: isTimeSeries ? { x: 0, y: 1 } : undefined,
data: buildSeriesData(normalized),
smooth: smooth,
showSymbol: false,
Expand Down
Loading