Skip to content

Commit

Permalink
Paginate the commit history page with query params (#1028)
Browse files Browse the repository at this point in the history
Motivation:

It could paginate the commit history page but a page index or a page size is not exposed in query parameters. Because these values were managed only as internal states of Javascript, sharing specific history as a URL was impossible.

Modifications:

- Update `Breadcrumbs` to specify query parameters
- Increase the width of `CompareButton`
- Add an option to render a custom component when data is empty in `DynamicDataTable`
- Add an option to hide a go-to button in `PaginationBar`.
- Expose a browse button when if the path is a directory in `HistoryList`
- Refactored `HistoryListPage` to use query parameters to specify revision ranges.
- Use a form tag to submit the revision range with an enter key in `ChangesViewPage`
- Link to a history page instead of a commit page in a `FileEditor`
  - Commits for a specific file must be retrieved sequentially from the `HEAD` revision, the Central Dogma server has a limit of 1000 if `maxCommits` is not specified. This means a commit for a specific file is not indexed and may not be found in the recent 1000 commits.
  - As we can't scan the entire history, it would be better to show the history and guide the user to find a commit rather than failing to find a commit for the file.

Result:

You can now share a specific range of commit history as a URL.
  • Loading branch information
ikhoon authored Aug 28, 2024
1 parent 0d365bd commit 534f6bf
Show file tree
Hide file tree
Showing 17 changed files with 389 additions and 229 deletions.
14 changes: 12 additions & 2 deletions webapp/src/dogma/common/components/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ import NextLink from 'next/link';
interface BreadcrumbsProps {
path: string;
omitIndexList?: number[];
omitQueryList?: number[];
unlinkedList?: number[];
replaces?: { [key: number]: string };
suffixes?: { [key: number]: string };
query?: string;
}

export const Breadcrumbs = ({
path,
omitIndexList = [],
omitQueryList = [],
unlinkedList = [],
replaces = {},
// /project/projectName/repos/repoName -> /project/projectName/repos/repoName/tree/head
suffixes = {},
query = '',
}: BreadcrumbsProps) => {
const asPathNestedRoutes = path
// If the path belongs to a file, the top level should be a directory
Expand All @@ -30,18 +34,24 @@ export const Breadcrumbs = ({
return (
<Breadcrumb spacing="8px" separator={<FcNext />} mb={8} fontWeight="medium" fontSize="2xl">
{asPathNestedRoutes.map((page, i) => {
prefixes.push(page);
const item = replaces[i] || page;
prefixes.push(item);
if (omitIndexList.includes(i)) {
return null;
}
let query0;
if (omitQueryList.includes(i) || omitQueryList.includes(i - asPathNestedRoutes.length)) {
query0 = '';
} else {
query0 = query ? `?${query}` : '';
}

return (
<BreadcrumbItem key={i}>
{!unlinkedList.includes(i) && i < asPathNestedRoutes.length - 1 ? (
<BreadcrumbLink
as={NextLink}
href={`/${prefixes.join('/')}${suffixes[i] || ''}`}
href={`/${prefixes.join('/')}${suffixes[i] || ''}${query0}`}
paddingBottom={1}
>
{decodeURI(item)}
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/dogma/common/components/CompareButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ const CompareButton = ({ projectName, repoName, headRevision }: CompareButtonPro
Compare
</Button>
</PopoverTrigger>
<PopoverContent width={'220px'}>
<PopoverContent width={'240px'}>
<PopoverArrow />
<PopoverBody>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack direction={'row'}>
<FormControl isRequired>
<Input
type="number"
placeholder={`Revision 1..${headRevision - 1}`}
placeholder={`Rev 1..${headRevision - 1}`}
autoFocus
{...register('baseRevision', { required: true, min: 1, max: headRevision - 1 })}
/>
Expand Down
1 change: 1 addition & 0 deletions webapp/src/dogma/common/components/Deferred.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Loading } from './Loading';

interface LoadingProps {
isLoading: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any;
children: () => ReactNode;
}
Expand Down
12 changes: 6 additions & 6 deletions webapp/src/dogma/common/components/editor/FileEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { newNotification } from 'dogma/features/notification/notificationSlice';
import ErrorMessageParser from 'dogma/features/services/ErrorMessageParser';
import Router from 'next/router';
import Link from 'next/link';
import { FaCodeCommit } from 'react-icons/fa6';
import { FaHistory } from 'react-icons/fa';

export type FileEditorProps = {
projectName: string;
Expand All @@ -37,7 +37,7 @@ export type FileEditorProps = {
originalContent: string;
path: string;
name: string;
commitRevision: number;
revision: string | number;
};

// Map file extension to language identifier
Expand Down Expand Up @@ -66,7 +66,7 @@ const FileEditor = ({
originalContent,
path,
name,
commitRevision,
revision,
}: FileEditorProps) => {
const dispatch = useAppDispatch();
const language = extensionToLanguageMap[extension] || extension;
Expand Down Expand Up @@ -117,12 +117,12 @@ const FileEditor = ({
<Button
size={'sm'}
as={Link}
href={`/app/projects/${projectName}/repos/${repoName}/commit/${commitRevision}/${path}`}
leftIcon={<FaCodeCommit />}
href={`/app/projects/${projectName}/repos/${repoName}/commits/${path}${revision !== 'head' ? `?from=${revision}` : ''}`}
leftIcon={<FaHistory />}
variant="outline"
colorScheme="gray"
>
Commit
History
</Button>
<Button
onClick={switchMode}
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/dogma/common/components/table/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const DataTable = <Data extends object>({ table }: { table: ReactTable<Da
<Tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
// see https://tanstack.com/table/v8/docs/api/core/column-def#meta to type this correctly
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const meta: any = header.column.columnDef.meta;
return (
<Th
Expand Down Expand Up @@ -41,6 +42,7 @@ export const DataTable = <Data extends object>({ table }: { table: ReactTable<Da
<Tr key={row.id} data-testid="table-row">
{row.getVisibleCells().map((cell) => {
// see https://tanstack.com/table/v8/docs/api/core/column-def#meta to type this correctly
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const meta: any = cell.column.columnDef.meta;
return (
<Td key={cell.id} isNumeric={meta?.isNumeric}>
Expand Down
28 changes: 19 additions & 9 deletions webapp/src/dogma/common/components/table/DynamicDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ import {
import { DataTable } from 'dogma/common/components/table/DataTable';
import { Filter } from 'dogma/common/components/table/Filter';
import { PaginationBar } from 'dogma/common/components/table/PaginationBar';
import { Dispatch, SetStateAction, useState } from 'react';
import React, { ReactElement, useState } from 'react';

export type DynamicDataTableProps<Data extends object> = {
data: Data[];
columns: ColumnDef<Data>[];
pagination?: { pageIndex: number; pageSize: number };
setPagination?: Dispatch<SetStateAction<PaginationState>>;
setPagination?: (updater: (old: PaginationState) => PaginationState) => void;
pageCount?: number;
disableGotoButton?: boolean;
onEmptyData?: ReactElement;
};

export const DynamicDataTable = <Data extends object>({
Expand All @@ -29,6 +31,8 @@ export const DynamicDataTable = <Data extends object>({
pagination,
setPagination,
pageCount,
disableGotoButton,
onEmptyData,
}: DynamicDataTableProps<Data>) => {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
Expand All @@ -53,13 +57,19 @@ export const DynamicDataTable = <Data extends object>({

return (
<>
<Text mb="8px">Filter by {table.getHeaderGroups()[0].headers[0].id} </Text>
<Filter
table={table}
column={table.getHeaderGroups()[0].headers[0].column /* Filter by the 1st column */}
/>
<DataTable table={table} />
{pagination && <PaginationBar table={table} />}
{table.getRowModel().rows.length == 0 && onEmptyData ? (
onEmptyData
) : (
<>
<Text mb="8px">Filter by {table.getHeaderGroups()[0].headers[0].id} </Text>
<Filter
table={table}
column={table.getHeaderGroups()[0].headers[0].column /* Filter by the 1st column */}
/>
<DataTable table={table} />
</>
)}
{pagination && <PaginationBar table={table} disableGotoButton={disableGotoButton} />}
</>
);
};
60 changes: 35 additions & 25 deletions webapp/src/dogma/common/components/table/PaginationBar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { Flex, Input, Text, Select, Spacer, IconButton } from '@chakra-ui/react';
import { Flex, IconButton, Input, Select, Spacer, Text } from '@chakra-ui/react';
import { Table as ReactTable } from '@tanstack/react-table';
import { MdNavigateBefore, MdNavigateNext, MdSkipNext, MdSkipPrevious } from 'react-icons/md';

export const PaginationBar = <Data extends object>({ table }: { table: ReactTable<Data> }) => {
type PaginationBarProps<Data extends object> = {
table: ReactTable<Data>;
disableGotoButton?: boolean;
};

export const PaginationBar = <Data extends object>({ table, disableGotoButton }: PaginationBarProps<Data>) => {
return (
<Flex gap={2} mt={2} alignItems="center">
{disableGotoButton && <Spacer />}
<IconButton
aria-label="First page"
icon={<MdSkipPrevious />}
Expand Down Expand Up @@ -34,29 +40,33 @@ export const PaginationBar = <Data extends object>({ table }: { table: ReactTabl
{table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
</Text>
<Spacer />
<Text>Go to page:</Text>
<Input
type="number"
defaultValue={table.getState().pagination.pageIndex + 1}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
table.setPageIndex(page);
}}
width={20}
/>
<Select
value={table.getState().pagination.pageSize}
onChange={(e) => {
table.setPageSize(Number(e.target.value));
}}
width="auto"
>
{[10, 20, 30, 40, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</Select>
{!disableGotoButton && (
<>
<Text>Go to page:</Text>
<Input
type="number"
defaultValue={table.getState().pagination.pageIndex + 1}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
table.setPageIndex(page);
}}
width={20}
/>
<Select
value={table.getState().pagination.pageSize}
onChange={(e) => {
table.setPageSize(Number(e.target.value));
}}
width="auto"
>
{[10, 20, 30, 40, 50, 100, 200, 400].map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</Select>
</>
)}
</Flex>
);
};
6 changes: 5 additions & 1 deletion webapp/src/dogma/features/api/apiSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export type GetFileContent = {
projectName: string;
repoName: string;
filePath: string;
revision: string;
revision: string | number;
};

export type TitleDto = {
Expand Down Expand Up @@ -328,6 +328,7 @@ export const apiSlice = createApi({
query: ({ projectName, id }) => `/api/v1/projects/${projectName}/mirrors/${id}`,
providesTags: ['Metadata'],
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addNewMirror: builder.mutation<any, MirrorDto>({
query: (mirror) => ({
url: `/api/v1/projects/${mirror.projectName}/mirrors`,
Expand All @@ -336,6 +337,7 @@ export const apiSlice = createApi({
}),
invalidatesTags: ['Metadata'],
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
updateMirror: builder.mutation<any, { projectName: string; id: string; mirror: MirrorDto }>({
query: ({ projectName, id, mirror }) => ({
url: `/api/v1/projects/${projectName}/mirrors/${id}`,
Expand All @@ -352,6 +354,7 @@ export const apiSlice = createApi({
query: ({ projectName, id }) => `/api/v1/projects/${projectName}/credentials/${id}`,
providesTags: ['Metadata'],
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addNewCredential: builder.mutation<any, { projectName: string; credential: CredentialDto }>({
query: ({ projectName, credential }) => ({
url: `/api/v1/projects/${projectName}/credentials`,
Expand All @@ -360,6 +363,7 @@ export const apiSlice = createApi({
}),
invalidatesTags: ['Metadata'],
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
updateCredential: builder.mutation<any, { projectName: string; id: string; credential: CredentialDto }>({
query: ({ projectName, id, credential }) => ({
url: `/api/v1/projects/${projectName}/credentials/${id}`,
Expand Down
1 change: 1 addition & 0 deletions webapp/src/dogma/features/file/FileDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export interface FileDto {
revision: number;
type: FileType;
url: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
content?: string | any;
}
Loading

0 comments on commit 534f6bf

Please sign in to comment.