Skip to content

Commit bd44c36

Browse files
authored
Cell composition part 1 (#369)
* add logic to send brain region with ancestors in list query * use atom for entities counts instead of promise * add new version of brain regions tree * dynamically render explore listing view * add cell-composition to explore interactive * use mtype etype alt_label on tree item * clean unneeded files for v2/brain-region-tree * remove unnecessary pages * use brain atlas to calculate density and count using volume * fix notification to use the App context (#400)
1 parent b5a7cd1 commit bd44c36

File tree

61 files changed

+1546
-325
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1546
-325
lines changed

.env.development

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,9 @@ MAILCHIMP_AUDIENCE_ID='dummy'
2020
MAILCHIMP_API_SERVER='dummy'
2121

2222
# AI Agent service from Machine Learning team
23-
NEXT_PUBLIC_AI_AGENT_URL="https://staging.openbraininstitute.org/api/agent/"
23+
NEXT_PUBLIC_AI_AGENT_URL="https://staging.openbraininstitute.org/api/agent/"
24+
25+
NEXT_PUBLIC_DEFAULT_BRAIN_REGION_HIERARCHY_ID="e3e70682-c209-4cac-a29f-6fbed82c07cd"
26+
NEXT_PUBLIC_DEFAULT_SELECTED_BRAIN_REGION_ID="4642cddb-4fbe-4aae-bbf7-0946d6ada066"
27+
NEXT_PUBLIC_DEFAULT_ROOT_BRAIN_REGION_ANNOTATION_VALUE=8
28+
NEXT_PUBLIC_DEFAULT_BRAIN_ATLAS_ID="a25231be-54c0-4a14-a89a-a1b1c7bd5837"

__tests__/ExploreInteractive/TabsWithStatPanel.spec.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import { mockBrainRegions } from '__tests__/__utils__/SelectedBrainRegions';
1010
import { DataType } from '@/constants/explore-section/list-views';
1111
import { EXPERIMENT_DATA_TYPE_CONFIG } from '@/constants/explore-section/data-types/experiment-data-types';
1212
import { DATA_TYPES_TO_CONFIGS } from '@/constants/explore-section/data-types';
13-
import DataTypeTabs, {
13+
import EntityGroupTabs, {
1414
dataTabAtom,
15-
} from '@/components/explore-section/ExploreInteractive/DataTypeTabs';
15+
} from '@/components/explore-section/ExploreInteractive/interactive/entity-group-tab';
1616
import DataTypeStatPanel from '@/components/entities-type-stats/panel';
1717

1818
jest.mock('next/navigation', () => ({
@@ -178,7 +178,7 @@ describe('SelectedBrainRegionPanel', () => {
178178
{mockBrainRegions.map((brainRegion) => (
179179
<VizButtons key={brainRegion.id} brainRegion={brainRegion} />
180180
))}
181-
<DataTypeTabs />
181+
<EntityGroupTabs />
182182
<DataTypeStatPanel />
183183
</TestProvider>
184184
);

src/api/apiClient.ts

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import omitBy from 'lodash/omitBy';
22
import isNil from 'lodash/isNil';
33

4-
import { getSession } from '@/authFetch';
54
import { compactRecord } from '@/utils/dictionary';
5+
import { getSession } from '@/authFetch';
6+
import { log } from '@/utils/logger';
7+
import { env } from '@/env';
68

79
type BackoffStrategy = {
810
type: 'exponential' | 'custom';
@@ -152,7 +154,7 @@ class ApiClient {
152154
response: cachedResponse,
153155
};
154156
} catch (e) {
155-
console.warn('Cache API access failed:', e);
157+
log('warn', 'Cache API access failed:', e);
156158
return { valid: false, response: null };
157159
}
158160
}
@@ -190,7 +192,7 @@ class ApiClient {
190192

191193
await cache.put(url, cachedResponseToStore);
192194
} catch (e) {
193-
console.warn('Failed to store in cache:', e);
195+
log('warn', 'Failed to store in cache:', e);
194196
}
195197
}
196198

@@ -242,7 +244,7 @@ class ApiClient {
242244
);
243245

244246
if (valid && cachedResponse) {
245-
console.debug(`[cached] ${urlString}`);
247+
log('debug', `[cached] ${urlString}`);
246248
const contentType = cachedResponse.headers.get('Content-Type') || '';
247249
if (config.asRawResponse) {
248250
return cachedResponse as unknown as T;
@@ -261,7 +263,7 @@ class ApiClient {
261263

262264
// if cache is invalid or expired, continue with the request
263265
if (cachedResponse) {
264-
console.log(`Cache expired for ${urlString}, fetching fresh data`);
266+
log('log', `Cache expired for ${urlString}, fetching fresh data`);
265267
}
266268
}
267269

@@ -274,12 +276,15 @@ class ApiClient {
274276
...(this._token ? { Authorization: `Bearer ${this._token}` } : {}),
275277
...options.headers,
276278
}),
277-
body:
278-
options.body && !(options.body instanceof FormData)
279-
? JSON.stringify(options.body)
280-
: options.body
281-
? options.body
282-
: undefined,
279+
body: (() => {
280+
if (!options.body) {
281+
return undefined;
282+
}
283+
if (options.body instanceof FormData) {
284+
return options.body;
285+
}
286+
return JSON.stringify(options.body);
287+
})(),
283288
signal: options.signal,
284289
cache: options.cache,
285290
next: options.next,
@@ -297,7 +302,10 @@ class ApiClient {
297302

298303
if (!response.ok && (config.retryOnError ?? this._retryOnError) && attempt < maxAttempts) {
299304
const delay = this.calculateBackoff(attempt, config.backoff ?? this._backoff);
300-
await new Promise((resolve) => setTimeout(resolve, delay));
305+
log('log', 'Retrying request in', delay, 'ms');
306+
await new Promise((resolve) => {
307+
setTimeout(resolve, delay);
308+
});
301309
return runRequest();
302310
}
303311

@@ -324,10 +332,19 @@ class ApiClient {
324332
if ((config.retryOnError ?? this._retryOnError) && attempt < maxAttempts) {
325333
const delay = this.calculateBackoff(attempt, config.backoff ?? this._backoff);
326334

327-
await new Promise((resolve) => setTimeout(resolve, delay));
335+
await new Promise((resolve) => {
336+
setTimeout(resolve, delay);
337+
});
328338
return runRequest();
329339
}
330-
340+
if (env.NEXT_PUBLIC_DEPLOYMENT_ENV !== 'production') {
341+
log('error', 'Request failed', {
342+
status: response.status,
343+
message:
344+
(responseData as any).message || `Request failed with status ${response.status}`,
345+
data: responseData,
346+
});
347+
}
331348
throw Error(`Request ${request.url} failed `, {
332349
cause: {
333350
status: response.status,
@@ -350,7 +367,9 @@ class ApiClient {
350367
}
351368
if ((config.retryOnException ?? this._retryOnException) && attempt < maxAttempts) {
352369
const delay = this.calculateBackoff(attempt, config.backoff ?? this._backoff);
353-
await new Promise((resolve) => setTimeout(resolve, delay));
370+
await new Promise((resolve) => {
371+
setTimeout(resolve, delay);
372+
});
354373
return this._request<T>(method, endpoint, options, { ...config, attempts: attempt + 1 });
355374
}
356375
throw error;
@@ -397,7 +416,7 @@ class ApiClient {
397416

398417
return true;
399418
} catch (e) {
400-
console.error('Failed to clear cache:', e);
419+
log('error', 'Failed to clear cache:', e);
401420
return false;
402421
}
403422
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import authApiClient from '@/api/apiClient';
2+
import { getEntityCoreContext } from '@/api/entitycore/utils';
3+
import { entityCoreUrl } from '@/config';
4+
5+
import type { EntityCoreResponse } from '@/api/entitycore/types/shared/response';
6+
import type { IEType, IETypeFilter } from '@/api/entitycore/types/shared/global';
7+
import type { WorkspaceContext } from '@/types/common';
8+
9+
const baseUri = '/etype';
10+
/**
11+
* Retrieves a list of etypes from the EntityCoreAPI.
12+
13+
* @returns {Promise<EntityCoreResponse<IEType>>} A promise that resolves to the list of etypes
14+
*/
15+
export async function getEtypes({
16+
filters,
17+
ctx,
18+
}: {
19+
filters?: IETypeFilter;
20+
ctx?: WorkspaceContext;
21+
}) {
22+
const api = await authApiClient(entityCoreUrl);
23+
return await api.get<EntityCoreResponse<IEType>>(baseUri, {
24+
...getEntityCoreContext(ctx),
25+
queryParams: {
26+
...filters,
27+
},
28+
});
29+
}
30+
31+
/**
32+
* Retrieves one etype from the EntityCoreAPI.
33+
34+
* @returns {Promise<IEType>} A promise that resolves to the single etype
35+
*/
36+
export async function getEtype({ id }: { id: string }) {
37+
const api = await authApiClient(entityCoreUrl);
38+
return await api.get<IEType>(`${baseUri}/${id}`);
39+
}

src/api/entitycore/queries/annotations/mtype.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
import authApiClient from '@/api/apiClient';
2-
import { EntityCoreResponse } from '@/api/entitycore/types/shared/response';
3-
import { IMType, IMtypeFilter } from '@/api/entitycore/types/shared/global';
42
import { getEntityCoreContext } from '@/api/entitycore/utils';
53
import { entityCoreUrl } from '@/config';
64

5+
import type { EntityCoreResponse } from '@/api/entitycore/types/shared/response';
6+
import type { IMType, IMTypeFilter } from '@/api/entitycore/types/shared/global';
7+
import type { WorkspaceContext } from '@/types/common';
8+
79
const baseUri = '/mtype';
810
/**
911
* Retrieves a list of mtypes from the EntityCoreAPI.
1012
1113
* @returns {Promise<EntityCoreResponse<IMType>>} A promise that resolves to the list of mtypes
1214
*/
13-
export async function getMtypes({ filters }: { filters?: IMtypeFilter }) {
15+
export async function getMtypes({
16+
filters,
17+
ctx,
18+
}: {
19+
filters?: IMTypeFilter;
20+
ctx: WorkspaceContext;
21+
}) {
1422
const api = await authApiClient(entityCoreUrl);
1523
return await api.get<EntityCoreResponse<IMType>>(baseUri, {
16-
...getEntityCoreContext(),
24+
...getEntityCoreContext(ctx),
1725
queryParams: {
1826
...filters,
1927
},
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { EntityCoreResponse } from '../../types/shared/response';
2+
import { entityCoreApi, getEntityCoreContext } from '@/api/entitycore/utils';
3+
import type {
4+
IBrainAtlas,
5+
IBrainAtlasRegion,
6+
IBrainAtlasFilter,
7+
IBrainAtlasRegionFilter,
8+
} from '@/api/entitycore/types/entities/brain-atlas';
9+
import { WorkspaceContext } from '@/types/common';
10+
11+
const baseUri = '/brain-atlas';
12+
13+
/**
14+
* Retrieves a list of brain atlases from the Entity Core API.
15+
*
16+
* @param params - The parameters for the request.
17+
* @param params.filters - Optional filters to apply to the brain atlas query.
18+
* @param params.context - Optional workspace context for the request.
19+
* @returns A promise that resolves to the EntityCoreResponse containing brain atlas data.
20+
*/
21+
export async function getBrainAtlases({
22+
filters,
23+
context,
24+
}: {
25+
filters?: IBrainAtlasFilter;
26+
context?: WorkspaceContext | null;
27+
}) {
28+
const api = await entityCoreApi();
29+
return await api.get<EntityCoreResponse<IBrainAtlas>>(baseUri, {
30+
queryParams: {
31+
...filters,
32+
},
33+
headers: {
34+
accept: 'application/json',
35+
'content-type': 'application/json',
36+
...getEntityCoreContext(context).headers,
37+
},
38+
});
39+
}
40+
41+
/**
42+
* Retrieves a Brain Atlas entity by its ID from the Entity Core API.
43+
*
44+
* @param params - The parameters for the request.
45+
* @param params.id - The unique identifier of the Brain Atlas to retrieve.
46+
* @param params.context - Optional workspace context for the request, which may include authentication and other headers.
47+
* @returns A promise that resolves to the requested `IBrainAtlas` object.
48+
*/
49+
export async function getBrainAtlas({
50+
id,
51+
context,
52+
}: {
53+
id: string;
54+
context?: WorkspaceContext | null;
55+
}) {
56+
const api = await entityCoreApi();
57+
return await api.get<IBrainAtlas>(`${baseUri}/${id}`, {
58+
headers: {
59+
accept: 'application/json',
60+
'content-type': 'application/json',
61+
...getEntityCoreContext(context).headers,
62+
},
63+
});
64+
}
65+
66+
/**
67+
* Retrieves the regions for a specified brain atlas.
68+
*
69+
* @param params - The parameters for the request.
70+
* @param params.atlasId - The unique identifier of the brain atlas.
71+
* @param params.filters - Optional filters to apply when retrieving regions.
72+
* @param params.context - Optional workspace context for the request.
73+
* @returns A promise that resolves to the response containing brain atlas regions.
74+
*/
75+
export async function getBrainAtlasRegions({
76+
atlasId,
77+
filters,
78+
context,
79+
}: {
80+
atlasId: string;
81+
filters?: Partial<IBrainAtlasRegionFilter>;
82+
context?: WorkspaceContext | null;
83+
}) {
84+
const api = await entityCoreApi();
85+
return await api.get<EntityCoreResponse<IBrainAtlasRegion>>(`${baseUri}/${atlasId}/regions`, {
86+
queryParams: {
87+
...filters,
88+
},
89+
headers: {
90+
accept: 'application/json',
91+
'content-type': 'application/json',
92+
...getEntityCoreContext(context).headers,
93+
},
94+
});
95+
}
96+
97+
/**
98+
* Fetches a specific brain atlas region by its ID and atlas ID.
99+
*
100+
* @param params - The parameters for the request.
101+
* @param params.id - The unique identifier of the brain atlas region.
102+
* @param params.atlasId - The unique identifier of the brain atlas.
103+
* @param params.context - Optional workspace context for the request.
104+
* @returns A promise that resolves to the brain atlas region data wrapped in an EntityCoreResponse.
105+
*/
106+
export async function getBrainAtlasRegion({
107+
id,
108+
atlasId,
109+
context,
110+
}: {
111+
id: string;
112+
atlasId: string;
113+
context?: WorkspaceContext | null;
114+
}) {
115+
const api = await entityCoreApi();
116+
return await api.get<EntityCoreResponse<IBrainAtlasRegion>>(
117+
`${baseUri}/${atlasId}/regions/${id}`,
118+
{
119+
headers: {
120+
accept: 'application/json',
121+
'content-type': 'application/json',
122+
...getEntityCoreContext(context).headers,
123+
},
124+
}
125+
);
126+
}

src/api/entitycore/queries/general/brain-region.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,11 @@ export async function getTemporaryBrainRegionHierarchy<T extends boolean>(
1919
const api = await entityCoreApi(); // cached it for 1 day
2020
return await api.get<
2121
T extends true ? TemporaryFlatBrainRegionHierarchy : ITemporaryBrainRegionHierarchy
22-
>(
23-
'/brain-region',
24-
{
25-
queryParams: {
26-
flat,
27-
},
22+
>('/brain-region', {
23+
queryParams: {
24+
flat,
2825
},
29-
{
30-
cache: { cacheName: 'brain-region', enabled: true, ttlInSeconds: 86_400 },
31-
}
32-
);
26+
});
3327
}
3428

3529
/**
@@ -57,11 +51,5 @@ export async function getBrainRegionHierarchy({
5751
id?: string;
5852
}) {
5953
const api = await entityCoreApi();
60-
return await api.get<IBrainRegionHierarchy>(
61-
`/brain-region-hierarchy/${id}/hierarchy`,
62-
{},
63-
{
64-
cache: { cacheName: 'brain-region-hierarchy', enabled: true, ttlInSeconds: 86_400 },
65-
}
66-
);
54+
return await api.get<IBrainRegionHierarchy>(`/brain-region-hierarchy/${id}/hierarchy`);
6755
}

0 commit comments

Comments
 (0)