添加依赖管理

This commit is contained in:
whyour 2021-10-24 17:07:06 +08:00
parent bd8953ea81
commit 0c2a2af308
12 changed files with 270 additions and 159 deletions

View File

@ -87,6 +87,24 @@ export default (app: Router) => {
}, },
); );
route.delete(
'/dependencies/force',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.removeDb(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get( route.get(
'/dependencies/:id', '/dependencies/:id',
celebrate({ celebrate({

View File

@ -24,6 +24,9 @@ export enum DependenceStatus {
'installing', 'installing',
'installed', 'installed',
'installFailed', 'installFailed',
'removing',
'removed',
'removeFailed',
} }
export enum DependenceTypes { export enum DependenceTypes {
@ -33,13 +36,13 @@ export enum DependenceTypes {
} }
export enum InstallDependenceCommandTypes { export enum InstallDependenceCommandTypes {
'pnpm install -g', 'npm i -g',
'pip3 install', 'pip3 install',
'apk add --no-cache', 'apk add --no-cache',
} }
export enum unInstallDependenceCommandTypes { export enum unInstallDependenceCommandTypes {
'pnpm uninstall -g', 'npm uninstall -g',
'pip3 uninstall', 'pip3 uninstall',
'apk del', 'apk del',
} }

View File

@ -26,8 +26,8 @@ export default async () => {
{ multi: true }, { multi: true },
); );
// 初始化时安装所有依赖 // 初始化时安装所有处于安装中,安装成功,安装失败的依赖
dependenceDb.find({}).exec((err, docs) => { dependenceDb.find({ status: { $in: [0, 1, 2] } }).exec((err, docs) => {
const groups = _.groupBy(docs, 'type'); const groups = _.groupBy(docs, 'type');
for (const key in groups) { for (const key in groups) {
if (Object.prototype.hasOwnProperty.call(groups, key)) { if (Object.prototype.hasOwnProperty.call(groups, key)) {

View File

@ -30,6 +30,8 @@ export default async ({ server }: { server: Server }) => {
}); });
return; return;
} else {
conn.write(JSON.stringify({ type: 'ping', message: '404' }));
} }
} }

View File

@ -85,11 +85,15 @@ export default class DependenceService {
public async remove(ids: string[]) { public async remove(ids: string[]) {
return new Promise((resolve: any) => { return new Promise((resolve: any) => {
this.dependenceDb.find({ _id: { $in: ids } }).exec((err, docs) => { this.dependenceDb.update(
this.installOrUninstallDependencies(docs, false); { _id: { $in: ids } },
this.removeDb(ids); { $set: { status: DependenceStatus.removing, log: [] } },
resolve(); { multi: true, returnUpdatedDocs: true },
}); async (err, num, docs: Dependence[]) => {
this.installOrUninstallDependencies(docs, false);
resolve(docs);
},
);
}); });
} }
@ -189,19 +193,20 @@ export default class DependenceService {
? InstallDependenceCommandTypes ? InstallDependenceCommandTypes
: unInstallDependenceCommandTypes : unInstallDependenceCommandTypes
)[dependencies[0].type as any]; )[dependencies[0].type as any];
const actionText = isInstall ? '安装' : '删除';
const depIds = dependencies.map((x) => x._id) as string[]; const depIds = dependencies.map((x) => x._id) as string[];
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' }); const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
const startTime = Date.now(); const startTime = Date.now();
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'installDependence', type: 'installDependence',
message: `开始安装依赖 ${depNames},开始时间 ${new Date( message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime, startTime,
).toLocaleString()}`, ).toLocaleString()}`,
references: depIds, references: depIds,
}); });
this.updateLog( this.updateLog(
depIds, depIds,
`开始安装依赖 ${depNames},开始时间 ${new Date( `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime, startTime,
).toLocaleString()}\n`, ).toLocaleString()}\n`,
); );
@ -211,7 +216,7 @@ export default class DependenceService {
message: data.toString(), message: data.toString(),
references: depIds, references: depIds,
}); });
isInstall && this.updateLog(depIds, data.toString()); this.updateLog(depIds, data.toString());
}); });
cp.stderr.on('data', (data) => { cp.stderr.on('data', (data) => {
@ -220,7 +225,7 @@ export default class DependenceService {
message: data.toString(), message: data.toString(),
references: depIds, references: depIds,
}); });
isInstall && this.updateLog(depIds, data.toString()); this.updateLog(depIds, data.toString());
}); });
cp.on('error', (err) => { cp.on('error', (err) => {
@ -229,34 +234,53 @@ export default class DependenceService {
message: JSON.stringify(err), message: JSON.stringify(err),
references: depIds, references: depIds,
}); });
isInstall && this.updateLog(depIds, JSON.stringify(err)); this.updateLog(depIds, JSON.stringify(err));
}); });
cp.on('close', (code) => { cp.on('close', (code) => {
const endTime = Date.now(); const endTime = Date.now();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'installDependence', type: 'installDependence',
message: `依赖安装结束,结束时间 ${new Date( message: `依赖${actionText}${resultText},结束时间 ${new Date(
endTime, endTime,
).toLocaleString()} ${(endTime - startTime) / 1000} `, ).toLocaleString()} ${(endTime - startTime) / 1000} `,
references: depIds, references: depIds,
}); });
isInstall && this.updateLog(
this.updateLog( depIds,
depIds, `依赖${actionText}${resultText},结束时间 ${new Date(
`依赖安装结束,结束时间 ${new Date(endTime).toLocaleString()},耗时 ${ endTime,
(endTime - startTime) / 1000 ).toLocaleString()} ${(endTime - startTime) / 1000} `,
} `, );
);
isInstall && let status = null;
this.dependenceDb.update( if (isSucceed) {
{ _id: { $in: depIds } }, status = isInstall
{ ? DependenceStatus.installed
$set: { status: DependenceStatus.installed }, : DependenceStatus.removed;
$unset: { pid: true }, } else {
}, status = isInstall
{ multi: true }, ? DependenceStatus.installFailed
); : DependenceStatus.removeFailed;
}
this.dependenceDb.update(
{ _id: { $in: depIds } },
{
$set: { status },
$unset: { pid: true },
},
{ multi: true },
);
// 如果删除依赖成功3秒后删除数据库记录
if (isSucceed && !isInstall) {
setTimeout(() => {
this.removeDb(depIds);
}, 5000);
}
}); });
} }
} }

View File

@ -7,6 +7,7 @@ import {
FolderOutlined, FolderOutlined,
RadiusSettingOutlined, RadiusSettingOutlined,
ControlOutlined, ControlOutlined,
ContainerOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
export default { export default {
@ -51,7 +52,7 @@ export default {
{ {
path: '/dependence', path: '/dependence',
name: '依赖管理', name: '依赖管理',
icon: <FormOutlined />, icon: <ContainerOutlined />,
component: '@/pages/dependence/index', component: '@/pages/dependence/index',
}, },
{ {

View File

@ -26,6 +26,7 @@ export default function (props: any) {
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>(); const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>();
const ws = useRef<any>(null); const ws = useRef<any>(null);
const [socketMessage, setSocketMessage] = useState<any>();
const logout = () => { const logout = () => {
request.post(`${config.apiPrefix}logout`).then(() => { request.post(`${config.apiPrefix}logout`).then(() => {
@ -124,6 +125,7 @@ export default function (props: any) {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!user) return;
ws.current = new SockJS( ws.current = new SockJS(
`${location.origin}/api/ws?token=${localStorage.getItem(config.authKey)}`, `${location.origin}/api/ws?token=${localStorage.getItem(config.authKey)}`,
); );
@ -131,11 +133,14 @@ export default function (props: any) {
ws.current.onmessage = (e: any) => { ws.current.onmessage = (e: any) => {
try { try {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
if (data && data.message === 'hanhh') { if (data.type === 'ping') {
console.log('websocket连接成功', e); if (data && data.message === 'hanhh') {
} else { console.log('websocket连接成功', e);
console.log('websocket连接失败', e); } else {
console.log('websocket连接失败', e);
}
} }
setSocketMessage(data);
} catch (error) { } catch (error) {
console.log('websocket连接失败', e); console.log('websocket连接失败', e);
} }
@ -146,7 +151,7 @@ export default function (props: any) {
return () => { return () => {
wsCurrent.close(); wsCurrent.close();
}; };
}, []); }, [user]);
if (['/login', '/initialization'].includes(props.location.pathname)) { if (['/login', '/initialization'].includes(props.location.pathname)) {
document.title = `${ document.title = `${
@ -246,7 +251,7 @@ export default function (props: any) {
user, user,
reloadUser, reloadUser,
reloadTheme: setTheme, reloadTheme: setTheme,
ws: ws.current, socketMessage,
}); });
})} })}
</ProLayout> </ProLayout>

View File

@ -37,6 +37,9 @@ enum Status {
'安装中', '安装中',
'已安装', '已安装',
'安装失败', '安装失败',
'删除中',
'已删除',
'删除失败',
} }
enum StatusColor { enum StatusColor {
@ -45,7 +48,7 @@ enum StatusColor {
'error', 'error',
} }
const Dependence = ({ headerStyle, isPhone, ws }: any) => { const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
const columns: any = [ const columns: any = [
{ {
title: '序号', title: '序号',
@ -69,7 +72,10 @@ const Dependence = ({ headerStyle, isPhone, ws }: any) => {
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return ( return (
<Space size="middle" style={{ cursor: 'text' }}> <Space size="middle" style={{ cursor: 'text' }}>
<Tag color={StatusColor[record.status]} style={{ marginRight: 0 }}> <Tag
color={StatusColor[record.status % 3]}
style={{ marginRight: 0 }}
>
{Status[record.status]} {Status[record.status]}
</Tag> </Tag>
</Space> </Space>
@ -93,20 +99,21 @@ const Dependence = ({ headerStyle, isPhone, ws }: any) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
<Space size="middle"> <Space size="middle">
{record.status !== Status. && ( {record.status !== Status. &&
<> record.status !== Status. && (
<Tooltip title={isPc ? '重新安装' : ''}> <>
<a onClick={() => reInstallDependence(record, index)}> <Tooltip title={isPc ? '重新安装' : ''}>
<BugOutlined /> <a onClick={() => reInstallDependence(record, index)}>
</a> <BugOutlined />
</Tooltip> </a>
<Tooltip title={isPc ? '删除' : ''}> </Tooltip>
<a onClick={() => deleteDependence(record, index)}> <Tooltip title={isPc ? '删除' : ''}>
<DeleteOutlined /> <a onClick={() => deleteDependence(record, index)}>
</a> <DeleteOutlined />
</Tooltip> </a>
</> </Tooltip>
)} </>
)}
<Tooltip title={isPc ? '日志' : ''}> <Tooltip title={isPc ? '日志' : ''}>
<a <a
onClick={() => { onClick={() => {
@ -171,10 +178,7 @@ const Dependence = ({ headerStyle, isPhone, ws }: any) => {
.delete(`${config.apiPrefix}dependencies`, { data: [record._id] }) .delete(`${config.apiPrefix}dependencies`, { data: [record._id] })
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
message.success('删除成功'); handleDependence(data.data[0]);
const result = [...value];
result.splice(index, 1);
setValue(result);
} else { } else {
message.error(data); message.error(data);
} }
@ -254,13 +258,12 @@ const Dependence = ({ headerStyle, isPhone, ws }: any) => {
const delDependencies = () => { const delDependencies = () => {
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
content: <></>, content: <></>,
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}dependencies`, { data: selectedRowIds }) .delete(`${config.apiPrefix}dependencies`, { data: selectedRowIds })
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
message.success('批量删除成功');
setSelectedRowIds([]); setSelectedRowIds([]);
getDependencies(); getDependencies();
} else { } else {
@ -312,25 +315,41 @@ const Dependence = ({ headerStyle, isPhone, ws }: any) => {
}, [logDependence]); }, [logDependence]);
useEffect(() => { useEffect(() => {
ws.onmessage = (e: any) => { if (!socketMessage) return;
const { type, message, references } = JSON.parse(e.data); const { type, message, references } = socketMessage;
if ( if (
type === 'installDependence' && type === 'installDependence' &&
message === '依赖安装结束' && message.includes('结束时间') &&
references.length > 0 references.length > 0
) { ) {
const result = [...value]; let status;
for (let i = 0; i < references.length; i++) { if (message.includes('安装')) {
const index = value.findIndex((x) => x._id === references[i]); status = message.includes('成功') ? Status.已安装 : Status.安装失败;
result.splice(index, 1, { } else {
...result[index], status = message.includes('成功') ? Status.已删除 : Status.删除失败;
status: Status.已安装,
});
}
setValue(result);
} }
}; const result = [...value];
}, [value]); for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x._id === references[i]);
result.splice(index, 1, {
...result[index],
status,
});
}
setValue(result);
if (status === Status.) {
setTimeout(() => {
const _result = [...value];
for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x._id === references[i]);
_result.splice(index, 1);
}
setValue(_result);
}, 5000);
}
}
}, [socketMessage]);
const panelContent = () => ( const panelContent = () => (
<> <>
@ -394,13 +413,13 @@ const Dependence = ({ headerStyle, isPhone, ws }: any) => {
tabPosition="top" tabPosition="top"
onChange={onTabChange} onChange={onTabChange}
> >
<Tabs.TabPane tab="nodejs" key="nodejs"> <Tabs.TabPane tab="NodeJs" key="nodejs">
{panelContent()} {panelContent()}
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane tab="python3" key="python3"> <Tabs.TabPane tab="Python3" key="python3">
{panelContent()} {panelContent()}
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane tab="linux" key="linux"> <Tabs.TabPane tab="Linux" key="linux">
{panelContent()} {panelContent()}
</Tabs.TabPane> </Tabs.TabPane>
</Tabs> </Tabs>
@ -412,11 +431,18 @@ const Dependence = ({ headerStyle, isPhone, ws }: any) => {
/> />
<DependenceLogModal <DependenceLogModal
visible={isLogModalVisible} visible={isLogModalVisible}
handleCancel={() => { handleCancel={(needRemove?: boolean) => {
setIsLogModalVisible(false); setIsLogModalVisible(false);
getDependenceDetail(logDependence); if (needRemove) {
const index = value.findIndex((x) => x._id === logDependence._id);
const result = [...value];
result.splice(index, 1);
setValue(result);
} else if ([...value].map((x) => x._id).includes(logDependence._id)) {
getDependenceDetail(logDependence);
}
}} }}
ws={ws} socketMessage={socketMessage}
dependence={logDependence} dependence={logDependence}
/> />
</PageContainer> </PageContainer>

View File

@ -12,21 +12,23 @@ const DependenceLogModal = ({
dependence, dependence,
handleCancel, handleCancel,
visible, visible,
ws, socketMessage,
}: { }: {
dependence?: any; dependence?: any;
visible: boolean; visible: boolean;
handleCancel: () => void; handleCancel: (needRemove?: boolean) => void;
ws: any; socketMessage: any;
}) => { }) => {
const [value, setValue] = useState<string>(''); const [value, setValue] = useState<string>('');
const [executing, setExecuting] = useState<any>(true); const [executing, setExecuting] = useState<any>(true);
const [isPhone, setIsPhone] = useState(false); const [isPhone, setIsPhone] = useState(false);
const [loading, setLoading] = useState<any>(true); const [loading, setLoading] = useState<boolean>(true);
const [isRemoveFailed, setIsRemoveFailed] = useState(false);
const [removeLoading, setRemoveLoading] = useState<boolean>(false);
const cancel = () => { const cancel = (needRemove: boolean = false) => {
localStorage.removeItem('logDependence'); localStorage.removeItem('logDependence');
handleCancel(); handleCancel(needRemove);
}; };
const titleElement = () => { const titleElement = () => {
@ -49,7 +51,8 @@ const DependenceLogModal = ({
if (localStorage.getItem('logDependence') === dependence._id) { if (localStorage.getItem('logDependence') === dependence._id) {
const log = (data.data.log || []).join('\n') as string; const log = (data.data.log || []).join('\n') as string;
setValue(log); setValue(log);
setExecuting(!log.includes('依赖安装结束')); setExecuting(!log.includes('结束时间'));
setIsRemoveFailed(log.includes('删除失败'));
} }
}) })
.finally(() => { .finally(() => {
@ -57,23 +60,48 @@ const DependenceLogModal = ({
}); });
}; };
const forceRemoveDependence = () => {
setRemoveLoading(true);
request
.delete(`${config.apiPrefix}dependencies/force`, {
data: [dependence._id],
})
.then((data: any) => {
cancel(true);
})
.finally(() => {
setRemoveLoading(false);
});
};
const footerClick = () => {
if (isRemoveFailed) {
forceRemoveDependence();
} else {
cancel();
}
};
useEffect(() => { useEffect(() => {
if (dependence) { if (dependence) {
getDependenceLog(); getDependenceLog();
ws.onmessage = (e: any) => {
const { type, message, references } = JSON.parse(e.data);
if (
type === 'installDependence' &&
message === '依赖安装结束' &&
references.length > 0
) {
setExecuting(false);
}
setValue(`${value} \n ${message}`);
};
} }
}, [dependence]); }, [dependence]);
useEffect(() => {
if (!socketMessage) return;
const { type, message, references } = socketMessage;
if (
type === 'installDependence' &&
message.includes('结束时间') &&
references.length > 0
) {
setExecuting(false);
setIsRemoveFailed(message.includes('删除失败'));
}
setValue(`${value} \n ${message}`);
}, [socketMessage]);
useEffect(() => { useEffect(() => {
setIsPhone(document.body.clientWidth < 768); setIsPhone(document.body.clientWidth < 768);
}, []); }, []);
@ -93,8 +121,8 @@ const DependenceLogModal = ({
onOk={() => cancel()} onOk={() => cancel()}
onCancel={() => cancel()} onCancel={() => cancel()}
footer={[ footer={[
<Button type="primary" onClick={() => cancel()}> <Button type="primary" onClick={footerClick} loading={removeLoading}>
{isRemoveFailed ? '强制删除' : '知道了'}
</Button>, </Button>,
]} ]}
> >

View File

@ -51,9 +51,7 @@ const DependenceModal = ({
data: payload, data: payload,
}, },
); );
if (code === 200) { if (code !== 200) {
message.success(dependence ? '更新依赖成功' : '添加依赖成功');
} else {
message.error(data); message.error(data);
} }
setLoading(false); setLoading(false);

View File

@ -6,8 +6,9 @@ import { version } from '../../version';
const { Countdown } = Statistic; const { Countdown } = Statistic;
const CheckUpdate = ({ ws }: any) => { const CheckUpdate = ({ socketMessage }: any) => {
const [updateLoading, setUpdateLoading] = useState(false); const [updateLoading, setUpdateLoading] = useState(false);
const [value, setValue] = useState('');
const modalRef = useRef<any>(); const modalRef = useRef<any>();
const checkUpgrade = () => { const checkUpgrade = () => {
@ -95,7 +96,7 @@ const CheckUpdate = ({ ws }: any) => {
fontWeight: 400, fontWeight: 400,
}} }}
> >
... {value}
</pre> </pre>
</div> </div>
), ),
@ -103,55 +104,60 @@ const CheckUpdate = ({ ws }: any) => {
}; };
useEffect(() => { useEffect(() => {
let _message = ''; if (!modalRef.current || !socketMessage) {
ws.onmessage = (e: any) => { return;
if (!modalRef.current) { }
return; const { type, message, references } = socketMessage;
}
_message = `${_message}\n${e.data}`;
modalRef.current.update({
content: (
<div style={{ height: '60vh', overflowY: 'auto' }}>
<pre
style={{
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
fontSize: 12,
fontWeight: 400,
}}
>
{_message}
</pre>
<div id="log-identifier" style={{ paddingBottom: 5 }}></div>
</div>
),
});
document.getElementById('log-identifier') &&
document
.getElementById('log-identifier')!
.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
if (e.data.includes('重启面板')) { if (type !== 'updateSystemVersion') {
message.warning({ return;
content: ( }
<span>
const newMessage = `${value} \n ${message}`;
<Countdown modalRef.current.update({
className="inline-countdown" content: (
format="ss" <div style={{ height: '60vh', overflowY: 'auto' }}>
value={Date.now() + 1000 * 10} <pre
/> style={{
wordBreak: 'break-all',
</span> whiteSpace: 'pre-wrap',
), fontSize: 12,
duration: 10, fontWeight: 400,
}); }}
setTimeout(() => { >
window.location.reload(); {newMessage}
}, 10000); </pre>
} <div id="log-identifier" style={{ paddingBottom: 5 }}></div>
}; </div>
}, []); ),
});
setValue(newMessage);
document.getElementById('log-identifier') &&
document
.getElementById('log-identifier')!
.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
if (newMessage.includes('重启面板')) {
message.warning({
content: (
<span>
<Countdown
className="inline-countdown"
format="ss"
value={Date.now() + 1000 * 10}
/>
</span>
),
duration: 10,
});
setTimeout(() => {
window.location.reload();
}, 10000);
}
}, [socketMessage]);
return ( return (
<> <>

View File

@ -47,7 +47,7 @@ const Setting = ({
user, user,
reloadUser, reloadUser,
reloadTheme, reloadTheme,
ws, socketMessage,
}: any) => { }: any) => {
const columns = [ const columns = [
{ {
@ -377,7 +377,7 @@ const Setting = ({
/> />
</Form.Item> </Form.Item>
<Form.Item label="检查更新" name="update"> <Form.Item label="检查更新" name="update">
<CheckUpdate ws={ws} /> <CheckUpdate socketMessage={socketMessage} />
</Form.Item> </Form.Item>
</Form> </Form>
</Tabs.TabPane> </Tabs.TabPane>