Skip to content

Commit 972bf24

Browse files
committed
Support batch binding and package deletion UI
1 parent 6e9db92 commit 972bf24

2 files changed

Lines changed: 241 additions & 51 deletions

File tree

src/pages/manage/components/bind-package.tsx

Lines changed: 115 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ type DepChangeSummary = {
3636

3737
type DepChangeFilters = Record<DepChangeType, boolean>;
3838

39+
type PublishPackage = {
40+
id: number;
41+
name: string;
42+
deps?: Record<string, string>;
43+
};
44+
45+
type DepsChangePackage = {
46+
pkg: PublishPackage;
47+
changes: DepChangeRow[];
48+
};
49+
3950
function getDepsChangeSummary(changes: DepChangeRow[]): DepChangeSummary {
4051
return changes.reduce(
4152
(acc, item) => {
@@ -304,44 +315,131 @@ const BindPackage = ({
304315
(p) => !matchedPackageIds.has(p.id),
305316
);
306317

307-
const publishToPackage = (
308-
pkg: { id: number; name: string; deps?: Record<string, string> },
309-
rollout?: number,
310-
) => {
311-
const publish = () =>
312-
api.upsertBinding({
318+
const publishToPackages = (pkgs: PublishPackage[], rollout?: number) => {
319+
if (pkgs.length === 0) {
320+
return;
321+
}
322+
323+
const publish = () => {
324+
if (pkgs.length === 1) {
325+
return api.upsertBinding({
326+
appId,
327+
packageId: pkgs[0].id,
328+
versionId,
329+
rollout,
330+
});
331+
}
332+
333+
return api.upsertBindings({
313334
appId,
314-
packageId: pkg.id,
335+
packageIds: pkgs.map((pkg) => pkg.id),
315336
versionId,
316337
rollout,
317338
});
339+
};
318340

319-
const changes = getDepsChanges(pkg.deps, versionDeps);
320-
if (!changes || changes.length === 0) {
341+
const depsChangedPackages = pkgs.reduce<DepsChangePackage[]>((acc, pkg) => {
342+
const changes = getDepsChanges(pkg.deps, versionDeps);
343+
if (changes?.length) {
344+
acc.push({ pkg, changes });
345+
}
346+
return acc;
347+
}, []);
348+
if (depsChangedPackages.length === 0) {
321349
void publish();
322350
return;
323351
}
324352

353+
const content =
354+
depsChangedPackages.length === 1 ? (
355+
<DepsChangeConfirmContent
356+
packageName={depsChangedPackages[0].pkg.name}
357+
versionDisplayName={versionName || versionId}
358+
changes={depsChangedPackages[0].changes}
359+
/>
360+
) : (
361+
<div className="max-h-96 space-y-6 overflow-y-auto pr-2">
362+
{depsChangedPackages.map(({ pkg, changes }) => (
363+
<DepsChangeConfirmContent
364+
key={pkg.id}
365+
packageName={pkg.name}
366+
versionDisplayName={versionName || versionId}
367+
changes={changes}
368+
/>
369+
))}
370+
</div>
371+
);
372+
325373
Modal.confirm({
326374
title: '检测到依赖变化,确认继续发布?',
327375
maskClosable: true,
328376
okButtonProps: { danger: true },
329377
okText: '继续发布',
330378
cancelText: '取消',
331379
width: 820,
332-
content: (
333-
<DepsChangeConfirmContent
334-
packageName={pkg.name}
335-
versionDisplayName={versionName || versionId}
336-
changes={changes}
337-
/>
338-
),
380+
content,
339381
async onOk() {
340382
await publish();
341383
},
342384
});
343385
};
344386

387+
const publishToPackage = (pkg: PublishPackage, rollout?: number) =>
388+
publishToPackages([pkg], rollout);
389+
390+
const publishMenuItems: MenuProps['items'] = [];
391+
if (availablePackages.length > 1) {
392+
publishMenuItems.push(
393+
{
394+
key: 'all',
395+
label: '全部可用原生包',
396+
children: [
397+
{
398+
key: 'all-full',
399+
label: '全量',
400+
icon: <CloudDownloadOutlined />,
401+
onClick: () => publishToPackages(availablePackages),
402+
},
403+
{
404+
key: 'all-gray',
405+
label: '灰度',
406+
icon: <ExperimentOutlined />,
407+
children: [1, 2, 5, 10, 20, 50].map((percentage) => ({
408+
key: `all-gray-${percentage}`,
409+
label: `${percentage}%`,
410+
onClick: () => publishToPackages(availablePackages, percentage),
411+
})),
412+
},
413+
],
414+
},
415+
{ type: 'divider' },
416+
);
417+
}
418+
publishMenuItems.push(
419+
...availablePackages.map((p) => ({
420+
key: `pkg-${p.id}`,
421+
label: p.name,
422+
children: [
423+
{
424+
key: `pkg-${p.id}-full`,
425+
label: '全量',
426+
icon: <CloudDownloadOutlined />,
427+
onClick: () => publishToPackage(p),
428+
},
429+
{
430+
key: `pkg-${p.id}-gray`,
431+
label: '灰度',
432+
icon: <ExperimentOutlined />,
433+
children: [1, 2, 5, 10, 20, 50].map((percentage) => ({
434+
key: `pkg-${p.id}-gray-${percentage}`,
435+
label: `${percentage}%`,
436+
onClick: () => publishToPackage(p, percentage),
437+
})),
438+
},
439+
],
440+
})),
441+
);
442+
345443
const bindedPackages = (() => {
346444
const result = [];
347445
if (matchedBindings.length === 0 || allPackages.length === 0) return null;
@@ -432,28 +530,7 @@ const BindPackage = ({
432530
{availablePackages.length !== 0 && (
433531
<Dropdown
434532
menu={{
435-
items: availablePackages.map((p) => ({
436-
key: `pkg-${p.id}`,
437-
label: p.name,
438-
children: [
439-
{
440-
key: `pkg-${p.id}-full`,
441-
label: '全量',
442-
icon: <CloudDownloadOutlined />,
443-
onClick: () => publishToPackage(p),
444-
},
445-
{
446-
key: `pkg-${p.id}-gray`,
447-
label: '灰度',
448-
icon: <ExperimentOutlined />,
449-
children: [1, 2, 5, 10, 20, 50].map((percentage) => ({
450-
key: `pkg-${p.id}-gray-${percentage}`,
451-
label: `${percentage}%`,
452-
onClick: () => publishToPackage(p, percentage),
453-
})),
454-
},
455-
],
456-
})),
533+
items: publishMenuItems,
457534
}}
458535
className="ant-typography-edit"
459536
>

src/pages/manage/components/package-list.tsx

Lines changed: 126 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from '@ant-design/icons';
88
import {
99
Button,
10+
Checkbox,
1011
Col,
1112
Form,
1213
Input,
@@ -18,6 +19,7 @@ import {
1819
Tag,
1920
Typography,
2021
} from 'antd';
22+
import { useMemo, useState } from 'react';
2123
import { Link } from 'react-router-dom';
2224
import { rootRouterPath } from '@/router';
2325
import { api } from '@/services/api';
@@ -32,32 +34,134 @@ const PackageList = ({
3234
dataSource?: Package[];
3335
loading?: boolean;
3436
}) => {
35-
const { app, packageTimestampWarnings } = useManageContext();
37+
const { app, appId, packageTimestampWarnings } = useManageContext();
38+
const [selectedPackageIds, setSelectedPackageIds] = useState<number[]>([]);
39+
const selectedPackageIdSet = useMemo(
40+
() => new Set(selectedPackageIds),
41+
[selectedPackageIds],
42+
);
43+
const visiblePackageIds = useMemo(
44+
() => dataSource?.map((item) => item.id) ?? [],
45+
[dataSource],
46+
);
47+
const selectedPackages = useMemo(
48+
() => dataSource?.filter((item) => selectedPackageIdSet.has(item.id)) ?? [],
49+
[dataSource, selectedPackageIdSet],
50+
);
51+
const selectedVisibleCount = visiblePackageIds.filter((id) =>
52+
selectedPackageIdSet.has(id),
53+
).length;
54+
const allVisibleSelected =
55+
visiblePackageIds.length > 0 &&
56+
selectedVisibleCount === visiblePackageIds.length;
57+
const hasSelectedVisiblePackages = selectedPackages.length > 0;
3658
const realtimeMetricsPath = app?.appKey
3759
? `${rootRouterPath.realtimeMetrics}?${new URLSearchParams({
3860
appKey: app.appKey,
3961
attribute: 'packageVersion_buildTime',
4062
}).toString()}`
4163
: undefined;
4264

65+
const togglePackageSelection = (packageId: number, checked: boolean) => {
66+
setSelectedPackageIds((prev) => {
67+
if (checked) {
68+
return [...new Set([...prev, packageId])];
69+
}
70+
return prev.filter((id) => id !== packageId);
71+
});
72+
};
73+
74+
const toggleAllVisiblePackages = (checked: boolean) => {
75+
setSelectedPackageIds((prev) => {
76+
if (checked) {
77+
return [...new Set([...prev, ...visiblePackageIds])];
78+
}
79+
return prev.filter((id) => !visiblePackageIds.includes(id));
80+
});
81+
};
82+
4383
return (
44-
<List
45-
loading={loading}
46-
className="packages"
47-
size="small"
48-
dataSource={dataSource}
49-
renderItem={(item) => (
50-
<Item
51-
item={item}
52-
warningTimestamps={packageTimestampWarnings.get(item.id) ?? []}
53-
realtimeMetricsPath={realtimeMetricsPath}
54-
/>
84+
<>
85+
{visiblePackageIds.length > 0 && (
86+
<div className="mb-2 flex items-center gap-2 px-2">
87+
<Checkbox
88+
checked={allVisibleSelected}
89+
indeterminate={selectedVisibleCount > 0 && !allVisibleSelected}
90+
onChange={({ target }) => {
91+
toggleAllVisiblePackages(target.checked);
92+
}}
93+
/>
94+
{hasSelectedVisiblePackages && (
95+
<Button
96+
danger
97+
size="small"
98+
icon={<DeleteOutlined />}
99+
onClick={() =>
100+
removeSelectedPackages(selectedPackages, appId, () => {
101+
setSelectedPackageIds((prev) =>
102+
prev.filter(
103+
(id) => !selectedPackages.some((item) => item.id === id),
104+
),
105+
);
106+
})
107+
}
108+
>
109+
删除
110+
</Button>
111+
)}
112+
</div>
55113
)}
56-
/>
114+
<List
115+
loading={loading}
116+
className="packages"
117+
size="small"
118+
dataSource={dataSource}
119+
renderItem={(item) => (
120+
<Item
121+
item={item}
122+
selected={selectedPackageIdSet.has(item.id)}
123+
onSelectedChange={(checked) =>
124+
togglePackageSelection(item.id, checked)
125+
}
126+
warningTimestamps={packageTimestampWarnings.get(item.id) ?? []}
127+
realtimeMetricsPath={realtimeMetricsPath}
128+
/>
129+
)}
130+
/>
131+
</>
57132
);
58133
};
59134
export default PackageList;
60135

136+
function removeSelectedPackages(
137+
items: Package[],
138+
appId: number,
139+
onSuccess: () => void,
140+
) {
141+
if (items.length === 0) {
142+
return;
143+
}
144+
Modal.confirm({
145+
title: '删除所选原生包:',
146+
content: (
147+
<div className="max-h-48 overflow-y-auto">
148+
{items.map((item) => (
149+
<div key={item.id}>{item.name}</div>
150+
))}
151+
</div>
152+
),
153+
maskClosable: true,
154+
okButtonProps: { danger: true },
155+
async onOk() {
156+
await api.deletePackages({
157+
appId,
158+
packageIds: items.map((item) => item.id),
159+
});
160+
onSuccess();
161+
},
162+
});
163+
}
164+
61165
function remove(item: Package, appId: number) {
62166
Modal.confirm({
63167
title: `删除后无法恢复,确定删除原生包“${item.name}”?`,
@@ -140,10 +244,14 @@ const TimestampWarning = ({
140244

141245
const Item = ({
142246
item,
247+
selected,
248+
onSelectedChange,
143249
warningTimestamps,
144250
realtimeMetricsPath,
145251
}: {
146252
item: Package;
253+
selected: boolean;
254+
onSelectedChange: (checked: boolean) => void;
147255
warningTimestamps: string[];
148256
realtimeMetricsPath?: string;
149257
}) => {
@@ -155,6 +263,11 @@ const Item = ({
155263
<List.Item.Meta
156264
title={
157265
<Row align="middle">
266+
<Checkbox
267+
className="mr-2"
268+
checked={selected}
269+
onChange={({ target }) => onSelectedChange(target.checked)}
270+
/>
158271
<Col flex={1}>
159272
<div className="flex flex-wrap items-center">
160273
<span>{item.name}</span>

0 commit comments

Comments
 (0)