feat: integrate provisioning snapshots into TeamListView for enhanced team display
- Added provisioningSnapshotByTeam to manage synthetic team summaries during active provisioning. - Implemented teamsWithProvisioning to merge real teams with synthetic cards, improving the team list display. - Updated filtering and rendering logic in TeamListView to utilize the new teamsWithProvisioning array, ensuring accurate representation of teams during provisioning states.
This commit is contained in:
parent
7502cc8e53
commit
65a9928cb5
2 changed files with 60 additions and 6 deletions
|
|
@ -225,6 +225,7 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
provisioningErrorByTeam,
|
provisioningErrorByTeam,
|
||||||
clearProvisioningError,
|
clearProvisioningError,
|
||||||
provisioningRuns,
|
provisioningRuns,
|
||||||
|
provisioningSnapshotByTeam,
|
||||||
currentProvisioningRunIdByTeam,
|
currentProvisioningRunIdByTeam,
|
||||||
leadActivityByTeam,
|
leadActivityByTeam,
|
||||||
} = useStore(
|
} = useStore(
|
||||||
|
|
@ -234,6 +235,7 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
provisioningErrorByTeam: s.provisioningErrorByTeam,
|
provisioningErrorByTeam: s.provisioningErrorByTeam,
|
||||||
clearProvisioningError: s.clearProvisioningError,
|
clearProvisioningError: s.clearProvisioningError,
|
||||||
provisioningRuns: s.provisioningRuns,
|
provisioningRuns: s.provisioningRuns,
|
||||||
|
provisioningSnapshotByTeam: s.provisioningSnapshotByTeam,
|
||||||
currentProvisioningRunIdByTeam: s.currentProvisioningRunIdByTeam,
|
currentProvisioningRunIdByTeam: s.currentProvisioningRunIdByTeam,
|
||||||
leadActivityByTeam: s.leadActivityByTeam,
|
leadActivityByTeam: s.leadActivityByTeam,
|
||||||
}))
|
}))
|
||||||
|
|
@ -251,6 +253,15 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
);
|
);
|
||||||
}, [currentProvisioningRunIdByTeam, provisioningState]);
|
}, [currentProvisioningRunIdByTeam, provisioningState]);
|
||||||
|
|
||||||
|
/** Merge real teams with synthetic launching cards for active provisioning. */
|
||||||
|
const teamsWithProvisioning = useMemo(() => {
|
||||||
|
const existingNames = new Set(teams.map((t) => t.teamName));
|
||||||
|
const synthetic = provisioningTeamNames
|
||||||
|
.filter((name) => !existingNames.has(name) && provisioningSnapshotByTeam[name])
|
||||||
|
.map((name) => provisioningSnapshotByTeam[name]);
|
||||||
|
return synthetic.length > 0 ? [...teams, ...synthetic] : teams;
|
||||||
|
}, [teams, provisioningTeamNames, provisioningSnapshotByTeam]);
|
||||||
|
|
||||||
// Fetch alive teams on mount and when teams list changes
|
// Fetch alive teams on mount and when teams list changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!electronMode) return;
|
if (!electronMode) return;
|
||||||
|
|
@ -303,7 +314,7 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const filteredTeams = useMemo<TeamSummary[]>(() => {
|
const filteredTeams = useMemo<TeamSummary[]>(() => {
|
||||||
let result = teams;
|
let result = teamsWithProvisioning;
|
||||||
|
|
||||||
const q = searchQuery.trim().toLowerCase();
|
const q = searchQuery.trim().toLowerCase();
|
||||||
if (q) {
|
if (q) {
|
||||||
|
|
@ -364,7 +375,7 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [
|
}, [
|
||||||
teams,
|
teamsWithProvisioning,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
currentProjectPath,
|
currentProjectPath,
|
||||||
aliveTeams,
|
aliveTeams,
|
||||||
|
|
@ -577,7 +588,7 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{teams.length > 0 ? (
|
{teamsWithProvisioning.length > 0 ? (
|
||||||
<div className="mt-3 flex items-center gap-2">
|
<div className="mt-3 flex items-center gap-2">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search
|
<Search
|
||||||
|
|
@ -594,7 +605,7 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
</div>
|
</div>
|
||||||
<TeamListFilterPopover
|
<TeamListFilterPopover
|
||||||
filter={filter}
|
filter={filter}
|
||||||
teams={teams}
|
teams={teamsWithProvisioning}
|
||||||
aliveTeams={aliveTeams}
|
aliveTeams={aliveTeams}
|
||||||
onFilterChange={setFilter}
|
onFilterChange={setFilter}
|
||||||
/>
|
/>
|
||||||
|
|
@ -633,7 +644,7 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (teams.length === 0) {
|
if (teamsWithProvisioning.length === 0) {
|
||||||
return <TeamEmptyState />;
|
return <TeamEmptyState />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,8 @@ export interface TeamSlice {
|
||||||
lastSendMessageResult: SendMessageResult | null;
|
lastSendMessageResult: SendMessageResult | null;
|
||||||
reviewActionError: string | null;
|
reviewActionError: string | null;
|
||||||
provisioningRuns: Record<string, TeamProvisioningProgress>;
|
provisioningRuns: Record<string, TeamProvisioningProgress>;
|
||||||
|
/** Synthetic TeamSummary snapshots for teams currently being provisioned (before config.json exists). */
|
||||||
|
provisioningSnapshotByTeam: Record<string, TeamSummary>;
|
||||||
currentProvisioningRunIdByTeam: Record<string, string | null>;
|
currentProvisioningRunIdByTeam: Record<string, string | null>;
|
||||||
currentRuntimeRunIdByTeam: Record<string, string | null>;
|
currentRuntimeRunIdByTeam: Record<string, string | null>;
|
||||||
/** Runs explicitly cleared after Unknown runId polling; late events/progress for them are ignored. */
|
/** Runs explicitly cleared after Unknown runId polling; late events/progress for them are ignored. */
|
||||||
|
|
@ -610,6 +612,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
crossTeamTargetsLoading: false,
|
crossTeamTargetsLoading: false,
|
||||||
reviewActionError: null,
|
reviewActionError: null,
|
||||||
provisioningRuns: {},
|
provisioningRuns: {},
|
||||||
|
provisioningSnapshotByTeam: {},
|
||||||
currentProvisioningRunIdByTeam: {},
|
currentProvisioningRunIdByTeam: {},
|
||||||
currentRuntimeRunIdByTeam: {},
|
currentRuntimeRunIdByTeam: {},
|
||||||
ignoredProvisioningRunIds: {},
|
ignoredProvisioningRunIds: {},
|
||||||
|
|
@ -743,7 +746,22 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set({ teams, teamByName, teamBySessionId, teamsLoading: false, teamsError: null });
|
// Atomic update: set teams AND clean up provisioning snapshots in one call
|
||||||
|
// to prevent any render cycle with duplicate cards.
|
||||||
|
set((state) => {
|
||||||
|
const nextSnapshots = { ...state.provisioningSnapshotByTeam };
|
||||||
|
for (const team of teams) {
|
||||||
|
delete nextSnapshots[team.teamName];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
teams,
|
||||||
|
teamByName,
|
||||||
|
teamBySessionId,
|
||||||
|
teamsLoading: false,
|
||||||
|
teamsError: null,
|
||||||
|
provisioningSnapshotByTeam: nextSnapshots,
|
||||||
|
};
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// On refresh failure, keep existing teams visible
|
// On refresh failure, keep existing teams visible
|
||||||
set({
|
set({
|
||||||
|
|
@ -1413,6 +1431,21 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
...state.currentProvisioningRunIdByTeam,
|
...state.currentProvisioningRunIdByTeam,
|
||||||
[request.teamName]: pendingRunId,
|
[request.teamName]: pendingRunId,
|
||||||
},
|
},
|
||||||
|
// Synthetic card for the team list — visible until fetchTeams() picks up the real team.
|
||||||
|
provisioningSnapshotByTeam: {
|
||||||
|
...state.provisioningSnapshotByTeam,
|
||||||
|
[request.teamName]: {
|
||||||
|
teamName: request.teamName,
|
||||||
|
displayName: request.displayName || request.teamName,
|
||||||
|
description: request.description || '',
|
||||||
|
color: request.color,
|
||||||
|
memberCount: request.members.length,
|
||||||
|
members: request.members.map((m) => ({ name: m.name, role: m.role })),
|
||||||
|
taskCount: 0,
|
||||||
|
lastActivity: null,
|
||||||
|
projectPath: request.cwd || undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
try {
|
try {
|
||||||
if (typeof api.teams.createTeam !== 'function') {
|
if (typeof api.teams.createTeam !== 'function') {
|
||||||
|
|
@ -1751,6 +1784,15 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
} else {
|
} else {
|
||||||
delete nextErrors[progress.teamName];
|
delete nextErrors[progress.teamName];
|
||||||
}
|
}
|
||||||
|
// Clean up provisioning snapshot on terminal failure states
|
||||||
|
const nextSnapshots =
|
||||||
|
progress.state === 'failed' || progress.state === 'cancelled'
|
||||||
|
? (() => {
|
||||||
|
const s = { ...state.provisioningSnapshotByTeam };
|
||||||
|
delete s[progress.teamName];
|
||||||
|
return s;
|
||||||
|
})()
|
||||||
|
: state.provisioningSnapshotByTeam;
|
||||||
return {
|
return {
|
||||||
provisioningRuns: nextRuns,
|
provisioningRuns: nextRuns,
|
||||||
currentProvisioningRunIdByTeam: nextCurrentRunIdByTeam,
|
currentProvisioningRunIdByTeam: nextCurrentRunIdByTeam,
|
||||||
|
|
@ -1759,6 +1801,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
[progress.teamName]: progress.runId,
|
[progress.teamName]: progress.runId,
|
||||||
},
|
},
|
||||||
provisioningErrorByTeam: nextErrors,
|
provisioningErrorByTeam: nextErrors,
|
||||||
|
provisioningSnapshotByTeam: nextSnapshots,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue