Skip to content

Commit 735e148

Browse files
committed
warning about mismatch timestamp
1 parent 7d24c37 commit 735e148

4 files changed

Lines changed: 252 additions & 45 deletions

File tree

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

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
// import { useDrag } from "react-dnd";
22

3-
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
3+
import {
4+
DeleteOutlined,
5+
EditOutlined,
6+
ExclamationCircleFilled,
7+
} from '@ant-design/icons';
48
import {
59
Button,
610
Col,
711
Form,
812
Input,
913
List,
1014
Modal,
15+
Popover,
1116
Row,
1217
Select,
1318
Tag,
1419
Typography,
1520
} from 'antd';
21+
import { Link } from 'react-router-dom';
22+
import { rootRouterPath } from '@/router';
1623
import { api } from '@/services/api';
24+
import { usePackageTimestampWarnings } from '@/utils/hooks';
1725
import { useManageContext } from '../hooks/useManageContext';
1826
import { Commit } from './commit';
1927
import { DepsTable } from './deps-table';
@@ -24,15 +32,32 @@ const PackageList = ({
2432
}: {
2533
dataSource?: Package[];
2634
loading?: boolean;
27-
}) => (
28-
<List
29-
loading={loading}
30-
className="packages"
31-
size="small"
32-
dataSource={dataSource}
33-
renderItem={(item) => <Item item={item} />}
34-
/>
35-
);
35+
}) => {
36+
const { appId } = useManageContext();
37+
const { app, packageTimestampWarnings } = usePackageTimestampWarnings(appId);
38+
const realtimeMetricsPath = app?.appKey
39+
? `${rootRouterPath.realtimeMetrics}?${new URLSearchParams({
40+
appKey: app.appKey,
41+
attribute: 'packageVersion_buildTime',
42+
}).toString()}`
43+
: undefined;
44+
45+
return (
46+
<List
47+
loading={loading}
48+
className="packages"
49+
size="small"
50+
dataSource={dataSource}
51+
renderItem={(item) => (
52+
<Item
53+
item={item}
54+
warningTimestamps={packageTimestampWarnings.get(item.id) ?? []}
55+
realtimeMetricsPath={realtimeMetricsPath}
56+
/>
57+
)}
58+
/>
59+
);
60+
};
3661
export default PackageList;
3762

3863
function remove(item: Package, appId: number) {
@@ -83,8 +108,49 @@ function edit(item: Package, appId: number) {
83108
});
84109
}
85110

86-
const Item = ({ item }: { item: Package }) => {
111+
const TimestampWarning = ({
112+
warningTimestamps,
113+
realtimeMetricsPath,
114+
}: {
115+
warningTimestamps: string[];
116+
realtimeMetricsPath: string;
117+
}) => (
118+
<Popover
119+
trigger="hover"
120+
content={
121+
<div className="max-w-72 text-xs leading-5">
122+
<div>发现不同时间戳:</div>
123+
<div className="mt-1 break-all text-gray-700">
124+
{warningTimestamps.map((timestamp) => (
125+
<div key={timestamp}>{timestamp}</div>
126+
))}
127+
</div>
128+
<div className="mt-2">
129+
需要在应用设置中打开“忽略时间戳”选项,否则这些包无法获得热更新。
130+
</div>
131+
<div className="mt-1">
132+
<Link to={realtimeMetricsPath}>点击此处查看实时数据</Link>
133+
</div>
134+
</div>
135+
}
136+
>
137+
<span className="ml-2 inline-flex cursor-help items-center text-amber-500">
138+
<ExclamationCircleFilled />
139+
</span>
140+
</Popover>
141+
);
142+
143+
const Item = ({
144+
item,
145+
warningTimestamps,
146+
realtimeMetricsPath,
147+
}: {
148+
item: Package;
149+
warningTimestamps: string[];
150+
realtimeMetricsPath?: string;
151+
}) => {
87152
const { appId } = useManageContext();
153+
const hasTimestampWarning = warningTimestamps.length > 0;
88154
return (
89155
// const [_, drag] = useDrag(() => ({ item, type: "package" }));
90156
<div className="bg-white my-0 [&_li]:px-0!">
@@ -93,10 +159,18 @@ const Item = ({ item }: { item: Package }) => {
93159
title={
94160
<Row align="middle">
95161
<Col flex={1}>
96-
{item.name}
97-
{item.status && item.status !== 'normal' && (
98-
<Tag className="ml-2">{status[item.status]}</Tag>
99-
)}
162+
<div className="flex flex-wrap items-center">
163+
<span>{item.name}</span>
164+
{hasTimestampWarning && realtimeMetricsPath && (
165+
<TimestampWarning
166+
warningTimestamps={warningTimestamps}
167+
realtimeMetricsPath={realtimeMetricsPath}
168+
/>
169+
)}
170+
{item.status && item.status !== 'normal' && (
171+
<Tag className="ml-2">{status[item.status]}</Tag>
172+
)}
173+
</div>
100174
</Col>
101175
<DepsTable deps={item.deps} name={`原生包 ${item.name}`} />
102176
<Commit commit={item.commit} />

src/pages/realtime-metrics.tsx

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import dayjs from 'dayjs';
66
import { useEffect, useMemo, useRef, useState } from 'react';
77
import { useSearchParams } from 'react-router-dom';
88
import { api } from '@/services/api';
9+
import { patchSearchParams } from '@/utils/helper';
910
import { useAppList, useUserInfo } from '@/utils/hooks';
1011

1112
const { Title } = Typography;
@@ -85,22 +86,22 @@ const attributeOptions = [
8586
];
8687

8788
export const Component = () => {
88-
const [searchParams, setSearchParams] = useSearchParams();
89+
const [searchParams, setSearchParams] = useSearchParams({ attribute: 'hash' });
8990
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([
9091
dayjs().subtract(24, 'hour'),
9192
dayjs(),
9293
]);
93-
const [selectedAppKey, setSelectedAppKey] = useState<string | undefined>(
94-
searchParams.get('appKey') || undefined,
95-
);
9694
const [manualAppKey, setManualAppKey] = useState('');
97-
const [selectedAttribute, setSelectedAttribute] =
98-
useState<MetricAttribute>('hash');
9995
const legendValuesRef = useRef<string[]>([]);
10096

10197
const { apps } = useAppList();
10298
const { user } = useUserInfo();
10399
const isAdmin = user?.admin === true;
100+
const urlAppKey = searchParams.get('appKey') || undefined;
101+
const selectedAttribute: MetricAttribute =
102+
searchParams.get('attribute') === 'packageVersion_buildTime'
103+
? 'packageVersion_buildTime'
104+
: 'hash';
104105

105106
const appOptions = useMemo(() => {
106107
return (apps || []).reduce<{ label: string; value: string }[]>(
@@ -116,42 +117,36 @@ export const Component = () => {
116117
[],
117118
);
118119
}, [apps]);
119-
120-
// Sync URL param to state on mount or URL change
121-
useEffect(() => {
122-
const urlAppKey = searchParams.get('appKey');
123-
if (urlAppKey && urlAppKey !== selectedAppKey) {
124-
// Admin can access any appKey, non-admin can only access their own
125-
if (isAdmin || appOptions.some((opt) => opt.value === urlAppKey)) {
126-
setSelectedAppKey(urlAppKey);
127-
}
120+
const selectedAppKey = useMemo(() => {
121+
if (!urlAppKey) {
122+
return undefined;
123+
}
124+
if (isAdmin || appOptions.some((opt) => opt.value === urlAppKey)) {
125+
return urlAppKey;
128126
}
129-
}, [searchParams, isAdmin, appOptions, selectedAppKey]);
127+
return undefined;
128+
}, [appOptions, isAdmin, urlAppKey]);
130129

131130
// Default to first app if no selection
132131
useEffect(() => {
133-
if (
134-
!selectedAppKey &&
135-
appOptions.length > 0 &&
136-
!searchParams.get('appKey')
137-
) {
138-
const firstAppKey = appOptions[0].value;
139-
setSelectedAppKey(firstAppKey);
140-
setSearchParams({ appKey: firstAppKey }, { replace: true });
132+
if (!urlAppKey && appOptions.length > 0) {
133+
patchSearchParams(setSearchParams, {
134+
appKey: appOptions[0].value,
135+
});
141136
}
142-
}, [appOptions, selectedAppKey, searchParams, setSearchParams]);
137+
}, [appOptions, setSearchParams, urlAppKey]);
143138

144139
// Update URL when selection changes
145140
const handleAppChange = (appKey: string) => {
146-
setSelectedAppKey(appKey);
147-
setSearchParams({ appKey }, { replace: true });
141+
patchSearchParams(setSearchParams, { appKey });
148142
};
149143

150144
// Admin manual appKey input
151145
const handleManualAppKeySubmit = () => {
152146
if (manualAppKey.trim()) {
153-
setSelectedAppKey(manualAppKey.trim());
154-
setSearchParams({ appKey: manualAppKey.trim() }, { replace: true });
147+
patchSearchParams(setSearchParams, {
148+
appKey: manualAppKey.trim(),
149+
});
155150
setManualAppKey('');
156151
}
157152
};
@@ -355,7 +350,11 @@ export const Component = () => {
355350
</div>
356351
<Radio.Group
357352
value={selectedAttribute}
358-
onChange={(e) => setSelectedAttribute(e.target.value)}
353+
onChange={(e) => {
354+
patchSearchParams(setSearchParams, {
355+
attribute: e.target.value as MetricAttribute,
356+
});
357+
}}
359358
optionType="button"
360359
buttonStyle="solid"
361360
>

src/utils/helper.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { NavigateOptions, SetURLSearchParams } from 'react-router-dom';
2+
13
export function isPasswordValid(password: string) {
24
return /(?!^[0-9]+$)(?!^[a-z]+$)(?!^[^A-Z]+$)^.{6,16}$/.test(password);
35
}
@@ -77,3 +79,23 @@ export const isExpVersion = (
7779

7880
return rollout < 100;
7981
};
82+
83+
export const patchSearchParams = (
84+
setSearchParams: SetURLSearchParams,
85+
patch: Record<string, string | null | undefined>,
86+
navigateOptions: NavigateOptions = { replace: true },
87+
) => {
88+
setSearchParams((prev) => {
89+
const next = new URLSearchParams(prev);
90+
91+
for (const [key, value] of Object.entries(patch)) {
92+
if (value == null) {
93+
next.delete(key);
94+
} else {
95+
next.set(key, value);
96+
}
97+
}
98+
99+
return next;
100+
}, navigateOptions);
101+
};

0 commit comments

Comments
 (0)