mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 13:29:41 +02:00
refactor(app): move react components to react codebase [EE-3179] (#6971)
This commit is contained in:
parent
212400c283
commit
18252ab854
346 changed files with 642 additions and 644 deletions
|
@ -0,0 +1,26 @@
|
|||
interface Props {
|
||||
value: number;
|
||||
onChange(value: number): void;
|
||||
showAll?: boolean;
|
||||
}
|
||||
|
||||
export function ItemsPerPageSelector({ value, onChange, showAll }: Props) {
|
||||
return (
|
||||
<span className="limitSelector">
|
||||
<span className="space-right">Items per page</span>
|
||||
<select
|
||||
className="form-control"
|
||||
value={value}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
data-cy="paginationSelect"
|
||||
>
|
||||
{showAll ? <option value={Number.MAX_SAFE_INTEGER}>All</option> : null}
|
||||
{[10, 25, 50, 100].map((v) => (
|
||||
<option value={v} key={v}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
);
|
||||
}
|
30
app/react/components/PaginationControls/PageButton.tsx
Normal file
30
app/react/components/PaginationControls/PageButton.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import clsx from 'clsx';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
active?: boolean;
|
||||
children: ReactNode;
|
||||
disabled?: boolean;
|
||||
onPageChange(page: number): void;
|
||||
page: number | '...';
|
||||
}
|
||||
|
||||
export function PageButton({
|
||||
children,
|
||||
page,
|
||||
disabled,
|
||||
active,
|
||||
onPageChange,
|
||||
}: Props) {
|
||||
return (
|
||||
<li className={clsx({ disabled, active })}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => typeof page === 'number' && onPageChange(page)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
58
app/react/components/PaginationControls/PageInput.tsx
Normal file
58
app/react/components/PaginationControls/PageInput.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { useFormik } from 'formik';
|
||||
import { ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { object, number } from 'yup';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
|
||||
interface Values {
|
||||
page: number | '';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onChange(page: number): void;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export function PageInput({ onChange, totalPages }: Props) {
|
||||
const { handleSubmit, setFieldValue, values, isValid } = useFormik<Values>({
|
||||
initialValues: { page: '' },
|
||||
onSubmit: async ({ page }) => page && onChange(page),
|
||||
validateOnMount: true,
|
||||
validationSchema: () =>
|
||||
object({ page: number().required().max(totalPages).min(1) }),
|
||||
});
|
||||
|
||||
return (
|
||||
<form className="mx-3" onSubmit={handleSubmit}>
|
||||
<label className="m-0 mr-2 font-normal small" htmlFor="go-to-page-input">
|
||||
Go to page
|
||||
</label>
|
||||
<Input
|
||||
id="go-to-page-input"
|
||||
className="!w-32"
|
||||
type="number"
|
||||
value={values.page}
|
||||
max={totalPages}
|
||||
min={1}
|
||||
step={1}
|
||||
onChange={handleChange}
|
||||
onKeyPress={preventNotNumber}
|
||||
/>
|
||||
<Button type="submit" disabled={!isValid}>
|
||||
Go
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
function preventNotNumber(e: KeyboardEvent<HTMLInputElement>) {
|
||||
if (e.key.match(/^\D$/)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
setFieldValue('page', Number.isNaN(value) ? '' : value);
|
||||
}
|
||||
}
|
98
app/react/components/PaginationControls/PageSelector.tsx
Normal file
98
app/react/components/PaginationControls/PageSelector.tsx
Normal file
|
@ -0,0 +1,98 @@
|
|||
import { generatePagesArray } from './generatePagesArray';
|
||||
import { PageButton } from './PageButton';
|
||||
import { PageInput } from './PageInput';
|
||||
|
||||
interface Props {
|
||||
boundaryLinks?: boolean;
|
||||
currentPage: number;
|
||||
directionLinks?: boolean;
|
||||
itemsPerPage: number;
|
||||
onPageChange(page: number): void;
|
||||
totalCount: number;
|
||||
maxSize: number;
|
||||
isInputVisible?: boolean;
|
||||
}
|
||||
|
||||
export function PageSelector({
|
||||
currentPage,
|
||||
totalCount,
|
||||
itemsPerPage,
|
||||
onPageChange,
|
||||
maxSize = 5,
|
||||
directionLinks = true,
|
||||
boundaryLinks = false,
|
||||
isInputVisible = false,
|
||||
}: Props) {
|
||||
const pages = generatePagesArray(
|
||||
currentPage,
|
||||
totalCount,
|
||||
itemsPerPage,
|
||||
maxSize
|
||||
);
|
||||
const last = pages[pages.length - 1];
|
||||
|
||||
if (pages.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isInputVisible && (
|
||||
<PageInput
|
||||
onChange={(page) => onPageChange(page)}
|
||||
totalPages={Math.ceil(totalCount / itemsPerPage)}
|
||||
/>
|
||||
)}
|
||||
<ul className="pagination">
|
||||
{boundaryLinks ? (
|
||||
<PageButton
|
||||
onPageChange={onPageChange}
|
||||
page={1}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
«
|
||||
</PageButton>
|
||||
) : null}
|
||||
{directionLinks ? (
|
||||
<PageButton
|
||||
onPageChange={onPageChange}
|
||||
page={currentPage - 1}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
‹
|
||||
</PageButton>
|
||||
) : null}
|
||||
{pages.map((pageNumber, index) => (
|
||||
<PageButton
|
||||
onPageChange={onPageChange}
|
||||
page={pageNumber}
|
||||
disabled={pageNumber === '...'}
|
||||
active={currentPage === pageNumber}
|
||||
key={index}
|
||||
>
|
||||
{pageNumber}
|
||||
</PageButton>
|
||||
))}
|
||||
|
||||
{directionLinks ? (
|
||||
<PageButton
|
||||
onPageChange={onPageChange}
|
||||
page={currentPage + 1}
|
||||
disabled={currentPage === last}
|
||||
>
|
||||
›
|
||||
</PageButton>
|
||||
) : null}
|
||||
{boundaryLinks ? (
|
||||
<PageButton
|
||||
disabled={currentPage === last}
|
||||
onPageChange={onPageChange}
|
||||
page={last}
|
||||
>
|
||||
»
|
||||
</PageButton>
|
||||
) : null}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import { ItemsPerPageSelector } from './ItemsPerPageSelector';
|
||||
import { PageSelector } from './PageSelector';
|
||||
|
||||
interface Props {
|
||||
onPageChange(page: number): void;
|
||||
onPageLimitChange(value: number): void;
|
||||
page: number;
|
||||
pageLimit: number;
|
||||
showAll?: boolean;
|
||||
totalCount: number;
|
||||
isPageInputVisible?: boolean;
|
||||
}
|
||||
|
||||
export function PaginationControls({
|
||||
pageLimit,
|
||||
page,
|
||||
onPageLimitChange,
|
||||
showAll,
|
||||
onPageChange,
|
||||
totalCount,
|
||||
isPageInputVisible,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="paginationControls">
|
||||
<div className="form-inline flex">
|
||||
<ItemsPerPageSelector
|
||||
value={pageLimit}
|
||||
onChange={handlePageLimitChange}
|
||||
showAll={showAll}
|
||||
/>
|
||||
|
||||
{pageLimit !== 0 && (
|
||||
<PageSelector
|
||||
maxSize={5}
|
||||
onPageChange={onPageChange}
|
||||
currentPage={page}
|
||||
itemsPerPage={pageLimit}
|
||||
totalCount={totalCount}
|
||||
isInputVisible={isPageInputVisible}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function handlePageLimitChange(value: number) {
|
||||
onPageLimitChange(value);
|
||||
onPageChange(1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Given the position in the sequence of pagination links, figure out what page number corresponds to that position.
|
||||
*
|
||||
* @param position
|
||||
* @param currentPage
|
||||
* @param paginationRange
|
||||
* @param totalPages
|
||||
*/
|
||||
export function calculatePageNumber(
|
||||
position: number,
|
||||
currentPage: number,
|
||||
paginationRange: number,
|
||||
totalPages: number
|
||||
) {
|
||||
const halfWay = Math.ceil(paginationRange / 2);
|
||||
if (position === paginationRange) {
|
||||
return totalPages;
|
||||
}
|
||||
|
||||
if (position === 1) {
|
||||
return position;
|
||||
}
|
||||
|
||||
if (paginationRange < totalPages) {
|
||||
if (totalPages - halfWay < currentPage) {
|
||||
return totalPages - paginationRange + position;
|
||||
}
|
||||
|
||||
if (halfWay < currentPage) {
|
||||
return currentPage - halfWay + position;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { calculatePageNumber } from './calculatePageNumber';
|
||||
|
||||
export /**
|
||||
* Generate an array of page numbers (or the '...' string) which is used in an ng-repeat to generate the
|
||||
* links used in pagination
|
||||
*
|
||||
* @param currentPage
|
||||
* @param rowsPerPage
|
||||
* @param paginationRange
|
||||
* @param collectionLength
|
||||
* @returns {Array}
|
||||
*/
|
||||
function generatePagesArray(
|
||||
currentPage: number,
|
||||
collectionLength: number,
|
||||
rowsPerPage: number,
|
||||
paginationRange: number
|
||||
): (number | '...')[] {
|
||||
const pages: (number | '...')[] = [];
|
||||
const totalPages = Math.ceil(collectionLength / rowsPerPage);
|
||||
const halfWay = Math.ceil(paginationRange / 2);
|
||||
|
||||
let position;
|
||||
if (currentPage <= halfWay) {
|
||||
position = 'start';
|
||||
} else if (totalPages - halfWay < currentPage) {
|
||||
position = 'end';
|
||||
} else {
|
||||
position = 'middle';
|
||||
}
|
||||
|
||||
const ellipsesNeeded = paginationRange < totalPages;
|
||||
|
||||
for (let i = 1; i <= totalPages && i <= paginationRange; i += 1) {
|
||||
const pageNumber = calculatePageNumber(
|
||||
i,
|
||||
currentPage,
|
||||
paginationRange,
|
||||
totalPages
|
||||
);
|
||||
|
||||
const openingEllipsesNeeded =
|
||||
i === 2 && (position === 'middle' || position === 'end');
|
||||
const closingEllipsesNeeded =
|
||||
i === paginationRange - 1 &&
|
||||
(position === 'middle' || position === 'start');
|
||||
if (ellipsesNeeded && (openingEllipsesNeeded || closingEllipsesNeeded)) {
|
||||
pages.push('...');
|
||||
} else {
|
||||
pages.push(pageNumber);
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
3
app/react/components/PaginationControls/index.ts
Normal file
3
app/react/components/PaginationControls/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import './pagination-controls.css';
|
||||
|
||||
export { PaginationControls } from './PaginationControls';
|
|
@ -0,0 +1,72 @@
|
|||
.pagination-controls {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.paginationControls form.form-inline {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pagination > li:first-child > button {
|
||||
margin-left: 0;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.pagination > .disabled > span,
|
||||
.pagination > .disabled > span:hover,
|
||||
.pagination > .disabled > span:focus,
|
||||
.pagination > .disabled > button,
|
||||
.pagination > .disabled > button:hover,
|
||||
.pagination > .disabled > button:focus,
|
||||
.pagination > .disabled > a,
|
||||
.pagination > .disabled > a:hover,
|
||||
.pagination > .disabled > a:focus {
|
||||
color: var(--text-pagination-color);
|
||||
background-color: var(--bg-pagination-color);
|
||||
border-color: var(--border-pagination-color);
|
||||
}
|
||||
|
||||
.pagination > li > button {
|
||||
position: relative;
|
||||
float: left;
|
||||
padding: 6px 12px;
|
||||
margin-left: -1px !important;
|
||||
line-height: 1.42857143;
|
||||
text-decoration: none;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.pagination > li > a,
|
||||
.pagination > li > button,
|
||||
.pagination > li > span {
|
||||
background-color: var(--bg-pagination-span-color);
|
||||
border-color: var(--border-pagination-span-color);
|
||||
color: var(--text-pagination-span-color);
|
||||
}
|
||||
|
||||
.pagination > li > a:hover,
|
||||
.pagination > li > button:hover,
|
||||
.pagination > li > span:hover,
|
||||
.pagination > li > a:focus,
|
||||
.pagination > li > button:focus,
|
||||
.pagination > li > span:focus {
|
||||
background-color: var(--bg-pagination-hover-color);
|
||||
border-color: var(--border-pagination-hover-color);
|
||||
color: var(--text-pagination-span-hover-color);
|
||||
}
|
||||
|
||||
.pagination > .active > a,
|
||||
.pagination > .active > span,
|
||||
.pagination > .active > button,
|
||||
.pagination > .active > a:hover,
|
||||
.pagination > .active > span:hover,
|
||||
.pagination > .active > button:hover,
|
||||
.pagination > .active > a:focus,
|
||||
.pagination > .active > span:focus,
|
||||
.pagination > .active > button:focus {
|
||||
z-index: 3;
|
||||
color: #fff;
|
||||
cursor: default;
|
||||
background-color: var(--text-pagination-span-color);
|
||||
border-color: var(--text-pagination-span-color);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue