Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/backend.ai-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
"react-dom": "^19.0.0",
"react-i18next": "^15.4.1",
"react-relay": "^20.1.0",
"relay-runtime": "^20.1.0"
"relay-runtime": "^20.1.0",
"react-router-dom": "^6.30.0"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
Expand Down
21 changes: 21 additions & 0 deletions packages/backend.ai-ui/src/components/BAIBackButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Button } from 'antd';
import { ArrowLeft } from 'lucide-react';
import { NavigateOptions, To, useNavigate } from 'react-router-dom';

export interface BAIBackButtonProps {
to: To;
options?: NavigateOptions;
}

const BAIBackButton = ({ to, options }: BAIBackButtonProps) => {
const navigate = useNavigate();
return (
<Button
type="text"
icon={<ArrowLeft size={18} />}
onClick={() => navigate(to, options)}
/>
);
};

export default BAIBackButton;
33 changes: 33 additions & 0 deletions packages/backend.ai-ui/src/components/BAITag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ConfigProvider, Tag, theme } from 'antd';
import { TagProps } from 'antd/lib/tag';
import React from 'react';

interface BAITagProps extends TagProps {}

const BAITag: React.FC<BAITagProps> = ({ ...tagProps }) => {
const { token } = theme.useToken();
return (
<ConfigProvider
theme={{
components: {
Tag: {
borderRadiusSM: 11,
colorText: '#999999',
defaultBg: 'transparent',
colorInfoBg: 'transparent',
colorWarningBg: 'transparent',
colorErrorBg: 'transparent',
colorSuccessBg: 'transparent',
},
},
}}
>
<Tag
style={{ paddingLeft: token.paddingSM, paddingRight: token.paddingSM }}
{...tagProps}
/>
</ConfigProvider>
);
};

export default BAITag;
40 changes: 40 additions & 0 deletions packages/backend.ai-ui/src/components/BAIText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Typography } from 'antd';
import type { TextProps as AntdTextProps } from 'antd/es/typography/Text';
import React from 'react';

export interface BAITextProps extends AntdTextProps {
monospace?: boolean;
}

const BAIText: React.FC<BAITextProps> = ({
type,
style,
monospace,
children,
...restProps
}) => {
// If monospace prop is true, apply monospace font styling
if (monospace) {
return (
<Typography.Text
type={type}
{...restProps}
style={{
fontFamily: 'monospace',
...style,
}}
>
{children}
</Typography.Text>
);
}

// For non-monospace text, pass all props directly to antd Text
return (
<Typography.Text type={type} style={style} {...restProps}>
{children}
</Typography.Text>
);
};

export default BAIText;
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import {
BAIArtifactRevisionTableArtifactRevisionFragment$data,
BAIArtifactRevisionTableArtifactRevisionFragment$key,
} from '../../__generated__/BAIArtifactRevisionTableArtifactRevisionFragment.graphql';
import { BAIArtifactRevisionTableLatestRevisionFragment$key } from '../../__generated__/BAIArtifactRevisionTableLatestRevisionFragment.graphql';
import { convertToDecimalUnit, filterOutEmpty } from '../../helper';
import BAIFlex from '../BAIFlex';
import BAITag from '../BAITag';
import BAIText from '../BAIText';
import { BAIColumnsType, BAITable, BAITableProps } from '../Table';
import { Button, Tag } from 'antd';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { Download } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { graphql, useFragment } from 'react-relay';

dayjs.extend(relativeTime);

export type ArtifactRevision = NonNullable<
NonNullable<BAIArtifactRevisionTableArtifactRevisionFragment$data>[number]
>;

export interface BAIArtifactRevisionTableProps
extends Omit<
BAITableProps<ArtifactRevision>,
'dataSource' | 'columns' | 'rowKey'
> {
artifactRevisionFrgmt: BAIArtifactRevisionTableArtifactRevisionFragment$key;
onClickDownload: (revisionId: string) => void;
latestRevisionFrgmt:
| BAIArtifactRevisionTableLatestRevisionFragment$key
| null
| undefined;
}

const BAIArtifactRevisionTable = ({
artifactRevisionFrgmt,
onClickDownload,
latestRevisionFrgmt,
...tableProps
}: BAIArtifactRevisionTableProps) => {
const { t } = useTranslation();

const artifactRevision =
useFragment<BAIArtifactRevisionTableArtifactRevisionFragment$key>(
graphql`
fragment BAIArtifactRevisionTableArtifactRevisionFragment on ArtifactRevision
@relay(plural: true) {
id
version
size
status
updatedAt
}
`,
artifactRevisionFrgmt,
);
const latestRevision =
useFragment<BAIArtifactRevisionTableLatestRevisionFragment$key>(
graphql`
fragment BAIArtifactRevisionTableLatestRevisionFragment on ArtifactRevision {
id
}
`,
latestRevisionFrgmt,
);

const columns: BAIColumnsType<ArtifactRevision> = [
{
title: t('comp:BAIArtifactRevisionTable.Version'),
dataIndex: 'version',
key: 'version',
width: '30%',
render: (version: string, record: ArtifactRevision) => (
<div>
<BAIFlex align="center" gap={'xs'}>
<BAIText monospace strong>
{version}
</BAIText>
{latestRevision && latestRevision.id === record.id && (
<Tag color="blue">Latest</Tag>
)}
{record.status === 'PULLED' && <BAITag>{record.status}</BAITag>}
</BAIFlex>
</div>
),
},
{
title: t('comp:BAIArtifactRevisionTable.Status'),
dataIndex: 'status',
key: 'status',
width: '15%',
render: (value: string) => {
return <BAITag>{value}</BAITag>;
},
},
{
title: t('comp:BAIArtifactRevisionTable.Action'),
key: 'action',
width: '15%',
render: (_, record: ArtifactRevision) => {
const status = record.status;
const isDownloadable = status === 'SCANNED';
const isLoading = status === 'PULLING' || status === 'VERIFYING';

return (
<Button
icon={<Download size={16} />}
type={'primary'}
size="small"
onClick={() => {
if (isDownloadable) {
onClickDownload(record.id);
}
}}
disabled={isLoading || !isDownloadable}
loading={isLoading}
>
Pull
</Button>
);
},
},
{
title: t('comp:BAIArtifactRevisionTable.Size'),
dataIndex: 'size',
key: 'size',
width: '15%',
render: (size: number) => {
if (!size) return <BAIText monospace>N/A</BAIText>;
return (
<BAIText monospace>
{convertToDecimalUnit(size, 'auto')?.displayValue}
</BAIText>
);
},
},
{
title: t('comp:BAIArtifactTable.Updated'),
dataIndex: 'updatedAt',
key: 'updatedAt',
width: '15%',
render: (updated_at: string) => (
<BAIText type="secondary" title={dayjs(updated_at).toString()}>
{dayjs(updated_at).fromNow()}
</BAIText>
),
},
];

return (
<BAITable<ArtifactRevision>
rowKey={(record) => record.id}
resizable
columns={filterOutEmpty(columns)}
dataSource={artifactRevision}
scroll={{ x: 'max-content' }}
{...tableProps}
></BAITable>
);
};

export default BAIArtifactRevisionTable;
Loading
Loading