Skip to content

Commit d6ba332

Browse files
authored
feat: Simplify CogniteTimeSeries tab with single View dropdown (#642)
## Summary Simplifies the CogniteTimeSeries tab by replacing separate Space, Version, and View dropdowns with a single View dropdown that fetches views from the container inspect API. ## Changes ### CogniteTimeSeries Tab - **Before:** Users had to select Space, enter Version, then select View separately - **After:** Single View dropdown shows all views implementing the CogniteTimeSeries container - Views are fetched from `/models/containers/inspect` API endpoint - Dropdown shows format: `ViewName (space) version` ### Variable Query Editor - Switched data models list from GraphQL to REST API (`GET /models/datamodels`) - Added `includeGlobal=true` parameter to include global data models ### Tests - Updated e2e tests for new View dropdown UI - All CogniteTimeSeries tests passing (7/7) ## API Used ``` POST /models/containers/inspect { "items": [{ "space": "cdf_cdm", "externalId": "CogniteTimeSeries" }], "inspectionOperations": { "involvedViews": { "allVersions": true } } } ```
1 parent 2067635 commit d6ba332

File tree

7 files changed

+214
-181
lines changed

7 files changed

+214
-181
lines changed

src/cdf/client.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
DMSListRequest,
2222
DMSListResponse,
2323
CogniteUnit,
24+
InvolvedView,
25+
ContainerInspectResponse,
2426
} from '../types/dms';
2527
import {
2628
Tab,
@@ -620,4 +622,45 @@ export async function getTimeSeriesUnit(
620622
return undefined;
621623
}
622624

625+
// Fetch views that implement the CogniteTimeSeries container
626+
export async function fetchCogniteTimeSeriesViews(
627+
connector: Connector
628+
): Promise<InvolvedView[]> {
629+
try {
630+
const response = await retryOnRateLimit(() =>
631+
connector.fetchData<{ data: ContainerInspectResponse }>({
632+
method: HttpMethod.POST,
633+
path: '/models/containers/inspect',
634+
data: {
635+
items: [
636+
{
637+
space: 'cdf_cdm',
638+
externalId: 'CogniteTimeSeries',
639+
},
640+
],
641+
inspectionOperations: {
642+
involvedViews: {
643+
allVersions: true,
644+
},
645+
totalInvolvedViewCount: {
646+
allVersions: true,
647+
includeUnavailableViews: true,
648+
},
649+
},
650+
},
651+
cacheTime: CacheTime.ResourceByIds,
652+
})
653+
);
654+
655+
const item = response.data?.items?.[0];
656+
if (item?.inspectionResults?.involvedViews) {
657+
return item.inspectionResults.involvedViews;
658+
}
659+
return [];
660+
} catch (err) {
661+
console.warn('Failed to fetch CogniteTimeSeries views:', err);
662+
return [];
663+
}
664+
}
665+
623666

src/components/cogniteTimeSeriesSearchTab.tsx

Lines changed: 40 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React, { useState, useEffect, useCallback, useMemo } from 'react';
2-
import { Select, AsyncSelect, Alert, InlineFieldRow, InlineField, Input, InlineSwitch } from '@grafana/ui';
2+
import { Select, AsyncSelect, Alert, InlineFieldRow, InlineField, InlineSwitch } from '@grafana/ui';
33
import { SelectableValue } from '@grafana/data';
44
import { SelectedProps } from '../types';
5-
import { fetchDMSSpaces, fetchDMSViews, searchDMSInstances, fetchCogniteUnits, getTimeSeriesUnit, stringifyError } from '../cdf/client';
6-
import { DMSSpace, DMSView, DMSInstance, DMSSearchRequest, CogniteUnit } from '../types/dms';
5+
import { searchDMSInstances, fetchCogniteUnits, getTimeSeriesUnit, stringifyError, fetchCogniteTimeSeriesViews } from '../cdf/client';
6+
import { DMSInstance, DMSSearchRequest, CogniteUnit, InvolvedView } from '../types/dms';
77
import { CommonEditors, LabelEditor } from './commonEditors';
88
import { Connector } from '../connector';
99

@@ -36,8 +36,7 @@ export const CogniteTimeSeriesSearchTab: React.FC<CogniteTimeSeriesSearchTabProp
3636
onQueryChange,
3737
connector,
3838
}) => {
39-
const [spaces, setSpaces] = useState<SelectableValue[]>([]);
40-
const [views, setViews] = useState<SelectableValue[]>([]);
39+
const [viewOptions, setViewOptions] = useState<Array<SelectableValue<InvolvedView>>>([]);
4140
const [loading, setLoading] = useState(false);
4241
const [error, setError] = useState<string | null>(null);
4342
const [units, setUnits] = useState<CogniteUnit[]>([]);
@@ -54,37 +53,19 @@ export const CogniteTimeSeriesSearchTab: React.FC<CogniteTimeSeriesSearchTabProp
5453
[cogniteTimeSeries.instanceId]
5554
);
5655

57-
const loadSpaces = useCallback(async () => {
56+
const loadViews = useCallback(async () => {
5857
try {
5958
setLoading(true);
60-
const spacesData = await fetchDMSSpaces(connector);
61-
const spaceOptions = spacesData.map((space: DMSSpace) => ({
62-
label: space.name || space.space,
63-
value: space.space,
64-
description: space.description,
59+
const views = await fetchCogniteTimeSeriesViews(connector);
60+
const options = views.map((view: InvolvedView) => ({
61+
label: `${view.externalId} (${view.space}) ${view.version}`,
62+
value: view,
63+
description: `Space: ${view.space}, Version: ${view.version}`,
6564
}));
66-
setSpaces(spaceOptions);
65+
setViewOptions(options);
6766
setError(null);
6867
} catch (err) {
69-
setError(`Failed to load spaces: ${stringifyError(err)}`);
70-
} finally {
71-
setLoading(false);
72-
}
73-
}, [connector]);
74-
75-
const loadViews = useCallback(async (space: string) => {
76-
try {
77-
setLoading(true);
78-
const viewsData = await fetchDMSViews(connector, space);
79-
const viewOptions = viewsData.map((view: DMSView) => ({
80-
label: view.name || view.externalId,
81-
value: view.externalId,
82-
description: view.description,
83-
}));
84-
setViews(viewOptions);
85-
setError(null);
86-
} catch (err) {
87-
setError(`Failed to load views for space ${space}: ${stringifyError(err)}`);
68+
setError(`Failed to load CogniteTimeSeries views: ${stringifyError(err)}`);
8869
} finally {
8970
setLoading(false);
9071
}
@@ -135,14 +116,8 @@ export const CogniteTimeSeriesSearchTab: React.FC<CogniteTimeSeriesSearchTabProp
135116
}, [connector, cogniteTimeSeries.space, cogniteTimeSeries.externalId, cogniteTimeSeries.version]);
136117

137118
useEffect(() => {
138-
loadSpaces();
139-
}, [loadSpaces]);
140-
141-
useEffect(() => {
142-
if (cogniteTimeSeries.space) {
143-
loadViews(cogniteTimeSeries.space);
144-
}
145-
}, [cogniteTimeSeries.space, loadViews]);
119+
loadViews();
120+
}, [loadViews]);
146121

147122
// Load available units
148123
useEffect(() => {
@@ -182,33 +157,15 @@ export const CogniteTimeSeriesSearchTab: React.FC<CogniteTimeSeriesSearchTabProp
182157
fetchUnit();
183158
}, [connector, instanceIdKey, cogniteTimeSeries.instanceId]);
184159

185-
const handleSpaceChange = (selectedSpace: SelectableValue | null) => {
186-
onQueryChange({
187-
cogniteTimeSeries: {
188-
...cogniteTimeSeries,
189-
space: selectedSpace?.value || '',
190-
externalId: '', // Reset external ID when space changes
191-
instanceId: undefined, // Reset selected timeseries
192-
},
193-
});
194-
};
195-
196-
const handleVersionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
197-
onQueryChange({
198-
cogniteTimeSeries: {
199-
...cogniteTimeSeries,
200-
version: event.target.value,
201-
instanceId: undefined, // Reset selected timeseries when version changes
202-
},
203-
});
204-
};
205-
206-
const handleExternalIdChange = (selectedView: SelectableValue | null) => {
160+
const handleViewChange = (selectedOption: SelectableValue<InvolvedView> | null) => {
161+
const view = selectedOption?.value;
207162
onQueryChange({
208163
cogniteTimeSeries: {
209164
...cogniteTimeSeries,
210-
externalId: selectedView?.value || '',
211-
instanceId: undefined, // Reset selected timeseries
165+
space: view?.space || '',
166+
externalId: view?.externalId || '',
167+
version: view?.version || '',
168+
instanceId: undefined, // Reset selected timeseries when view changes
212169
},
213170
});
214171
};
@@ -288,56 +245,36 @@ export const CogniteTimeSeriesSearchTab: React.FC<CogniteTimeSeriesSearchTabProp
288245

289246
const isUnitConversionEnabled = !!timeSeriesUnit && !!cogniteTimeSeries.instanceId;
290247

248+
// Find the currently selected view option
249+
const selectedViewOption = useMemo(() => {
250+
if (cogniteTimeSeries.space && cogniteTimeSeries.externalId && cogniteTimeSeries.version) {
251+
return viewOptions.find(
252+
(opt) =>
253+
opt.value?.space === cogniteTimeSeries.space &&
254+
opt.value?.externalId === cogniteTimeSeries.externalId &&
255+
opt.value?.version === cogniteTimeSeries.version
256+
);
257+
}
258+
return null;
259+
}, [viewOptions, cogniteTimeSeries.space, cogniteTimeSeries.externalId, cogniteTimeSeries.version]);
260+
291261
return (
292262
<div>
293263
<div className="gf-form-group">
294-
<InlineFieldRow>
295-
<InlineField
296-
label="Space"
297-
labelWidth={14}
298-
tooltip="Select the space to search in"
299-
>
300-
<Select
301-
options={spaces}
302-
value={cogniteTimeSeries.space}
303-
onChange={handleSpaceChange}
304-
placeholder="Select space"
305-
isClearable
306-
isLoading={loading}
307-
width={20}
308-
/>
309-
</InlineField>
310-
</InlineFieldRow>
311-
312-
<InlineFieldRow>
313-
<InlineField
314-
label="Version"
315-
labelWidth={14}
316-
tooltip="Version of the view"
317-
>
318-
<Input
319-
value={cogniteTimeSeries.version}
320-
onChange={handleVersionChange}
321-
placeholder="v1"
322-
width={20}
323-
/>
324-
</InlineField>
325-
</InlineFieldRow>
326-
327264
<InlineFieldRow>
328265
<InlineField
329266
label="View"
330267
labelWidth={14}
331-
tooltip="Select the view to search in"
268+
tooltip="Select a CogniteTimeSeries view to search in"
332269
>
333270
<Select
334-
options={views}
335-
value={cogniteTimeSeries.externalId}
336-
onChange={handleExternalIdChange}
337-
placeholder="Select view"
271+
options={viewOptions}
272+
value={selectedViewOption}
273+
onChange={handleViewChange}
274+
placeholder="Select a CogniteTimeSeries view"
338275
isClearable
339276
isLoading={loading}
340-
width={20}
277+
width={40}
341278
/>
342279
</InlineField>
343280
</InlineFieldRow>
@@ -349,7 +286,7 @@ export const CogniteTimeSeriesSearchTab: React.FC<CogniteTimeSeriesSearchTabProp
349286
tooltip="Search for timeseries by name or description"
350287
>
351288
<AsyncSelect
352-
key={`${cogniteTimeSeries.space}-${cogniteTimeSeries.version}-${cogniteTimeSeries.externalId}`}
289+
key={`${cogniteTimeSeries.space}-${cogniteTimeSeries.externalId}-${cogniteTimeSeries.version}`}
353290
loadOptions={searchTimeseries}
354291
value={getCurrentTimeseriesValue()}
355292
onChange={handleTimeseriesSelection}

src/datasources/FlexibleDataModelingDatasource.spec.ts

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,28 @@ describe('FlexibleDataModellingDatasource', () => {
1313

1414
describe('listFlexibleDataModelling', () => {
1515
it('should return data when successful', async () => {
16-
// Mock the fetchQuery method of the connector to return a successful response
17-
connectorMock.fetchQuery = jest.fn().mockResolvedValue({
18-
data: {
19-
listGraphQlDmlVersions: {
20-
items: [
21-
{
22-
space: 'test',
23-
externalId: 'test',
24-
version: '1',
25-
name: 'test',
26-
description: 'test',
27-
graphQlDml: 'test',
28-
createdTime: 'test',
29-
lastUpdatedTime: 'test',
30-
},
31-
],
32-
},
16+
// Mock the fetchItems method of the connector to return a successful response
17+
// Uses REST API: GET /models/datamodels with includeGlobal=true
18+
connectorMock.fetchItems = jest.fn().mockResolvedValue([
19+
{
20+
space: 'test',
21+
externalId: 'test',
22+
version: '1',
23+
name: 'test',
24+
description: 'test',
3325
},
34-
});
26+
]);
3527

3628
const result = await datasource.listFlexibleDataModelling('test-ref-id');
3729

3830
expect(result.listGraphQlDmlVersions.items).toHaveLength(1);
31+
expect(result.listGraphQlDmlVersions.items[0].space).toBe('test');
32+
expect(result.listGraphQlDmlVersions.items[0].externalId).toBe('test');
3933
});
4034

4135
it('should return an empty list when an error occurs', async () => {
42-
// Mock the fetchQuery method of the connector to throw an error
43-
connectorMock.fetchQuery = jest.fn().mockRejectedValue(new Error('Test error'));
36+
// Mock the fetchItems method of the connector to throw an error
37+
connectorMock.fetchItems = jest.fn().mockRejectedValue(new Error('Test error'));
4438

4539
const result = await datasource.listFlexibleDataModelling('test-ref-id');
4640

src/datasources/FlexibleDataModellingDatasource.ts

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -65,37 +65,37 @@ export class FlexibleDataModellingDatasource {
6565
}>
6666
> {
6767
try {
68-
const { data } = await this.connector.fetchQuery<{
68+
// Use REST API instead of GraphQL to support includeGlobal parameter
69+
// https://api-docs.cognite.com/20230101/tag/Data-models/operation/listDataModels
70+
const items = await this.connector.fetchItems<{
6971
space: string;
7072
externalId: string;
7173
version: string;
7274
name: string;
7375
description: string;
74-
graphQlDml: string;
76+
isGlobal?: boolean;
7577
}>({
76-
path: '/dml/graphql',
77-
method: HttpMethod.POST,
78-
data: JSON.stringify({
79-
query: `
80-
query listDataModelVersions($limit: Int) {
81-
listGraphQlDmlVersions(limit: $limit) {
82-
items {
83-
space
84-
externalId
85-
version
86-
name
87-
description
88-
graphQlDml
89-
createdTime
90-
lastUpdatedTime
91-
}
92-
}
93-
}
94-
`,
95-
variables: { limit: 1000 },
96-
}),
78+
path: '/models/datamodels',
79+
method: HttpMethod.GET,
80+
data: undefined,
81+
params: { limit: 1000, includeGlobal: true },
9782
});
98-
return data;
83+
84+
// Map to expected format (graphQlDml will be fetched separately when needed)
85+
const mappedItems = items.map((item) => ({
86+
space: item.space,
87+
externalId: item.externalId,
88+
version: item.version,
89+
name: item.name || item.externalId,
90+
description: item.description || '',
91+
graphQlDml: '', // Will be fetched when a specific model is selected
92+
}));
93+
94+
return {
95+
listGraphQlDmlVersions: {
96+
items: mappedItems,
97+
},
98+
};
9999
} catch (error) {
100100
handleError(error, refId);
101101
return {

0 commit comments

Comments
 (0)