mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site): fix resource selection when workspace has no prev resources (#11594)
This commit is contained in:
@@ -26,7 +26,7 @@ import { SidebarIconButton } from "components/FullPageLayout/Sidebar";
|
||||
import HubOutlined from "@mui/icons-material/HubOutlined";
|
||||
import { ResourcesSidebar } from "./ResourcesSidebar";
|
||||
import { ResourceCard } from "components/Resources/ResourceCard";
|
||||
import { useResourcesNav } from "./useResourcesNav";
|
||||
import { resourceOptionValue, useResourcesNav } from "./useResourcesNav";
|
||||
import { MemoizedInlineMarkdown } from "components/Markdown/Markdown";
|
||||
|
||||
export type WorkspaceError =
|
||||
@@ -163,6 +163,9 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
(a, b) => countAgents(b) - countAgents(a),
|
||||
);
|
||||
const resourcesNav = useResourcesNav(resources);
|
||||
const selectedResource = resources.find(
|
||||
(r) => resourceOptionValue(r) === resourcesNav.value,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -377,9 +380,9 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
|
||||
{buildLogs}
|
||||
|
||||
{resourcesNav.selected && (
|
||||
{selectedResource && (
|
||||
<ResourceCard
|
||||
resource={resourcesNav.selected}
|
||||
resource={selectedResource}
|
||||
agentRow={(agent) => (
|
||||
<AgentRow
|
||||
key={agent.id}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { resourceOptionId, useResourcesNav } from "./useResourcesNav";
|
||||
import { resourceOptionValue, useResourcesNav } from "./useResourcesNav";
|
||||
import { WorkspaceResource } from "api/typesGenerated";
|
||||
import { MockWorkspaceResource } from "testHelpers/entities";
|
||||
import { RouterProvider, createMemoryRouter } from "react-router-dom";
|
||||
@@ -20,27 +20,9 @@ describe("useResourcesNav", () => {
|
||||
/>
|
||||
),
|
||||
});
|
||||
expect(result.current.selected?.id).toBe(MockWorkspaceResource.id);
|
||||
});
|
||||
|
||||
it("selects the first resource if it has agents and selected resource is not find", async () => {
|
||||
const resources: WorkspaceResource[] = [
|
||||
MockWorkspaceResource,
|
||||
{
|
||||
...MockWorkspaceResource,
|
||||
agents: [],
|
||||
},
|
||||
];
|
||||
const { result } = renderHook(() => useResourcesNav(resources), {
|
||||
wrapper: ({ children }) => (
|
||||
<RouterProvider
|
||||
router={createMemoryRouter([{ path: "/", element: children }], {
|
||||
initialEntries: ["/?resources=not_found_resource_id"],
|
||||
})}
|
||||
/>
|
||||
),
|
||||
});
|
||||
expect(result.current.selected?.id).toBe(MockWorkspaceResource.id);
|
||||
expect(result.current.value).toBe(
|
||||
resourceOptionValue(MockWorkspaceResource),
|
||||
);
|
||||
});
|
||||
|
||||
it("selects the resource passed in the URL", () => {
|
||||
@@ -66,12 +48,14 @@ describe("useResourcesNav", () => {
|
||||
wrapper: ({ children }) => (
|
||||
<RouterProvider
|
||||
router={createMemoryRouter([{ path: "/", element: children }], {
|
||||
initialEntries: [`/?resources=${resourceOptionId(resources[1])}`],
|
||||
initialEntries: [
|
||||
`/?resources=${resourceOptionValue(resources[1])}`,
|
||||
],
|
||||
})}
|
||||
/>
|
||||
),
|
||||
});
|
||||
expect(result.current.selected?.id).toBe(resources[1].id);
|
||||
expect(result.current.value).toBe(resourceOptionValue(resources[1]));
|
||||
});
|
||||
|
||||
it("selects a resource when resources are updated", () => {
|
||||
@@ -104,7 +88,7 @@ describe("useResourcesNav", () => {
|
||||
initialProps: { resources: startedResources },
|
||||
},
|
||||
);
|
||||
expect(result.current.selected?.id).toBe(startedResources[0].id);
|
||||
expect(result.current.value).toBe(resourceOptionValue(startedResources[0]));
|
||||
|
||||
// When a workspace is stopped, there are no resources with agents, so we
|
||||
// need to retain the currently selected resource. This ensures consistency
|
||||
@@ -121,12 +105,46 @@ describe("useResourcesNav", () => {
|
||||
},
|
||||
];
|
||||
rerender({ resources: stoppedResources });
|
||||
expect(result.current.selectedValue).toBe(
|
||||
resourceOptionId(startedResources[0]),
|
||||
);
|
||||
expect(result.current.value).toBe(resourceOptionValue(startedResources[0]));
|
||||
|
||||
// When a workspace is started again a resource is selected
|
||||
rerender({ resources: startedResources });
|
||||
expect(result.current.selected?.id).toBe(startedResources[0].id);
|
||||
expect(result.current.value).toBe(resourceOptionValue(startedResources[0]));
|
||||
});
|
||||
|
||||
// This happens when a new workspace is created and there are no resources
|
||||
it("selects a resource when resources are not defined previously", () => {
|
||||
const startingResources: WorkspaceResource[] = [];
|
||||
const { result, rerender } = renderHook(
|
||||
({ resources }) => useResourcesNav(resources),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<RouterProvider
|
||||
router={createMemoryRouter([{ path: "/", element: children }])}
|
||||
/>
|
||||
),
|
||||
initialProps: { resources: startingResources },
|
||||
},
|
||||
);
|
||||
const startedResources: WorkspaceResource[] = [
|
||||
{
|
||||
...MockWorkspaceResource,
|
||||
type: "docker_container",
|
||||
name: "coder_python",
|
||||
},
|
||||
{
|
||||
...MockWorkspaceResource,
|
||||
type: "docker_container",
|
||||
name: "coder_java",
|
||||
},
|
||||
{
|
||||
...MockWorkspaceResource,
|
||||
type: "docker_image",
|
||||
name: "coder_image_python",
|
||||
agents: [],
|
||||
},
|
||||
];
|
||||
rerender({ resources: startedResources });
|
||||
expect(result.current.value).toBe(resourceOptionValue(startedResources[0]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTab } from "hooks";
|
||||
import { useEffectEvent } from "hooks/hookPolyfills";
|
||||
import { useCallback, useEffect } from "react";
|
||||
|
||||
export const resourceOptionId = (resource: WorkspaceResource) => {
|
||||
export const resourceOptionValue = (resource: WorkspaceResource) => {
|
||||
return `${resource.type}_${resource.name}`;
|
||||
};
|
||||
|
||||
@@ -18,30 +18,29 @@ export const useResourcesNav = (resources: WorkspaceResource[]) => {
|
||||
|
||||
const isSelected = useCallback(
|
||||
(resource: WorkspaceResource) => {
|
||||
return resourceOptionId(resource) === resourcesNav.value;
|
||||
return resourceOptionValue(resource) === resourcesNav.value;
|
||||
},
|
||||
[resourcesNav.value],
|
||||
);
|
||||
|
||||
const selectedResource = resources.find(isSelected);
|
||||
const onSelectedResourceChange = useEffectEvent(
|
||||
(previousResource?: WorkspaceResource) => {
|
||||
const onResourceChanges = useEffectEvent(
|
||||
(resources?: WorkspaceResource[]) => {
|
||||
const hasSelectedResource = resourcesNav.value !== "";
|
||||
const hasResources = resources && resources.length > 0;
|
||||
const hasResourcesWithAgents =
|
||||
resources.length > 0 &&
|
||||
resources[0].agents &&
|
||||
resources[0].agents.length > 0;
|
||||
if (!previousResource && hasResourcesWithAgents) {
|
||||
resourcesNav.set(resourceOptionId(resources[0]));
|
||||
hasResources && resources[0].agents && resources[0].agents.length > 0;
|
||||
if (!hasSelectedResource && hasResourcesWithAgents) {
|
||||
resourcesNav.set(resourceOptionValue(resources[0]));
|
||||
}
|
||||
},
|
||||
);
|
||||
useEffect(() => {
|
||||
onSelectedResourceChange(selectedResource);
|
||||
}, [onSelectedResourceChange, selectedResource]);
|
||||
onResourceChanges(resources);
|
||||
}, [onResourceChanges, resources]);
|
||||
|
||||
const select = useCallback(
|
||||
(resource: WorkspaceResource) => {
|
||||
resourcesNav.set(resourceOptionId(resource));
|
||||
resourcesNav.set(resourceOptionValue(resource));
|
||||
},
|
||||
[resourcesNav],
|
||||
);
|
||||
@@ -49,7 +48,6 @@ export const useResourcesNav = (resources: WorkspaceResource[]) => {
|
||||
return {
|
||||
isSelected,
|
||||
select,
|
||||
selected: selectedResource,
|
||||
selectedValue: resourcesNav.value,
|
||||
value: resourcesNav.value,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user