Skip to content

Commit 04e7748

Browse files
feat(site): load previous builds (#10076)
1 parent 1eb21d2 commit 04e7748

File tree

11 files changed

+161
-143
lines changed

11 files changed

+161
-143
lines changed

codersdk/workspaces.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,9 @@ func (c *Client) getWorkspace(ctx context.Context, id uuid.UUID, opts ...Request
135135
}
136136

137137
type WorkspaceBuildsRequest struct {
138-
WorkspaceID uuid.UUID
138+
WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid" typescript:"-"`
139139
Pagination
140-
Since time.Time
140+
Since time.Time `json:"since,omitempty" format:"date-time"`
141141
}
142142

143143
func (c *Client) WorkspaceBuilds(ctx context.Context, req WorkspaceBuildsRequest) ([]WorkspaceBuild, error) {

site/src/api/api.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -777,10 +777,10 @@ export const regenerateUserSSHKey = async (
777777

778778
export const getWorkspaceBuilds = async (
779779
workspaceId: string,
780-
since: Date,
781-
): Promise<TypesGen.WorkspaceBuild[]> => {
780+
req?: TypesGen.WorkspaceBuildsRequest,
781+
) => {
782782
const response = await axios.get<TypesGen.WorkspaceBuild[]>(
783-
`/api/v2/workspaces/${workspaceId}/builds?since=${since.toISOString()}`,
783+
getURLWithSearchParams(`/api/v2/workspaces/${workspaceId}/builds`, req),
784784
);
785785
return response.data;
786786
};
Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
1+
import { UseInfiniteQueryOptions } from "@tanstack/react-query";
12
import * as API from "api/api";
3+
import { WorkspaceBuild, WorkspaceBuildsRequest } from "api/typesGenerated";
24

35
export const workspaceBuildByNumber = (
46
username: string,
57
workspaceName: string,
68
buildNumber: number,
79
) => {
810
return {
9-
queryKey: [username, workspaceName, "workspaceBuild", buildNumber],
11+
queryKey: ["workspaceBuild", username, workspaceName, buildNumber],
1012
queryFn: () =>
1113
API.getWorkspaceBuildByNumber(username, workspaceName, buildNumber),
1214
};
1315
};
16+
17+
export const infiniteWorkspaceBuilds = (
18+
workspaceId: string,
19+
req?: WorkspaceBuildsRequest,
20+
): UseInfiniteQueryOptions<WorkspaceBuild[]> => {
21+
const limit = req?.limit ?? 25;
22+
23+
return {
24+
queryKey: ["workspaceBuilds", workspaceId, req],
25+
getNextPageParam: (lastPage, pages) => {
26+
return pages.length + 1;
27+
},
28+
queryFn: ({ pageParam = 0 }) => {
29+
return API.getWorkspaceBuilds(workspaceId, {
30+
limit,
31+
offset: pageParam <= 0 ? 0 : (pageParam - 1) * limit,
32+
});
33+
},
34+
};
35+
};

site/src/api/typesGenerated.ts

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,12 @@ export const WorkspaceBuildPage: FC = () => {
2323
keepPreviousData: true,
2424
});
2525
const build = wsBuildQuery.data;
26-
const { data: builds } = useQuery({
26+
const buildsQuery = useQuery({
2727
queryKey: ["builds", username, build?.workspace_id],
2828
queryFn: () => {
29-
return getWorkspaceBuilds(
30-
build?.workspace_id ?? "",
31-
dayjs().add(-30, "day").toDate(),
32-
);
29+
return getWorkspaceBuilds(build?.workspace_id ?? "", {
30+
since: dayjs().add(-30, "day").toISOString(),
31+
});
3332
},
3433
enabled: Boolean(build),
3534
});
@@ -50,7 +49,7 @@ export const WorkspaceBuildPage: FC = () => {
5049
<WorkspaceBuildPageView
5150
logs={logs}
5251
build={build}
53-
builds={builds}
52+
builds={buildsQuery.data}
5453
activeBuildNumber={buildNumber}
5554
/>
5655
</>

site/src/pages/WorkspacePage/BuildsTable.stories.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type Story = StoryObj<typeof BuildsTable>;
1313
export const Example: Story = {
1414
args: {
1515
builds: MockBuilds,
16+
hasMoreBuilds: true,
1617
},
1718
};
1819

@@ -21,3 +22,10 @@ export const Empty: Story = {
2122
builds: [],
2223
},
2324
};
25+
26+
export const NoMoreBuilds: Story = {
27+
args: {
28+
builds: MockBuilds,
29+
hasMoreBuilds: false,
30+
},
31+
};

site/src/pages/WorkspacePage/BuildsTable.tsx

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,43 +10,71 @@ import * as TypesGen from "api/typesGenerated";
1010
import { EmptyState } from "components/EmptyState/EmptyState";
1111
import { TableLoader } from "components/TableLoader/TableLoader";
1212
import { BuildRow } from "./BuildRow";
13+
import { Stack } from "components/Stack/Stack";
14+
import LoadingButton from "@mui/lab/LoadingButton";
15+
import ArrowDownwardOutlined from "@mui/icons-material/ArrowDownwardOutlined";
1316

1417
export const Language = {
1518
emptyMessage: "No builds found",
1619
};
1720

1821
export interface BuildsTableProps {
19-
builds?: TypesGen.WorkspaceBuild[];
22+
builds: TypesGen.WorkspaceBuild[] | undefined;
23+
onLoadMoreBuilds: () => void;
24+
isLoadingMoreBuilds: boolean;
25+
hasMoreBuilds: boolean;
2026
}
2127

2228
export const BuildsTable: FC<React.PropsWithChildren<BuildsTableProps>> = ({
2329
builds,
30+
onLoadMoreBuilds,
31+
isLoadingMoreBuilds,
32+
hasMoreBuilds,
2433
}) => {
2534
return (
26-
<TableContainer>
27-
<Table data-testid="builds-table" aria-describedby="builds table">
28-
<TableBody>
29-
{builds ? (
30-
<Timeline
31-
items={builds}
32-
getDate={(build) => new Date(build.created_at)}
33-
row={(build) => <BuildRow key={build.id} build={build} />}
34-
/>
35-
) : (
36-
<TableLoader />
37-
)}
35+
<Stack>
36+
<TableContainer>
37+
<Table data-testid="builds-table" aria-describedby="builds table">
38+
<TableBody>
39+
{builds ? (
40+
<Timeline
41+
items={builds}
42+
getDate={(build) => new Date(build.created_at)}
43+
row={(build) => <BuildRow key={build.id} build={build} />}
44+
/>
45+
) : (
46+
<TableLoader />
47+
)}
3848

39-
{builds && builds.length === 0 && (
40-
<TableRow>
41-
<TableCell colSpan={999}>
42-
<Box p={4}>
43-
<EmptyState message={Language.emptyMessage} />
44-
</Box>
45-
</TableCell>
46-
</TableRow>
47-
)}
48-
</TableBody>
49-
</Table>
50-
</TableContainer>
49+
{builds && builds.length === 0 && (
50+
<TableRow>
51+
<TableCell colSpan={999}>
52+
<Box p={4}>
53+
<EmptyState message={Language.emptyMessage} />
54+
</Box>
55+
</TableCell>
56+
</TableRow>
57+
)}
58+
</TableBody>
59+
</Table>
60+
</TableContainer>
61+
{hasMoreBuilds && (
62+
<LoadingButton
63+
onClick={onLoadMoreBuilds}
64+
loading={isLoadingMoreBuilds}
65+
loadingPosition="start"
66+
variant="outlined"
67+
color="neutral"
68+
startIcon={<ArrowDownwardOutlined />}
69+
css={{
70+
display: "inline-flex",
71+
margin: "auto",
72+
borderRadius: "9999px",
73+
}}
74+
>
75+
Load previous builds
76+
</LoadingButton>
77+
)}
78+
</Stack>
5179
);
5280
};

site/src/pages/WorkspacePage/Workspace.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ export interface WorkspaceProps {
5555
isRestarting: boolean;
5656
workspace: TypesGen.Workspace;
5757
resources?: TypesGen.WorkspaceResource[];
58-
builds?: TypesGen.WorkspaceBuild[];
5958
templateWarnings?: TypesGen.TemplateVersionWarning[];
6059
canUpdateWorkspace: boolean;
6160
updateMessage?: string;
@@ -70,6 +69,10 @@ export interface WorkspaceProps {
7069
quotaBudget?: number;
7170
handleBuildRetry: () => void;
7271
buildLogs?: React.ReactNode;
72+
builds: TypesGen.WorkspaceBuild[] | undefined;
73+
onLoadMoreBuilds: () => void;
74+
isLoadingMoreBuilds: boolean;
75+
hasMoreBuilds: boolean;
7376
}
7477

7578
/**
@@ -105,6 +108,9 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
105108
handleBuildRetry,
106109
templateWarnings,
107110
buildLogs,
111+
onLoadMoreBuilds,
112+
isLoadingMoreBuilds,
113+
hasMoreBuilds,
108114
}) => {
109115
const styles = useStyles();
110116
const navigate = useNavigate();
@@ -345,7 +351,12 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
345351
error={workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR]}
346352
/>
347353
) : (
348-
<BuildsTable builds={builds} />
354+
<BuildsTable
355+
builds={builds}
356+
onLoadMoreBuilds={onLoadMoreBuilds}
357+
isLoadingMoreBuilds={isLoadingMoreBuilds}
358+
hasMoreBuilds={hasMoreBuilds}
359+
/>
349360
)}
350361
</Stack>
351362
</Margins>

site/src/pages/WorkspacePage/WorkspacePage.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { useOrganizationId } from "hooks";
1010
import { isAxiosError } from "axios";
1111
import { Margins } from "components/Margins/Margins";
1212
import { workspaceQuota } from "api/queries/workspaceQuota";
13-
import { useQuery } from "@tanstack/react-query";
13+
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
14+
import { infiniteWorkspaceBuilds } from "api/queries/workspaceBuilds";
1415

1516
export const WorkspacePage: FC = () => {
1617
const params = useParams() as {
@@ -26,10 +27,19 @@ export const WorkspacePage: FC = () => {
2627
workspaceName,
2728
username,
2829
},
30+
actions: {
31+
refreshBuilds: async () => {
32+
await buildsQuery.refetch();
33+
},
34+
},
2935
});
3036
const { workspace, error } = workspaceState.context;
3137
const quotaQuery = useQuery(workspaceQuota(username));
3238
const pageError = error ?? quotaQuery.error;
39+
const buildsQuery = useInfiniteQuery({
40+
...infiniteWorkspaceBuilds(workspace?.id ?? ""),
41+
enabled: Boolean(workspace),
42+
});
3343

3444
if (pageError) {
3545
return (
@@ -53,6 +63,13 @@ export const WorkspacePage: FC = () => {
5363
workspaceState={workspaceState}
5464
quota={quotaQuery.data}
5565
workspaceSend={workspaceSend}
66+
builds={buildsQuery.data?.pages.flat()}
67+
buildsError={buildsQuery.error}
68+
isLoadingMoreBuilds={buildsQuery.isFetchingNextPage}
69+
onLoadMoreBuilds={async () => {
70+
await buildsQuery.fetchNextPage();
71+
}}
72+
hasMoreBuilds={Boolean(buildsQuery.hasNextPage)}
5673
/>
5774
</RequirePermission>
5875
);

site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,22 @@ interface WorkspaceReadyPageProps {
3939
workspaceState: StateFrom<typeof workspaceMachine>;
4040
workspaceSend: (event: WorkspaceEvent) => void;
4141
quota?: TypesGen.WorkspaceQuota;
42+
builds: TypesGen.WorkspaceBuild[] | undefined;
43+
buildsError: unknown;
44+
onLoadMoreBuilds: () => void;
45+
isLoadingMoreBuilds: boolean;
46+
hasMoreBuilds: boolean;
4247
}
4348

4449
export const WorkspaceReadyPage = ({
4550
workspaceState,
4651
workspaceSend,
4752
quota,
53+
builds,
54+
buildsError,
55+
onLoadMoreBuilds,
56+
isLoadingMoreBuilds,
57+
hasMoreBuilds,
4858
}: WorkspaceReadyPageProps): JSX.Element => {
4959
const [_, bannerSend] = useActor(
5060
workspaceState.children["scheduleBannerMachine"],
@@ -56,8 +66,6 @@ export const WorkspaceReadyPage = ({
5666
template,
5767
templateVersion: currentVersion,
5868
deploymentValues,
59-
builds,
60-
getBuildsError,
6169
buildError,
6270
cancellationError,
6371
sshPrefix,
@@ -168,14 +176,17 @@ export const WorkspaceReadyPage = ({
168176
handleDormantActivate={() => workspaceSend({ type: "ACTIVATE" })}
169177
resources={workspace.latest_build.resources}
170178
builds={builds}
179+
onLoadMoreBuilds={onLoadMoreBuilds}
180+
isLoadingMoreBuilds={isLoadingMoreBuilds}
181+
hasMoreBuilds={hasMoreBuilds}
171182
canUpdateWorkspace={canUpdateWorkspace}
172183
updateMessage={latestVersion?.message}
173184
canRetryDebugMode={canRetryDebugMode}
174185
canChangeVersions={canUpdateTemplate}
175186
hideSSHButton={featureVisibility["browser_only"]}
176187
hideVSCodeDesktopButton={featureVisibility["browser_only"]}
177188
workspaceErrors={{
178-
[WorkspaceErrors.GET_BUILDS_ERROR]: getBuildsError,
189+
[WorkspaceErrors.GET_BUILDS_ERROR]: buildsError,
179190
[WorkspaceErrors.BUILD_ERROR]: buildError || restartBuildError,
180191
[WorkspaceErrors.CANCELLATION_ERROR]: cancellationError,
181192
}}

0 commit comments

Comments
 (0)