Skip to content

Commit 08dd52e

Browse files
committed
feat: implementing import and delete for revision
1 parent 006b666 commit 08dd52e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+2455
-1425
lines changed

data/schema.graphql

Lines changed: 116 additions & 390 deletions
Large diffs are not rendered by default.

packages/backend.ai-ui/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@
6363
"react-dom": "^19.0.0",
6464
"react-i18next": "^15.4.1",
6565
"react-relay": "^20.1.0",
66-
"relay-runtime": "^20.1.0"
66+
"relay-runtime": "^20.1.0",
67+
"react-router-dom": "^6.30.0"
6768
},
6869
"dependencies": {
6970
"@dnd-kit/core": "^6.1.0",
@@ -93,6 +94,7 @@
9394
"@types/react": "^19.0.0",
9495
"@types/react-dom": "^19.0.3",
9596
"@types/react-relay": "^18.2.1",
97+
"@types/relay-runtime": "^19.0.2",
9698
"@types/react-resizable": "^3.0.8",
9799
"@types/relay-test-utils": "^19.0.0",
98100
"@vitejs/plugin-react": "^4.5.0",

packages/backend.ai-ui/src/components/BAIGraphQLPropertyFilter.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,25 @@ interface FilterCondition {
136136
}
137137

138138
const OPERATORS_BY_TYPE: Record<FilterPropertyType, FilterOperator[]> = {
139-
string: ['equals', 'notEquals', 'contains', 'startsWith', 'endsWith', 'in', 'notIn'],
140-
number: ['equals', 'notEquals', 'greaterThan', 'greaterOrEqual', 'lessThan', 'lessOrEqual', 'in', 'notIn'],
139+
string: [
140+
'equals',
141+
'notEquals',
142+
'contains',
143+
'startsWith',
144+
'endsWith',
145+
'in',
146+
'notIn',
147+
],
148+
number: [
149+
'equals',
150+
'notEquals',
151+
'greaterThan',
152+
'greaterOrEqual',
153+
'lessThan',
154+
'lessOrEqual',
155+
'in',
156+
'notIn',
157+
],
141158
boolean: ['equals'],
142159
enum: ['equals', 'notEquals', 'in', 'notIn'],
143160
};
@@ -488,7 +505,9 @@ const BAIGraphQLPropertyFilter: React.FC<BAIGraphQLPropertyFilterProps> = ({
488505
options={propertyOptions}
489506
value={selectedProperty.key}
490507
onChange={(_value, options) => {
508+
console.log(options);
491509
const property = _.castArray(options)[0].filter;
510+
console.log(property);
492511
setSelectedProperty(property);
493512
const mode =
494513
property.valueMode ||
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import BAIFlex from './BAIFlex';
2+
import { Divider, Select, SelectProps, theme, Tooltip, Typography } from 'antd';
3+
import { createStyles } from 'antd-style';
4+
import { BaseOptionType, DefaultOptionType } from 'antd/es/select';
5+
import { GetRef } from 'antd/lib';
6+
import classNames from 'classnames';
7+
import _ from 'lodash';
8+
import React, { useLayoutEffect, useRef } from 'react';
9+
10+
const useStyles = createStyles(({ css, token }) => ({
11+
ghostSelect: css`
12+
&.ant-select {
13+
.ant-select-selector {
14+
background-color: transparent;
15+
border-color: ${token.colorBgBase} !important;
16+
/* box-shadow: none; */
17+
color: ${token.colorBgBase};
18+
/* transition: color 0.3s, border-color 0.3s; */
19+
}
20+
21+
&:hover .ant-select-selector {
22+
background-color: rgb(255 255 255 / 10%);
23+
}
24+
25+
&:active .ant-select-selector {
26+
background-color: rgb(255 255 255 / 10%);
27+
}
28+
29+
.ant-select-arrow {
30+
color: ${token.colorBgBase};
31+
}
32+
33+
&:hover .ant-select-arrow {
34+
color: ${token.colorBgBase};
35+
}
36+
37+
&:active .ant-select-arrow {
38+
color: ${token.colorBgBase};
39+
}
40+
}
41+
`,
42+
}));
43+
44+
export interface BAISelectProps<
45+
ValueType = any,
46+
OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType,
47+
> extends SelectProps<ValueType, OptionType> {
48+
ref?: React.RefObject<GetRef<typeof Select<ValueType, OptionType>> | null>;
49+
ghost?: boolean;
50+
autoSelectOption?:
51+
| boolean
52+
| ((options: SelectProps<ValueType, OptionType>['options']) => ValueType);
53+
tooltip?: string;
54+
atBottomThreshold?: number;
55+
atBottomStateChange?: (atBottom: boolean) => void;
56+
bottomLoading?: boolean;
57+
footer?: React.ReactNode;
58+
endReached?: () => void; // New prop for endReached
59+
}
60+
61+
function BAISelect<
62+
ValueType = any,
63+
OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType,
64+
>({
65+
ref,
66+
autoSelectOption,
67+
ghost,
68+
tooltip = '',
69+
atBottomThreshold = 30,
70+
atBottomStateChange,
71+
bottomLoading,
72+
footer,
73+
endReached, // Destructure the new prop
74+
...selectProps
75+
}: BAISelectProps<ValueType, OptionType>): React.ReactElement {
76+
const { value, options, onChange } = selectProps;
77+
const { styles } = useStyles();
78+
// const dropdownRef = useRef<HTMLDivElement | null>(null);
79+
const lastScrollTop = useRef<number>(0);
80+
const isAtBottom = useRef<boolean>(false);
81+
const { token } = theme.useToken();
82+
83+
useLayoutEffect(() => {
84+
if (autoSelectOption && _.isEmpty(value) && options?.[0]) {
85+
if (_.isBoolean(autoSelectOption)) {
86+
onChange?.(options?.[0].value || options?.[0], options?.[0]);
87+
} else if (_.isFunction(autoSelectOption)) {
88+
onChange?.(autoSelectOption(options), options?.[0]);
89+
}
90+
}
91+
}, [value, options, onChange, autoSelectOption]);
92+
93+
// Function to check if the scroll has reached the bottom
94+
const handlePopupScroll = (e: React.UIEvent<HTMLDivElement>) => {
95+
if (!atBottomStateChange && !endReached) return; // Check for endReached
96+
97+
const target = e.target as HTMLElement;
98+
const scrollTop = target.scrollTop;
99+
// const scrollDirection = scrollTop > lastScrollTop.current ? 'down' : 'up';
100+
lastScrollTop.current = scrollTop;
101+
102+
const isAtBottomNow =
103+
target.scrollHeight - scrollTop - target.clientHeight <=
104+
atBottomThreshold;
105+
106+
// Only notify when the state changes
107+
// ~~or when scrolling down at the bottom~~
108+
if (
109+
isAtBottomNow !== isAtBottom.current
110+
// ||
111+
// (isAtBottomNow && scrollDirection === 'down')
112+
) {
113+
isAtBottom.current = isAtBottomNow;
114+
atBottomStateChange?.(isAtBottomNow);
115+
116+
if (isAtBottomNow) {
117+
endReached?.(); // Call endReached when at the bottom
118+
}
119+
}
120+
};
121+
122+
return (
123+
<Tooltip title={tooltip}>
124+
<Select<ValueType, OptionType>
125+
{...selectProps}
126+
ref={ref}
127+
className={
128+
ghost
129+
? classNames(styles.ghostSelect, selectProps.className)
130+
: selectProps.className
131+
}
132+
onPopupScroll={(e) => {
133+
if (atBottomStateChange || endReached) handlePopupScroll(e);
134+
selectProps.onPopupScroll?.(e);
135+
}}
136+
dropdownRender={
137+
footer
138+
? (menu) => {
139+
// Process with custom dropdownRender if provided
140+
// const renderedMenu = selectProps.dropdownRender
141+
// ? selectProps.dropdownRender(menu)
142+
// : menu;
143+
144+
return (
145+
<BAIFlex direction="column" align="stretch">
146+
{menu}
147+
<Divider
148+
style={{
149+
margin: 0,
150+
marginBottom: token.paddingXS,
151+
}}
152+
/>
153+
<BAIFlex
154+
direction="column"
155+
align="end"
156+
gap={'xs'}
157+
style={{
158+
paddingBottom: token.paddingXXS,
159+
paddingInline: token.paddingSM,
160+
}}
161+
>
162+
{_.isString(footer) ? (
163+
<Typography.Text type="secondary">
164+
{footer}
165+
</Typography.Text>
166+
) : (
167+
footer
168+
)}
169+
</BAIFlex>
170+
</BAIFlex>
171+
);
172+
}
173+
: undefined
174+
}
175+
/>
176+
</Tooltip>
177+
);
178+
}
179+
180+
export default BAISelect;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import BAIFlex from './BAIFlex';
2+
import { LoadingOutlined } from '@ant-design/icons';
3+
import { theme, Typography } from 'antd';
4+
import { useTranslation } from 'react-i18next';
5+
6+
const TotalFooter: React.FC<{
7+
loading?: boolean;
8+
total?: number;
9+
}> = ({ loading, total }) => {
10+
const { token } = theme.useToken();
11+
const { t } = useTranslation();
12+
return (
13+
<BAIFlex justify="end" gap={'xs'}>
14+
{loading ? (
15+
<LoadingOutlined
16+
spin
17+
style={{
18+
color: token.colorTextSecondary,
19+
}}
20+
/>
21+
) : (
22+
<div />
23+
)}
24+
<Typography.Text type="secondary">
25+
{t('general.TotalItems', {
26+
total: total,
27+
})}
28+
</Typography.Text>
29+
</BAIFlex>
30+
);
31+
};
32+
33+
export default TotalFooter;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { BAIArtifactDescriptionsFragment$key } from '../../__generated__/BAIArtifactDescriptionsFragment.graphql';
2+
import BAILink from '../BAILink';
3+
import BAIArtifactTypeTag from './BAIArtifactTypeTag';
4+
import { Descriptions, DescriptionsProps } from 'antd';
5+
import dayjs from 'dayjs';
6+
import relativeTime from 'dayjs/plugin/relativeTime';
7+
import { useTranslation } from 'react-i18next';
8+
import { graphql, useFragment } from 'react-relay';
9+
10+
export interface BAIArtifactDescriptionsProps {
11+
artifactFrgmt: BAIArtifactDescriptionsFragment$key;
12+
}
13+
14+
dayjs.extend(relativeTime);
15+
16+
const BAIArtifactDescriptions = ({
17+
artifactFrgmt,
18+
}: BAIArtifactDescriptionsProps) => {
19+
const { t } = useTranslation();
20+
const artifact = useFragment<BAIArtifactDescriptionsFragment$key>(
21+
graphql`
22+
fragment BAIArtifactDescriptionsFragment on Artifact {
23+
name
24+
description
25+
source {
26+
name
27+
url
28+
}
29+
...BAIArtifactTypeTagFragment
30+
}
31+
`,
32+
artifactFrgmt,
33+
);
34+
35+
const items: DescriptionsProps['items'] = [
36+
{
37+
key: 'name',
38+
label: t('comp:BAIArtifactDescriptions.Name'),
39+
children: artifact.name,
40+
span: 2,
41+
},
42+
{
43+
key: 'type',
44+
label: t('comp:BAIArtifactDescriptions.Type'),
45+
children: <BAIArtifactTypeTag artifactTypeFrgmt={artifact} />,
46+
},
47+
{
48+
key: 'source',
49+
label: t('comp:BAIArtifactDescriptions.Source'),
50+
children: (
51+
<BAILink to={artifact.source.url ?? ''} target="_blank">
52+
{artifact.source.name}
53+
</BAILink>
54+
),
55+
},
56+
{
57+
key: 'description',
58+
label: t('comp:BAIArtifactDescriptions.Description'),
59+
children: artifact.description || '-',
60+
span: 2,
61+
},
62+
];
63+
64+
return <Descriptions column={2} bordered items={items} />;
65+
};
66+
67+
export default BAIArtifactDescriptions;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { BAIArtifactRevisionDeleteButtonFragment$key } from '../../__generated__/BAIArtifactRevisionDeleteButtonFragment.graphql';
2+
import { BAITrashBinIcon } from '../../icons';
3+
import { Button, ButtonProps, theme } from 'antd';
4+
import _ from 'lodash';
5+
import { graphql, useFragment } from 'react-relay';
6+
7+
export interface BAIArtifactRevisionDeleteButtonProps
8+
extends Omit<ButtonProps, 'icon'> {
9+
revisionsFrgmt: BAIArtifactRevisionDeleteButtonFragment$key;
10+
loading?: boolean;
11+
}
12+
13+
const BAIArtifactRevisionDeleteButton = ({
14+
revisionsFrgmt,
15+
...buttonProps
16+
}: BAIArtifactRevisionDeleteButtonProps) => {
17+
const { token } = theme.useToken();
18+
const revisions = useFragment<BAIArtifactRevisionDeleteButtonFragment$key>(
19+
graphql`
20+
fragment BAIArtifactRevisionDeleteButtonFragment on ArtifactRevision
21+
@relay(plural: true) {
22+
status
23+
}
24+
`,
25+
revisionsFrgmt,
26+
);
27+
const isDeletable = revisions.some(
28+
(revision) =>
29+
revision.status !== 'SCANNED' && revision.status !== 'PULLING',
30+
);
31+
32+
return (
33+
<Button
34+
icon={<BAITrashBinIcon />}
35+
disabled={buttonProps.disabled || buttonProps.loading || !isDeletable}
36+
style={{
37+
color: isDeletable ? token.colorError : token.colorTextDisabled,
38+
backgroundColor: isDeletable
39+
? token.colorErrorBg
40+
: token.colorBgContainerDisabled,
41+
...buttonProps.style,
42+
}}
43+
{..._.omit(buttonProps, ['style', 'disabled', 'loading'])}
44+
/>
45+
);
46+
};
47+
48+
export default BAIArtifactRevisionDeleteButton;

0 commit comments

Comments
 (0)