Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit232c72f

Browse files
authored
feat: group apps together on workspace page (#18018)
1 parente906ce2 commit232c72f

File tree

7 files changed

+208
-31
lines changed

7 files changed

+208
-31
lines changed

‎site/src/components/Button/Button.tsx

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,45 @@ import { forwardRef } from "react";
88
import{cn}from"utils/cn";
99

1010
constbuttonVariants=cva(
11-
`inline-flex items-center justify-center gap-1 whitespace-nowrap font-sans
11+
`
12+
inline-flex items-center justify-center gap-1 whitespace-nowrap font-sans
1213
border-solid rounded-md transition-colors
13-
text-sm font-semibold font-medium cursor-pointer no-underline
14+
text-sm font-medium cursor-pointer no-underline
1415
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link
1516
disabled:pointer-events-none disabled:text-content-disabled
1617
[&:is(a):not([href])]:pointer-events-none [&:is(a):not([href])]:text-content-disabled
1718
[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:p-0.5
18-
[&>img]:pointer-events-none [&>img]:shrink-0 [&>img]:p-0.5`,
19+
[&_img]:pointer-events-none [&_img]:shrink-0 [&_img]:p-0.5
20+
`,
1921
{
2022
variants:{
2123
variant:{
22-
default:
23-
"bg-surface-invert-primary text-content-invert hover:bg-surface-invert-secondary border-none disabled:bg-surface-secondary font-semibold",
24-
outline:
25-
"border border-border-default text-content-primary bg-transparent hover:bg-surface-secondary",
26-
subtle:
27-
"border-none bg-transparent text-content-secondary hover:text-content-primary",
28-
destructive:
29-
"border border-border-destructive text-content-primary bg-surface-destructive hover:bg-transparent disabled:bg-transparent disabled:text-content-disabled font-semibold",
24+
default:`
25+
border-none bg-surface-invert-primary font-semibold text-content-invert
26+
hover:bg-surface-invert-secondary
27+
disabled:bg-surface-secondary
28+
`,
29+
outline:`
30+
border border-border-default bg-transparent text-content-primary
31+
hover:bg-surface-secondary
32+
`,
33+
subtle:`
34+
border-none bg-transparent text-content-secondary
35+
hover:text-content-primary
36+
`,
37+
destructive:`
38+
border border-border-destructive font-semibold text-content-primary bg-surface-destructive
39+
hover:bg-transparent
40+
disabled:bg-transparent disabled:text-content-disabled
41+
`,
3042
},
3143

3244
size:{
33-
lg:"min-w-20 h-10 px-3 py-2 [&_svg]:size-icon-lg [&>img]:size-icon-lg",
34-
sm:"min-w-20 h-8 px-2 py-1.5 text-xs [&_svg]:size-icon-sm [&>img]:size-icon-sm",
45+
lg:"min-w-20 h-10 px-3 py-2 [&_svg]:size-icon-lg [&_img]:size-icon-lg",
46+
sm:"min-w-20 h-8 px-2 py-1.5 text-xs [&_svg]:size-icon-sm [&_img]:size-icon-sm",
3547
xs:"min-w-8 py-1 px-2 text-2xs rounded-md",
36-
icon:"size-8 px-1.5 [&_svg]:size-icon-sm [&>img]:size-icon-sm",
37-
"icon-lg":"size-10 px-2 [&_svg]:size-icon-lg [&>img]:size-icon-lg",
48+
icon:"size-8 px-1.5 [&_svg]:size-icon-sm [&_img]:size-icon-sm",
49+
"icon-lg":"size-10 px-2 [&_svg]:size-icon-lg [&_img]:size-icon-lg",
3850
},
3951
},
4052
defaultVariants:{

‎site/src/components/DropdownMenu/DropdownMenu.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,15 @@ export const DropdownMenuItem = forwardRef<
109109
ref={ref}
110110
className={cn(
111111
[
112-
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-2 text-sm text-content-secondary font-medium outline-none transition-colors",
113-
"focus:bg-surface-secondary focus:text-content-primary data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
114-
"[&>svg]:size-4 [&>svg]:shrink-0 [&>img]:size-4 [&>img]:shrink-0 no-underline",
112+
`
113+
relative flex cursor-default select-none items-center gap-2 rounded-sm
114+
px-2 py-1.5 text-sm text-content-secondary font-medium outline-none
115+
no-underline
116+
focus:bg-surface-secondary focus:text-content-primary
117+
data-[disabled]:pointer-events-none data-[disabled]:opacity-50
118+
[&_svg]:size-icon-sm [&>svg]:shrink-0
119+
[&_img]:size-icon-sm [&>img]:shrink-0
120+
`,
115121
inset&&"pl-8",
116122
],
117123
className,

‎site/src/modules/resources/AgentRow.stories.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
importtype{Meta,StoryObj}from"@storybook/react";
2-
import{spyOn}from"@storybook/test";
2+
import{spyOn,userEvent,within}from"@storybook/test";
33
import{API}from"api/api";
44
import{getPreferredProxy}from"contexts/ProxyContext";
55
import{chromatic}from"testHelpers/chromatic";
@@ -265,3 +265,22 @@ export const HideApp: Story = {
265265
},
266266
},
267267
};
268+
269+
exportconstGroupApp:Story={
270+
args:{
271+
agent:{
272+
...M.MockWorkspaceAgent,
273+
apps:[
274+
{
275+
...M.MockWorkspaceApp,
276+
group:"group",
277+
},
278+
],
279+
},
280+
},
281+
282+
play:async({ canvasElement})=>{
283+
constcanvas=within(canvasElement);
284+
awaituserEvent.click(canvas.getByText("group"));
285+
},
286+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import{MockWorkspaceApp}from"testHelpers/entities";
2+
import{organizeAgentApps}from"./AgentRow";
3+
4+
describe("organizeAgentApps",()=>{
5+
test("returns one ungrouped app",()=>{
6+
constresult=organizeAgentApps([{ ...MockWorkspaceApp}]);
7+
8+
expect(result).toEqual([{apps:[MockWorkspaceApp]}]);
9+
});
10+
11+
test("handles ordering correctly",()=>{
12+
constbugApp={ ...MockWorkspaceApp,slug:"bug",group:"creatures"};
13+
constbirdApp={ ...MockWorkspaceApp,slug:"bird",group:"creatures"};
14+
constfishApp={ ...MockWorkspaceApp,slug:"fish",group:"creatures"};
15+
constriderApp={ ...MockWorkspaceApp,slug:"rider"};
16+
constzedApp={ ...MockWorkspaceApp,slug:"zed"};
17+
constresult=organizeAgentApps([
18+
bugApp,
19+
riderApp,
20+
birdApp,
21+
zedApp,
22+
fishApp,
23+
]);
24+
25+
expect(result).toEqual([
26+
{group:"creatures",apps:[bugApp,birdApp,fishApp]},
27+
{apps:[riderApp]},
28+
{apps:[zedApp]},
29+
]);
30+
});
31+
});

‎site/src/modules/resources/AgentRow.tsx

Lines changed: 104 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,19 @@ import type {
99
Workspace,
1010
WorkspaceAgent,
1111
WorkspaceAgentMetadata,
12+
WorkspaceApp,
1213
}from"api/typesGenerated";
1314
import{isAxiosError}from"axios";
1415
import{DropdownArrow}from"components/DropdownArrow/DropdownArrow";
15-
importtype{Line}from"components/Logs/LogLine";
16+
import{
17+
DropdownMenu,
18+
DropdownMenuContent,
19+
DropdownMenuItem,
20+
DropdownMenuTrigger,
21+
}from"components/DropdownMenu/DropdownMenu";
1622
import{Stack}from"components/Stack/Stack";
1723
import{useProxy}from"contexts/ProxyContext";
24+
import{Folder}from"lucide-react";
1825
import{useFeatureVisibility}from"modules/dashboard/useFeatureVisibility";
1926
import{AppStatuses}from"pages/WorkspacePage/AppStatuses";
2027
import{
@@ -29,6 +36,7 @@ import {
2936
import{useQuery}from"react-query";
3037
importAutoSizerfrom"react-virtualized-auto-sizer";
3138
importtype{FixedSizeListasList,ListOnScrollProps}from"react-window";
39+
import{AgentButton}from"./AgentButton";
3240
import{AgentDevcontainerCard}from"./AgentDevcontainerCard";
3341
import{AgentLatency}from"./AgentLatency";
3442
import{AGENT_LOG_LINE_HEIGHT}from"./AgentLogs/AgentLogLine";
@@ -59,10 +67,10 @@ export const AgentRow: FC<AgentRowProps> = ({
5967
onUpdateAgent,
6068
initialMetadata,
6169
})=>{
62-
// Apps visibility
6370
const{ browser_only}=useFeatureVisibility();
64-
constvisibleApps=agent.apps.filter((app)=>!app.hidden);
65-
consthasAppsToDisplay=!browser_only&&visibleApps.length>0;
71+
constappSections=organizeAgentApps(agent.apps);
72+
consthasAppsToDisplay=
73+
!browser_only||appSections.some((it)=>it.apps.length>0);
6674
constshouldDisplayApps=
6775
(agent.status==="connected"&&hasAppsToDisplay)||
6876
agent.status==="connecting";
@@ -223,10 +231,10 @@ export const AgentRow: FC<AgentRowProps> = ({
223231
displayApps={agent.display_apps}
224232
/>
225233
)}
226-
{visibleApps.map((app)=>(
227-
<AppLink
228-
key={app.slug}
229-
app={app}
234+
{appSections.map((section,i)=>(
235+
<Apps
236+
key={section.group??i}
237+
section={section}
230238
agent={agent}
231239
workspace={workspace}
232240
/>
@@ -296,7 +304,7 @@ export const AgentRow: FC<AgentRowProps> = ({
296304
width={width}
297305
css={styles.startupLogs}
298306
onScroll={handleLogScroll}
299-
logs={startupLogs.map<Line>((l)=>({
307+
logs={startupLogs.map((l)=>({
300308
id:l.id,
301309
level:l.level,
302310
output:l.output,
@@ -327,6 +335,93 @@ export const AgentRow: FC<AgentRowProps> = ({
327335
);
328336
};
329337

338+
typeAppSection={
339+
/**
340+
* If there is no `group`, just render all of the apps inline. If there is a
341+
* group name, show them all in a dropdown.
342+
*/
343+
group?:string;
344+
345+
apps:WorkspaceApp[];
346+
};
347+
348+
/**
349+
* organizeAgentApps returns an ordering of agent apps that accounts for
350+
* grouping. When we receive the list of apps from the backend, they have
351+
* already been "ordered" by their `order` attribute, but we are not given that
352+
* value. We must be careful to preserve that ordering, while also properly
353+
* grouping together all apps of any given group.
354+
*
355+
* The position of the group overall is determined by the `order` position of
356+
* the first app in the group. There may be several sections returned without
357+
* a group name, to allow placing grouped apps in between non-grouped apps. Not
358+
* every ungrouped section is expected to have a group in between, to make the
359+
* algorithm a little simpler to implement.
360+
*/
361+
exportfunctionorganizeAgentApps(apps:readonlyWorkspaceApp[]):AppSection[]{
362+
letcurrentSection:AppSection|undefined=undefined;
363+
constappGroups:AppSection[]=[];
364+
constgroupsByName=newMap<string,AppSection>();
365+
366+
for(constappofapps){
367+
if(app.hidden){
368+
continue;
369+
}
370+
371+
if(!currentSection||app.group!==currentSection.group){
372+
constexistingSection=groupsByName.get(app.group!);
373+
if(existingSection){
374+
currentSection=existingSection;
375+
}else{
376+
currentSection={
377+
group:app.group,
378+
apps:[],
379+
};
380+
appGroups.push(currentSection);
381+
if(app.group){
382+
groupsByName.set(app.group,currentSection);
383+
}
384+
}
385+
}
386+
387+
currentSection.apps.push(app);
388+
}
389+
390+
returnappGroups;
391+
}
392+
393+
typeAppsProps={
394+
section:AppSection;
395+
agent:WorkspaceAgent;
396+
workspace:Workspace;
397+
};
398+
399+
constApps:FC<AppsProps>=({ section, agent, workspace})=>{
400+
returnsection.group ?(
401+
<DropdownMenu>
402+
<DropdownMenuTriggerasChild>
403+
<AgentButton>
404+
<Folder/>
405+
{section.group}
406+
</AgentButton>
407+
</DropdownMenuTrigger>
408+
<DropdownMenuContentalign="start">
409+
{section.apps.map((app)=>(
410+
<DropdownMenuItemkey={app.slug}>
411+
<AppLinkgroupedapp={app}agent={agent}workspace={workspace}/>
412+
</DropdownMenuItem>
413+
))}
414+
</DropdownMenuContent>
415+
</DropdownMenu>
416+
) :(
417+
<>
418+
{section.apps.map((app)=>(
419+
<AppLinkkey={app.slug}app={app}agent={agent}workspace={workspace}/>
420+
))}
421+
</>
422+
);
423+
};
424+
330425
conststyles={
331426
agentRow:(theme)=>({
332427
fontSize:14,

‎site/src/modules/resources/AppLink/AppLink.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import{useTheme}from"@emotion/react";
22
importtype*asTypesGenfrom"api/typesGenerated";
3+
import{DropdownMenuItem}from"components/DropdownMenu/DropdownMenu";
34
import{Spinner}from"components/Spinner/Spinner";
45
import{
56
Tooltip,
@@ -28,9 +29,15 @@ interface AppLinkProps {
2829
workspace:TypesGen.Workspace;
2930
app:TypesGen.WorkspaceApp;
3031
agent:TypesGen.WorkspaceAgent;
32+
grouped?:boolean;
3133
}
3234

33-
exportconstAppLink:FC<AppLinkProps>=({ app, workspace, agent})=>{
35+
exportconstAppLink:FC<AppLinkProps>=({
36+
app,
37+
workspace,
38+
agent,
39+
grouped,
40+
})=>{
3441
const{ proxy}=useProxy();
3542
consthost=proxy.preferredWildcardHostname;
3643
const[iconError,setIconError]=useState(false);
@@ -90,7 +97,15 @@ export const AppLink: FC<AppLinkProps> = ({ app, workspace, agent }) => {
9097

9198
constcanShare=app.sharing_level!=="owner";
9299

93-
constbutton=(
100+
constbutton=grouped ?(
101+
<DropdownMenuItemasChild>
102+
<ahref={canClick ?link.href :undefined}onClick={link.onClick}>
103+
{icon}
104+
{link.label}
105+
{canShare&&<ShareIconapp={app}/>}
106+
</a>
107+
</DropdownMenuItem>
108+
) :(
94109
<AgentButtonasChild>
95110
<ahref={canClick ?link.href :undefined}onClick={link.onClick}>
96111
{icon}

‎site/src/testHelpers/entities.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,6 @@ export const MockWorkspaceApp: TypesGen.WorkspaceApp = {
903903
health:"disabled",
904904
external:false,
905905
sharing_level:"owner",
906-
group:"",
907906
hidden:false,
908907
open_in:"slim-window",
909908
statuses:[],

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp