Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react-components): select single model simple list when has no scene #4746

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
de144fe
add simple list + button to select single 3d model
danpriori Sep 5, 2024
bc20f9c
improve the all 3d model list and add the revision and model id fetchers
danpriori Sep 8, 2024
743e6ea
cleanup
danpriori Sep 8, 2024
e27c037
lint
danpriori Sep 8, 2024
07bd25f
Merge branch 'master' into danpriori/BND3D-3961-select-single-model-s…
danpriori Sep 8, 2024
44a0674
update toolbar screenshot
danpriori Sep 8, 2024
f51740e
changes from cr
danpriori Sep 9, 2024
f8e536b
refactoring and add initial useRevisions utest
danpriori Sep 9, 2024
add9cc0
add initial utest for all3dmodels hook
danpriori Sep 9, 2024
e84ecda
utest refactoring
danpriori Sep 9, 2024
82cf0b3
Merge branch 'master' into danpriori/BND3D-3961-select-single-model-s…
danpriori Sep 9, 2024
17dfe70
Merge remote-tracking branch 'origin/master' into danpriori/BND3D-396…
danpriori Sep 10, 2024
4b52c09
Merge branch 'danpriori/BND3D-3961-select-single-model-simple-list' o…
danpriori Sep 10, 2024
612d255
cr changes
danpriori Sep 13, 2024
e1bf69b
Merge remote-tracking branch 'origin/master' into danpriori/BND3D-396…
danpriori Sep 13, 2024
cc1179e
remove unnecessary extra context
danpriori Sep 13, 2024
12aa21f
Merge remote-tracking branch 'origin/master' into danpriori/BND3D-396…
danpriori Sep 17, 2024
42bd5c9
fix query keys and return the query parameters to the default
danpriori Sep 23, 2024
d6c6da6
move resource selection type definitions to the react components and …
danpriori Sep 23, 2024
bfb2f78
Merge remote-tracking branch 'origin/master' into danpriori/BND3D-396…
danpriori Sep 24, 2024
ffd5aa2
move exports
danpriori Sep 24, 2024
f4ae0d6
refactoring to support changes for single model selection - wip
danpriori Sep 25, 2024
b09014c
cleanup and update type
danpriori Sep 28, 2024
8911551
add callback for any resource is loaded
danpriori Sep 28, 2024
a43f6c2
add loading icon and execute parallel for revisions
danpriori Oct 1, 2024
477d80b
add search input and filtering
danpriori Oct 2, 2024
072eae5
Merge remote-tracking branch 'origin/master' into danpriori/BND3D-396…
danpriori Oct 2, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"RULESET_NO_SELECTION": "No Rule Set Selected",
"RULESET_SELECT_HEADER": "Select color overlay",
"SCENE_SELECT_HEADER": "Select 3D location",
"MODEL_SELECT_HEADER": "Select 3D model",
"SEARCH_PLACEHOLDER": "Search",
"SELECT_INDIVIDUAL_3D_MODELS": "Select individual models",
"SETTINGS_TOOLTIP": "Settings",
Expand Down
39 changes: 39 additions & 0 deletions react-components/src/components/RevealToolbar/ModelsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*!
* Copyright 2024 Cognite AS
*/
import { type ReactElement } from 'react';

import { Menu } from '@cognite/cogs.js';
import { type ModelWithRevision } from '../../hooks/types';

type ModelListProps = {
modelsWithRevision: ModelWithRevision[];
selectedModel: ModelWithRevision | undefined;
onModelChange: (model: ModelWithRevision | undefined) => void;
};

export const ModelsList = ({
modelsWithRevision,
selectedModel,
onModelChange
}: ModelListProps): ReactElement => {
if (modelsWithRevision.length === 0) {
return <></>;
}
return (
<>
{modelsWithRevision.map((modelData) => {
return (
<Menu.Item
key={`${modelData.model.id}`}
toggled={selectedModel?.model.id === modelData.model.id}
onClick={() => {
onModelChange(modelData);
}}>
{modelData.model?.name}
</Menu.Item>
);
})}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ResetCameraButton } from './ResetCameraButton';
import { type QualitySettings } from './SettingsContainer/types';
import styled from 'styled-components';
import { SelectSceneButton } from './SelectSceneButton';
import { SelectSingle3DModelButton } from './SelectSingle3DModelButton';
import { RuleBasedOutputsButton } from './RuleBasedOutputsButton';
import {
SetFlexibleControlsType,
Expand Down Expand Up @@ -48,6 +49,7 @@ const DefaultContentWrapper = (props: CustomToolbarContent): ReactElement => {
<LayersButton />
<FitModelsButton />
<RuleBasedOutputsButton />
<SelectSingle3DModelButton />
<Divider weight="2px" length="75%" />

<SlicerButton />
Expand Down Expand Up @@ -108,6 +110,7 @@ export const RevealToolbar = withSuppressRevealEvents(
HelpButton: typeof HelpButton;
ResetCameraButton: typeof ResetCameraButton;
SelectSceneButton: typeof SelectSceneButton;
SelectSingle3DModelButton: typeof SelectSingle3DModelButton;
RuleBasedOutputsButton: typeof RuleBasedOutputsButton;
AssetContextualizedButton: typeof AssetContextualizedButton;
SetOrbitOrFirstPersonControlsType: typeof SetOrbitOrFirstPersonControlsType;
Expand All @@ -122,6 +125,7 @@ RevealToolbar.SettingsButton = SettingsButton;
RevealToolbar.HelpButton = HelpButton;
RevealToolbar.ResetCameraButton = ResetCameraButton;
RevealToolbar.SelectSceneButton = SelectSceneButton;
RevealToolbar.SelectSingle3DModelButton = SelectSingle3DModelButton;
RevealToolbar.RuleBasedOutputsButton = RuleBasedOutputsButton;
RevealToolbar.AssetContextualizedButton = AssetContextualizedButton;
RevealToolbar.SetOrbitOrFirstPersonControlsType = SetOrbitOrFirstPersonControlsType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*!
* Copyright 2024 Cognite AS
*/
import { type ReactElement } from 'react';

import { useSDK } from '../RevealCanvas/SDKProvider';
import { RevealContext } from '../RevealContext/RevealContext';
import { type ModelWithRevision } from '../../hooks/types';
import { Single3DModelSelection } from './SelectSingle3DModelSelection';

type SelectSingle3DModelButtonProps = {
onSingleModelChanged?: (model: ModelWithRevision | undefined) => void;
};

export const SelectSingle3DModelButton = ({
onSingleModelChanged
}: SelectSingle3DModelButtonProps): ReactElement => {
const sdk = useSDK();

const handleSingleModelChange = (model: ModelWithRevision | undefined): void => {
if (onSingleModelChanged !== undefined) onSingleModelChanged(model);
};

return (
<RevealContext sdk={sdk}>
Copy link
Contributor

@pramodcog pramodcog Sep 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should the component be under RevealContext?
As SelectSingle3DModelButton is exported under RevealToolbar which should be within RevealContext, I do not think it is necessary

Copy link
Contributor Author

@danpriori danpriori Sep 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm strange. I only added because it was always complaining about without the context. Even under revealToolbar. Now is working without it.🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<Single3DModelSelection sdk={sdk} onModelChange={handleSingleModelChange} />
</RevealContext>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*!
* Copyright 2024 Cognite AS
*/
import { useState, type ReactElement } from 'react';

import { Button, Dropdown, Menu, Tooltip as CogsTooltip } from '@cognite/cogs.js';
import { useTranslation } from '../i18n/I18n';
import styled from 'styled-components';
import { useAll3dModels } from '../../hooks/useAll3dModels';
import { type CogniteClient } from '@cognite/sdk';
import { ModelsList } from './ModelsList';
import { useRevisions } from '../../hooks/useRevisions';
import { type ModelWithRevision } from '../../hooks/types';

type Single3DModelSelectionProps = {
sdk: CogniteClient;
onModelChange: (model: ModelWithRevision | undefined) => void;
};

export const Single3DModelSelection = ({
sdk,
onModelChange
}: Single3DModelSelectionProps): ReactElement => {
const { data: models } = useAll3dModels(sdk, true);

const { data: modelsWithRevision } = useRevisions(sdk, models);

const [selectedModel, setSelectedModel] = useState<ModelWithRevision | undefined>();

const { t } = useTranslation();

const handleSelectedModelChange = (model: ModelWithRevision | undefined): void => {
danpriori marked this conversation as resolved.
Show resolved Hide resolved
setSelectedModel(model);
onModelChange(model);
};

return (
<CogsTooltip
content={t('MODEL_SELECT_HEADER', 'Select 3D model')}
placement="right"
appendTo={document.body}>
<Dropdown
placement="right-start"
content={
<StyledMenu>
<Menu.Header>{t('MODEL_SELECT_HEADER', 'Select 3D model')}</Menu.Header>
<ModelsList
modelsWithRevision={modelsWithRevision ?? []}
selectedModel={selectedModel}
onModelChange={handleSelectedModelChange}
/>
</StyledMenu>
danpriori marked this conversation as resolved.
Show resolved Hide resolved
}>
<Button icon="World" aria-label="Select 3D model" type="ghost"></Button>
</Dropdown>
</CogsTooltip>
);
};

const StyledMenu = styled(Menu)`
max-height: 400px;
overflow: auto;
`;
8 changes: 8 additions & 0 deletions react-components/src/hooks/network/get3dModels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*!
* Copyright 2024 Cognite AS
*/
import { type CogniteClient, type Model3D } from '@cognite/sdk';

export const get3dModels = async (sdk: CogniteClient): Promise<Model3D[]> => {
return await sdk.models3D.list().autoPagingToArray({ limit: Infinity });
};
19 changes: 19 additions & 0 deletions react-components/src/hooks/network/getRevisions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*!
* Copyright 2024 Cognite AS
*/
import { type Model3D, type CogniteClient, type Revision3D } from '@cognite/sdk';
import { type ModelWithRevision } from '../types';

export const getRevisions = async (
model: Model3D | undefined,
sdk: CogniteClient
): Promise<ModelWithRevision | undefined> => {
if (model === undefined) return;
const revisions = await sdk.revisions3D.list(model.id).autoPagingToArray({ limit: Infinity });
const revisionFound =
revisions.find((revision: Revision3D) => revision.published) ?? revisions[0];
return {
model,
revision: revisionFound
};
};
13 changes: 12 additions & 1 deletion react-components/src/hooks/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
/*!
* Copyright 2023 Cognite AS
*/
import { type Node3D, type CogniteExternalId, type Asset } from '@cognite/sdk';
import {
type Node3D,
type CogniteExternalId,
type Asset,
type Revision3D,
type Model3D
} from '@cognite/sdk';
import { type AssetAnnotationImage360Info } from '@cognite/reveal';

export type ThreeDModelFdmMappings = {
Expand Down Expand Up @@ -58,3 +64,8 @@ export type Image360AnnotationMappedAssetData = {
asset: Asset;
annotationIds: number[];
};

export type ModelWithRevision = {
model: Model3D;
revision: Revision3D;
};
16 changes: 16 additions & 0 deletions react-components/src/hooks/useAll3dModels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*!
danpriori marked this conversation as resolved.
Show resolved Hide resolved
* Copyright 2024 Cognite AS
*/
import { useQuery, type UseQueryResult } from '@tanstack/react-query';

import { type CogniteClient, type HttpError, type Model3D } from '@cognite/sdk';
import { get3dModels } from './network/get3dModels';

export const useAll3dModels = (sdk: CogniteClient, enabled: boolean): UseQueryResult<Model3D[]> => {
return useQuery<Model3D[], HttpError>({
queryKey: ['models'],
queryFn: async () => await get3dModels(sdk),
staleTime: Infinity,
enabled
});
};
27 changes: 27 additions & 0 deletions react-components/src/hooks/useRevisions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*!
danpriori marked this conversation as resolved.
Show resolved Hide resolved
* Copyright 2024 Cognite AS
*/
import { useQuery, type UseQueryResult } from '@tanstack/react-query';

import { type Model3D, type CogniteClient } from '@cognite/sdk';
import { type ModelWithRevision } from './types';
import { getRevisions } from './network/getRevisions';

const STALE_TIME = 10 * 1000;
const REFRESH_INTERVAL = 10 * 1000;

export function useRevisions(
sdk: CogniteClient,
models: Model3D[] | undefined
): UseQueryResult<ModelWithRevision[]> {
return useQuery({
queryKey: ['model-revision', models],
queryFn: async () => {
const fetchPromises = models?.map(async (model) => await getRevisions(model, sdk));
return await Promise.all(fetchPromises ?? []);
},
staleTime: STALE_TIME,
refetchInterval: REFRESH_INTERVAL,
enabled: models !== undefined && models.length > 0
});
}
3 changes: 2 additions & 1 deletion react-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ export {
type Image360AnnotationMappedAssetData,
type LayersUrlStateParam,
type DefaultLayersConfiguration,
type ThreeDModelFdmMappings
type ThreeDModelFdmMappings,
type ModelWithRevision
} from './hooks/types';
export { type LayersButtonProps } from './components/RevealToolbar/LayersButton';
export type { CameraNavigationActions } from './hooks/useCameraNavigation';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { type CogniteClient, type Model3D } from '@cognite/sdk';
import { renderHook, waitFor } from '@testing-library/react';
import { useAll3dModels } from '../../../src/hooks/useAll3dModels';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { get3dModels } from '../../../src/hooks/network/get3dModels';

const models: Model3D[] = [
{
id: 1,
name: 'model1',
createdTime: new Date()
},
{
id: 2,
name: 'model2',
createdTime: new Date()
},
{
id: 3,
name: 'model3',
createdTime: new Date()
},
{
id: 4,
name: 'model4',
createdTime: new Date()
}
];

const sdk = {
post: vi.fn().mockResolvedValue({ data: {} }),
project: 'project'
} as unknown as CogniteClient;

const queryClient = new QueryClient();

const wrapper = ({ children }: { children: any }): any => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

vi.mock('../../../src/hooks/network/get3dModels');

describe('useAll3dModels', () => {
beforeEach(() => {
vi.clearAllMocks();
queryClient.clear();
});

test('should return a list of models', async () => {
vi.mocked(get3dModels).mockResolvedValue(models);

const { result } = renderHook(() => useAll3dModels(sdk, true), { wrapper });

await waitFor(() => {
expect(result.current.data).toEqual(models);
});
});

test('should return an empty list of models if no models exist', async () => {
vi.mocked(get3dModels).mockResolvedValue([]);
const { result } = renderHook(() => useAll3dModels(sdk, true), { wrapper });

await waitFor(() => {
expect(result.current.data).toEqual([]);
});
});

test('should return undefined if not enabled', async () => {
const { result } = renderHook(() => useAll3dModels(sdk, false), { wrapper });

await waitFor(() => {
expect(result.current.data).toEqual(undefined);
});
});
});
Loading
Loading