feat(desktop): image hub list paging & pagination component (#2437)

This commit is contained in:
zhujingyang
2023-01-19 17:08:02 +08:00
committed by GitHub
parent 813ed4f2c0
commit 3ffdcc993f
17 changed files with 358 additions and 61 deletions
+2
View File
@@ -31,6 +31,7 @@
"github-markdown-css": "^5.1.0",
"immer": "^9.0.16",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"next": "13.1.0",
"next-pwa": "^5.6.0",
"qrcode.react": "^3.1.0",
@@ -48,6 +49,7 @@
"devDependencies": {
"@svgr/webpack": "^6.5.1",
"@types/js-yaml": "^4.0.5",
"@types/lodash": "^4.14.191",
"@types/node": "18.11.17",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
+17 -5
View File
@@ -13,6 +13,7 @@ specifiers:
'@svgr/webpack': ^6.5.1
'@tanstack/react-query': ^4.20.4
'@types/js-yaml': ^4.0.5
'@types/lodash': ^4.14.191
'@types/node': 18.11.17
'@types/react': 18.0.26
'@types/react-dom': 18.0.9
@@ -26,6 +27,7 @@ specifiers:
github-markdown-css: ^5.1.0
immer: ^9.0.16
js-yaml: ^4.1.0
lodash: ^4.17.21
next: 13.1.0
next-pwa: ^5.6.0
postcss: ^8.4.20
@@ -62,6 +64,7 @@ dependencies:
github-markdown-css: registry.npmmirror.com/github-markdown-css/5.1.0
immer: 9.0.16
js-yaml: 4.1.0
lodash: registry.npmmirror.com/lodash/4.17.21
next: 13.1.0_3duxcpitbtplz62feflag7fwby
next-pwa: 5.6.0_next@13.1.0
qrcode.react: 3.1.0_react@18.2.0
@@ -79,6 +82,7 @@ dependencies:
devDependencies:
'@svgr/webpack': 6.5.1
'@types/js-yaml': 4.0.5
'@types/lodash': registry.npmmirror.com/@types/lodash/4.14.191
'@types/node': 18.11.17
'@types/react': 18.0.26
'@types/react-dom': 18.0.9
@@ -5963,10 +5967,6 @@ packages:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
dev: false
/lodash/4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: false
/loose-envify/1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@@ -7884,7 +7884,7 @@ packages:
fast-json-stable-stringify: 2.1.0
fs-extra: 9.1.0
glob: 7.2.3
lodash: 4.17.21
lodash: registry.npmmirror.com/lodash/4.17.21
pretty-bytes: 5.6.0
rollup: 2.79.1
rollup-plugin-terser: 7.0.2_rollup@2.79.1
@@ -8219,6 +8219,12 @@ packages:
dev: false
optional: true
registry.npmmirror.com/@types/lodash/4.14.191:
resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.191.tgz}
name: '@types/lodash'
version: 4.14.191
dev: true
registry.npmmirror.com/fsevents/2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz}
name: fsevents
@@ -8242,6 +8248,12 @@ packages:
dev: false
optional: true
registry.npmmirror.com/lodash/4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz}
name: lodash
version: 4.17.21
dev: false
registry.npmmirror.com/openid-client/5.3.1:
resolution: {integrity: sha512-RLfehQiHch9N6tRWNx68cicf3b1WR0x74bJWHRc25uYIbSRwjxYcTFaRnzbbpls5jroLAaB/bFIodTgA5LJMvw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/openid-client/-/openid-client-5.3.1.tgz}
name: openid-client
@@ -27,6 +27,17 @@ $background-max-hover: rgba(255, 255, 255, 0.65);
}
}
.customScrollStyle {
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
background: #7291be;
border-radius: 10px;
}
}
.backgroundWrap {
background-image: url('/images/appstore/background.png');
background-size: cover;
@@ -78,17 +78,3 @@ export function getSelectLabels(labels: TImageLabels[]): string {
.filter((item) => item)
.join(',');
}
export function sortByName(arr: TAppInfo[]) {
const map = new Map();
const result = [];
for (const item of arr) {
map.set(item.name, item);
}
let mapKeys = [...map.keys()];
mapKeys.sort();
for (const name of mapKeys) {
result.push(map.get(name));
}
return result;
}
@@ -0,0 +1,43 @@
.pageContainer {
display: flex;
font-size: 12px;
align-items: center;
padding: 4px 0;
}
.pageWrapper {
display: flex;
align-items: center;
li.pageItemActive {
color: #239bf2;
border-color: #239bf2;
}
}
.pageWrapper li {
cursor: pointer;
border: 1px transparent solid;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 4px;
margin-left: 4px;
background: rgba(255, 255, 255, 0.8);
border-radius: 8px;
color: #0d1a2d;
font-weight: 400;
font-size: 12px;
line-height: 16px;
}
.pageWrapper li:hover {
background: rgba(255, 255, 255, 0.5);
}
.pageWrapper li.disabled {
color: #0d1a2d;
cursor: not-allowed;
}
@@ -0,0 +1,183 @@
import clsx from 'clsx';
import { useState } from 'react';
import styles from './pgination.module.scss';
import Iconfont from 'components/iconfont';
export type TPagination = {
defaultCurrent: number; // current page count
pageSize: number; // quantity per page
total: number; // total
showTotal?: boolean;
onChange: (page: number, pageSize: number) => void; // Callback that change the page number or pageSize
special?: boolean; //can only take one step back
};
function Pagination(props: TPagination) {
const { defaultCurrent = 1, pageSize, total, showTotal, onChange, special } = props;
const [currentPage, setCurrentPage] = useState(defaultCurrent);
const totalPageSize = Math.ceil(total / pageSize) || 1;
let centerPages: number[] = [];
const centerSize = 5;
const jumpSize = centerSize;
const startEllipsisSize = centerSize + 3;
const updateCenterPage = () => {
let centerPage = currentPage;
if (currentPage > totalPageSize - 3) {
centerPage = totalPageSize - 3;
}
if (currentPage < 4) {
centerPage = 4;
}
if (totalPageSize <= centerSize + 2) {
let temp = [];
for (let i = 2, len = totalPageSize; i < len; i++) {
temp.push(i);
}
centerPages = temp;
} else {
centerPages = [centerPage - 2, centerPage - 1, centerPage, centerPage + 1, centerPage + 2];
}
};
updateCenterPage();
const pageChange = (cur: number) => {
if (currentPage === cur) return;
setCurrentPage(cur);
onChange(cur, pageSize);
};
// next page
const nextPageChange = () => {
if (currentPage === totalPageSize) return;
pageChange(currentPage + 1);
};
// previous page
const prePageChange = () => {
if (currentPage === 1) return;
pageChange(currentPage - 1);
};
// First 5 pages
const preOmitPageChange = () => {
let newPage = currentPage - jumpSize;
if (newPage < 1) {
newPage = 1;
}
pageChange(newPage);
};
// Back 5 pages
const nextOmitPageChange = () => {
let newPage = currentPage + jumpSize;
if (newPage > totalPageSize) {
newPage = totalPageSize;
}
pageChange(newPage);
};
const firstPage = (
<li
key={'first'}
className={clsx({ [styles.pageItemActive]: currentPage === 1 })}
onClick={() => {
pageChange(1);
}}
>
1
</li>
);
const lastPage = (
<li
key={'last'}
onClick={() => pageChange(totalPageSize)}
className={clsx({ [styles.pageItemActive]: currentPage === totalPageSize })}
>
{totalPageSize}
</li>
);
const pageItem = (currentValue: number) => {
return (
<li
onClick={() => pageChange(currentValue)}
className={clsx({ [styles.pageItemActive]: currentPage === currentValue })}
key={'item' + currentValue}
>
{currentValue}
</li>
);
};
const PageOmit = (props: any) => {
return (
<li title={props.title} onClick={props.onClick}>
...
</li>
);
};
const initPage = () => {
return (
<>
{firstPage}
{currentPage >= centerSize && totalPageSize >= startEllipsisSize && (
<PageOmit key={'omit-pre'} title="向前5页" onClick={preOmitPageChange} />
)}
{centerPages.map((page) => pageItem(page))}
{currentPage <= totalPageSize - centerSize + 1 && totalPageSize >= startEllipsisSize && (
<PageOmit key={'omit-next'} title="向后5页" onClick={nextOmitPageChange} />
)}
{lastPage}
</>
);
};
const initSecialPage = () => {
const smallPage = Array.from({ length: currentPage }).map((_, i) => pageItem(i + 1));
const largePage = (
<>
{firstPage}
<PageOmit key={'omit-pre'} title="向前5页" onClick={preOmitPageChange} />
{Array.from({ length: 3 }).map((_, i) => pageItem(i + currentPage - 2))}
</>
);
return <>{currentPage < 6 ? smallPage : largePage}</>;
};
return (
<div className={styles.pageContainer}>
<ul className={styles.pageWrapper}>
<li
key={'<'}
onClick={prePageChange}
className={clsx({ [styles.disabled]: currentPage === 1 })}
>
<Iconfont iconName="icon-left" color="#0D1A2D" width={28} height={28} />
</li>
{special ? initSecialPage() : initPage()}
<li
key={'>'}
onClick={nextPageChange}
className={clsx({ [styles.disabled]: currentPage === totalPageSize })}
>
<Iconfont iconName="icon-right" color="#0D1A2D" width={28} height={28} />
</li>
</ul>
{showTotal && (
<span style={{ color: `#333`, marginRight: `.5em`, marginLeft: `1em` }}>{total}</span>
)}
{special && '共' + totalPageSize + '页'}
</div>
);
}
export default Pagination;
@@ -6,11 +6,12 @@ import MarkDown from 'components/markdown';
import { useEffect, useRef, useState } from 'react';
import request from 'services/request';
import useSessionStore from 'stores/session';
import { EPageType, formattedSize, handleImageName, TAppDetail, TTag } from './app_store_common';
import { EPageType, formattedSize, handleImageName, TAppDetail } from './app_store_common';
import Button from './components/button';
import styles from './detail.module.scss';
import { useAppStoreContext } from './index';
import { CardLoading, ImageMarkdownLoading, ImageTagsLoading } from './components/imagehub_loading';
import { throttle } from 'lodash';
export default function DetailPage() {
const { toPage, detailAppName } = useAppStoreContext();
@@ -23,6 +24,15 @@ export default function DetailPage() {
const [open, setOpen] = useState(false);
const queryClient = useQueryClient();
const handleScrollEvent = () => {
const scrollTop = appRef.current?.scrollTop;
setFixed(Boolean(scrollTop && scrollTop > 20));
};
useEffect(() => {
appRef.current?.addEventListener('scroll', throttle(handleScrollEvent));
}, []);
const { data, isLoading, isSuccess } = useQuery(
['getAppDetail', fullImageName],
async () => {
@@ -59,18 +69,6 @@ export default function DetailPage() {
navigator.clipboard.writeText(value);
};
const handleScrollEvent = () => {
const scrollTop = appRef.current?.scrollTop;
setFixed(Boolean(scrollTop && scrollTop > 20));
};
useEffect(() => {
appRef.current?.addEventListener('scroll', handleScrollEvent);
return () => {
appRef.current?.removeEventListener('scroll', handleScrollEvent);
};
}, []);
if (isLoading) {
return (
<div className={clsx(styles.backgroundWrap, 'flex flex-col grow h-full p-8 pb-0')}>
@@ -84,7 +82,7 @@ export default function DetailPage() {
<CardLoading width={400} height={110} />
</div>
<div className={clsx('flex grow my-4')}>
<div className={clsx(styles.baseCard, styles.mainLeft, 'py-8 px-6')}>
<div ref={appRef} className={clsx(styles.baseCard, styles.mainLeft, 'py-8 px-6')}>
<div className={clsx('flex items-center mb-4')}>
<div className={styles.iconBtn}>
<Iconfont iconName="icon-overview" />
@@ -128,7 +126,7 @@ export default function DetailPage() {
<div>{fullImageName}</div>
<div className={styles.fingerPrint}>
<Iconfont iconName="icon-hash" color="#239BF2" />
<span className={styles.imageId}>{appDetail.ID?.substring(0, 12)}</span>
<span className={styles.imageId}>{appDetail?.ID?.substring(0, 12)}</span>
</div>
</div>
<p className={styles.text}>{appDetail?.description}</p>
@@ -172,9 +170,14 @@ export default function DetailPage() {
<div className={clsx('flex grow my-4')}>
<div
ref={appRef}
className={clsx(styles.baseCard, styles.mainLeft, styles.hiddenScrollWrap)}
className={clsx(
styles.baseCard,
styles.mainLeft,
styles.pageWrapperScroll,
styles.hiddenScrollWrap
)}
>
<div className="absolute w-full py-8 px-6 ">
<div className="absolute w-full py-8 px-6">
<div className={clsx('flex items-center mb-4')}>
<div className={styles.iconBtn}>
<Iconfont iconName="icon-overview" />
@@ -211,7 +214,7 @@ export default function DetailPage() {
className={clsx(styles.tag, { [styles.activeTag]: selectTag === item.name })}
onClick={() => setSelectTag(item.name)}
>
<div className={clsx('w-12')}>
<div className={clsx('w-8')}>
{selectTag === item.name && (
<Iconfont iconName="icon-checked" color="#239BF2" />
)}
@@ -29,7 +29,7 @@ export default function AppStore() {
return (
<ApppStoreContext.Provider value={{ toPage, detailAppName }}>
<div className={clsx(styles.backgroundWrap, 'flex grow flex-col h-full p-6 pb-0')}>
<div className={clsx(styles.backgroundWrap, 'flex grow flex-col h-full pt-6 pl-6')}>
{pageId === EPageType.StorePage && <StorePage />}
{pageId === EPageType.OrganizationPage && <OrganizationPage />}
{pageId === EPageType.MinePage && <MinePage />}
@@ -14,8 +14,7 @@ import {
ImagehubLabels,
TImageLabels,
getSelectLabels,
TAppInfo,
sortByName
TAppInfo
} from './app_store_common';
import Button from './components/button';
import Labels from './components/labels';
@@ -23,6 +22,8 @@ import Error from './components/error';
import { useAppStoreContext } from './index';
import styles from './store_page.module.scss';
import { ListContextLoading } from './components/imagehub_loading';
import Pagination, { TPagination } from './components/pgination';
import { sortBy } from 'lodash';
function StorePage() {
const { toPage } = useAppStoreContext();
@@ -30,13 +31,33 @@ function StorePage() {
const { kubeconfig } = useSessionStore((state) => state.getSession());
const selectedLabels = getSelectLabels(imageLabels);
const queryClient = useQueryClient();
const [test, setTest] = useState(1);
const [pagination, setPagination] = useState({
current: 1,
total: 0,
pageSize: 10,
continueKey: ['']
});
const onChangePagination = (page: number, pageSize: number) => {
setPagination(
produce((draft) => {
draft.current = page;
if (page === 1) {
draft.continueKey = [''];
}
})
);
};
const { data, isLoading, isSuccess, isError } = useQuery(
['getAppLists', selectedLabels],
['getAppLists', selectedLabels, pagination.current],
async () => {
const res = await request.post('/api/image_hub/get_list', {
kubeconfig,
labels: selectedLabels
labels: selectedLabels,
limit: pagination.pageSize,
_continue: pagination.continueKey[pagination.current - 1]
});
return res;
},
@@ -45,12 +66,24 @@ function StorePage() {
if (data.status === 201) {
queryClient.invalidateQueries({ queryKey: ['getAppLists', selectedLabels] });
}
if (data.status === 200) {
setPagination(
produce((draft) => {
draft.total =
(data.data.metadata.remainingItemCount || 0) + draft.pageSize * draft.current;
if (!draft.continueKey.includes(data.data.metadata.continue)) {
draft.continueKey.push(data.data.metadata.continue);
}
})
);
}
}
}
);
let appLists: TAppInfo[] = [];
if (data?.status === 200) {
appLists = sortByName(data.data);
appLists = sortBy(data.data.data, (o) => o.name);
}
const handleLabelsChange = (value: string) => {
@@ -91,7 +124,7 @@ function StorePage() {
<Error />
</div>
)}
<div className={clsx(styles.pageWrapperScroll, styles.hiddenScrollWrap)}>
<div className={clsx(styles.pageWrapperScroll, styles.customScrollStyle)}>
{isLoading && <ListContextLoading />}
{isSuccess && (
<div className="absolute w-full space-y-4 pb-10">
@@ -154,6 +187,17 @@ function StorePage() {
</div>
);
})}
<div className="float-right mr-8">
<Pagination
special
defaultCurrent={pagination.current}
total={pagination.total}
pageSize={pagination.pageSize}
onChange={(page, pageSize) => {
onChangePagination(page, pageSize);
}}
/>
</div>
</div>
)}
</div>
@@ -17,7 +17,7 @@ const AddPage = () => {
const { infraName, toPage } = useScpContext();
const { kubeconfig } = useSessionStore((state) => state.getSession());
const [image1, setImage1] = useState('labring/kubernetes:v1.25.5');
const [image2, setImage2] = useState('labring/calico:v3.22.1');
const [image2, setImage2] = useState('labring/calico:v3.24.1');
const [yamlTemplate, setYamlTemplate] = useState('');
const [scpPrice, setScpPrice] = useState(0);
const [inputNameErr, setInputNameErr] = useState(false);
@@ -109,7 +109,7 @@ function DetailPage() {
</span>
<span className="mt-4">
<SvgIcon src="/images/infraicon/scp_image.svg" />
kuberentes:v1.24.0
{clusterInfo?.data?.spec?.image[0]}
</span>
<span className="mt-4">
<SvgIcon src="/images/infraicon/scp_user.svg" />
@@ -120,7 +120,7 @@ function DetailPage() {
ref={sshRef}
onClick={() => {
positioningRef.current?.setTarget(sshRef.current as any);
copyContext(scpInfo?.data.spec.ssh?.pkData);
copyContext(scpInfo?.data?.spec?.ssh?.pkData);
}}
>
<SvgIcon src="/images/infraicon/scp_ssh.svg" />
@@ -44,7 +44,7 @@ spec:
- roles: [master]
count: ${infraForm.masterCount}
flavor: ${infraForm.masterType}
image: "ami-073b7de61877e5b7b"
image: "ami-09123565dfe16d662"
disks:
- capacity: ${infraForm.masterDisk}
volumeType: ${infraForm.masterDiskType}
@@ -54,7 +54,7 @@ spec:
- roles: [ node ]
count: ${infraForm.nodeCount}
flavor: ${infraForm.nodeType}
image: "ami-073b7de61877e5b7b"
image: "ami-09123565dfe16d662"
disks:
- capacity: ${infraForm.nodeDisk}
volumeType: ${infraForm.nodeDiskType}
@@ -1,12 +1,16 @@
type TIconfont = {
iconName: string;
color?: string;
width?: number;
height?: number;
};
function Iconfont(props: TIconfont) {
const { iconName, color } = props;
const { iconName, color, width, height } = props;
const style = {
fill: color
fill: color,
width,
height
};
return (
+2 -2
View File
@@ -17,7 +17,7 @@ spec:
- roles: [ master ]
count: {{ .masterCount }}
flavor: {{ .masterType }}
image: "ami-073b7de61877e5b7b"
image: "ami-09123565dfe16d662"
disks:
- capacity: {{.masterDisk}}
volumeType: {{.masterDiskType}}
@@ -25,7 +25,7 @@ spec:
- roles: [ node ]
count: {{ .nodeCount }}
flavor: {{ .nodeType }}
image: "ami-073b7de61877e5b7b"
image: "ami-09123565dfe16d662"
disks:
- capacity: {{.nodeDisk}}
volumeType: {{.nodeDiskType}}
+1 -1
View File
@@ -8,7 +8,7 @@ const installedApps: TApp[] = [
icon: '/images/sealos.svg',
type: APPTYPE.IFRAME,
data: {
url: 'https://www.sealos.io/docs/Intro',
url: 'https://www.sealos.io/docs/cloud/Intro',
desc: 'Sealos Documents'
},
gallery: [
@@ -34,6 +34,11 @@ type repositoryStatus = {
};
};
}[];
metadata: {
continue: string;
remainingItemCount: number;
resourceVersion: string;
};
};
type DataPackDesc = {
@@ -49,8 +54,7 @@ enum DataPackStatus {
}
export default async function handler(req: NextApiRequest, resp: NextApiResponse) {
const { kubeconfig, labels } = req.body;
const { kubeconfig, labels, _continue, limit } = req.body;
if (kubeconfig === '') {
return UnprocessableResp('kubeconfig or user empty', resp);
}
@@ -64,10 +68,12 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse
let images_names: Array<string> = [];
let repositories_metadata = {};
try {
const repositoryDesc = await ListClusterObject(kc, RepositoryMeta, labels);
const repositoryDesc = await ListClusterObject(kc, RepositoryMeta, labels, limit, _continue);
if (repositoryDesc?.body) {
const result = repositoryDesc.body as repositoryStatus;
repositories_metadata = result.metadata;
for (const item of result.items) {
images_names.push(item.spec.name + ':' + item.status.latestTag.name);
}
@@ -87,7 +93,7 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse
const datapackDesc = dataDesc.body.status as DataPackDesc;
if (datapackDesc.codes === DataPackStatus.Ok) {
let result = Object.values(datapackDesc.datas);
return JsonResp(result, resp);
return JsonResp({ data: result, metadata: repositories_metadata }, resp);
}
return JsonResp(datapackDesc, resp);
}
@@ -225,7 +225,9 @@ export async function ListSecret(
export async function ListClusterObject(
kc: k8s.KubeConfig,
meta: CRDMeta,
labelSelector: string
labelSelector: string,
limit: number,
_continue: string
): Promise<{
response: http.IncomingMessage;
body: object;
@@ -238,9 +240,10 @@ export async function ListClusterObject(
meta.plural,
undefined,
undefined,
_continue,
undefined,
undefined,
labelSelector
labelSelector,
limit
);
}