Browse Source

(*)网关状态指标; 网关操作等; 解决冲突

master
wuqun 2 years ago
parent
commit
4921b85d0d
  1. BIN
      code/web/client/assets/images/background/logo.png
  2. BIN
      code/web/client/assets/videos/administer_banner.mp4
  3. BIN
      code/web/client/assets/videos/gateway_banner.mp4
  4. 2
      code/web/client/src/index.jsx
  5. 150
      code/web/client/src/layout/components/header/index.jsx
  6. 2
      code/web/client/src/layout/components/sider/index.jsx
  7. 154
      code/web/client/src/sections/auth/containers/login.jsx
  8. 40
      code/web/client/src/sections/edition/actions/index.js
  9. 284
      code/web/client/src/sections/edition/containers/administer.jsx
  10. 59
      code/web/client/src/sections/edition/containers/gateway.jsx
  11. 5
      code/web/client/src/utils/webapi.js

BIN
code/web/client/assets/images/background/logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
code/web/client/assets/videos/administer_banner.mp4

Binary file not shown.

BIN
code/web/client/assets/videos/gateway_banner.mp4

Binary file not shown.

2
code/web/client/src/index.jsx

@ -9,4 +9,4 @@ import microApp from '@micro-zoe/micro-app'
microApp.start()
render((<App projectName="飞尚物联" />), document.getElementById('IotAuthApp'));
render((<App projectName="边缘网关" />), document.getElementById('IotAuthApp'));

150
code/web/client/src/layout/components/header/index.jsx

@ -4,84 +4,92 @@ import { connect } from "react-redux";
import { Nav, Avatar, Dropdown } from "@douyinfe/semi-ui";
const Header = (props) => {
const { dispatch, history, user, actions, } = props;
const { dispatch, history, user, actions, } = props;
return (
<>
<Nav
mode={"horizontal"}
onClick={({ itemKey }) => {
if (itemKey == "logout") {
dispatch(actions.auth.logout());
// if (socket) {
// socket.disconnect();
// }
history.push(`/signin`);
}
}}
style={{
height: 60,
minWidth: 520,
background: `url(${__webpack_public_path__}assets/images/background/header.png)`,
backgroundSize: "100% 100%",
color: "white",
}}
header={{
logo: (
return (
<>
<Nav
mode={"horizontal"}
onClick={({ itemKey }) => {
if (itemKey == "logout") {
dispatch(actions.auth.logout());
// if (socket) {
// socket.disconnect();
// }
history.push(`/signin`)
localStorage.setItem('fs_iot_auth_selected_sider', JSON.stringify(['gateway']))
localStorage.setItem('fs_iot_auth_open_sider', JSON.stringify(['edgeGateway']))
}
}}
style={{
height: 60,
minWidth: 520,
background: `url(${__webpack_public_path__}assets/images/background/header.png)`,
backgroundSize: "100% 100%",
color: "white",
}}
header={{
logo: (
<img
src={`/assets/images/background/logo.png`}
style={{ display: "inline-block", width: 280, height: 52 }}
/>
),
text: "",
}}
footer={
<Nav.Sub
itemKey={"user"}
dropdownStyle={{
position: 'relative'
}}
text={
<div
style={{
marginLeft: 20,
display: "inline-block",
color: "white",
}}
>
<img
src={`/assets/images/background/logo.png`}
style={{ display: "inline-block", width: 280, height: 52 }}
src={`/assets/images/background/notice.png`}
style={{
display: "inline-block",
width: 18,
height: 18,
position: "relative",
top: 6,
left: -10,
}}
/>
),
text: "",
}}
footer={
<Nav.Sub
itemKey={"user"}
dropdownStyle={{
position: 'relative'
}}
text={
<div
style={{
marginLeft: 20,
display: "inline-block",
color: "white",
}}
>
<img
src={`/assets/images/background/notice.png`}
style={{
display: "inline-block",
width: 18,
height: 18,
position: "relative",
top: 6,
left: -10,
}}
/>
<Avatar size="small" color="light-blue" style={{ margin: 4 }}>
<img src="/assets/images/avatar/6.png" />
</Avatar>
</div>
}
>
<Nav.Item itemKey={"logout"} text={"退出"} />
</Nav.Sub>
}
/>
</>
);
<Avatar size="small" color="light-blue" style={{ margin: 4 }}>
<img src="/assets/images/avatar/6.png" />
</Avatar>
<div style={{
display: "inline-block", position: "relative",
top: 4,
left: 4,
}}>
{user && user.userName}
</div>
</div>
}
>
<Nav.Item itemKey={"logout"} text={"退出"} />
</Nav.Sub>
}
/>
</>
);
};
function mapStateToProps (state) {
const { global, auth, } = state;
return {
actions: global.actions,
user: auth.user,
const { global, auth, } = state;
return {
actions: global.actions,
user: auth.user,
// socket: webSocket.socket,
};
};
}
export default connect(mapStateToProps)(Header);

2
code/web/client/src/layout/components/sider/index.jsx

@ -14,7 +14,6 @@ const Sider = props => {
useEffect(() => {
const { sections, dispatch, user } = props;
let nextItems = []
console.log(sections);
for (let c of sections) {
if (typeof c.getNavItem == 'function') {
let item = c.getNavItem(user, dispatch);
@ -23,7 +22,6 @@ const Sider = props => {
}
}
}
console.log(nextItems);
setItems(nextItems)
const lastSelectedKeys = localStorage.getItem('fs_iot_auth_selected_sider')

154
code/web/client/src/sections/auth/containers/login.jsx

@ -8,90 +8,90 @@ import { IconLock, IconUser } from '@douyinfe/semi-icons';
import '../style.less'
const Login = props => {
const { dispatch, user, error, actions, apiRoot, isRequesting } = props
const form = useRef();
const { dispatch, user, error, actions, apiRoot, isRequesting } = props
const form = useRef();
useEffect(() => {
if (error) {
Toast.error(error);
form.current.setValue('password', '')
}
}, [error])
useEffect(() => {
if (error) {
Toast.error(error);
form.current.setValue('password', '')
}
}, [error])
useEffect(() => {
if (user && user.authorized) {
dispatch(push('/edgeGateway/gateway'));
// localStorage.setItem('fs_iot_auth_selected_sider', JSON.stringify([]))
// localStorage.setItem('fs_iot_auth_open_sider', JSON.stringify([]))
}
}, [user])
useEffect(() => {
if (user && user.authorized) {
dispatch(push('/edgeGateway/gateway'));
localStorage.setItem('fs_iot_auth_selected_sider', JSON.stringify(['gateway']))
localStorage.setItem('fs_iot_auth_open_sider', JSON.stringify(['edgeGateway']))
}
}, [user])
return (
<div style={{
height: '100vh',
backgroundImage: "url('/assets/images/background/loginBackground.png')",
backgroundSize: 'cover',
return (
<div style={{
height: '100vh',
backgroundImage: "url('/assets/images/background/loginBackground.png')",
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
position: 'relative',
}}>
<div style={{
width: 446,
height: 348,
padding: '45px 60px',
backgroundImage: "url('/assets/images/background/loginbg.png')",
backgroundSize: '100% 100%',
backgroundRepeat: 'no-repeat',
position: 'relative',
}}>
<div style={{
width: 446,
height: 348,
padding: '45px 60px',
backgroundImage: "url('/assets/images/background/loginbg.png')",
backgroundSize: '100% 100%',
backgroundRepeat: 'no-repeat',
position: 'absolute',
top: '33.89%',
right: '16.43%',
}}>
<div style={{ width: 113, height: 24, marginTop: 3, marginLeft: 5 }}>
<img src="/assets/images/background/user_login.png" alt="" style={{ width: '100%', height: '100%' }} />
</div>
<Form
onSubmit={values => {
dispatch(login(values.username, values.password)).then(res => {
const data = res.payload.user
// dispatch(actions.layout.initWebSocket({ ioUrl: apiRoot, token: data.token }))
})
}}
getFormApi={formApi => form.current = formApi}
>
<Form.Input
className='inputbgc'
field='username'
noLabel={true}
label='用户名'
placeholder='请输入账号'
prefix={<IconUser style={{ color: '#1859C1', marginRight: 14, marginLeft: 8 }} />}
style={{ background: 'rgba(24, 89, 193, 0.08)', height: 40, marginTop: 26 }}
/>
<Form.Input
field='password'
noLabel={true}
mode="password"
autoComplete=""
placeholder='请输入密码'
label='密码'
prefix={<IconLock style={{ color: '#1859C1', marginRight: 14, marginLeft: 8 }} />}
style={{ background: 'rgba(24, 89, 193, 0.08)', height: 40 }}
/>
<Button htmlType='submit' block theme="solid" style={{ marginTop: 17, height: 40, backgroundColor: '#1859C1' }}>立即登录</Button>
</Form>
position: 'absolute',
top: '33.89%',
right: '16.43%',
}}>
<div style={{ width: 113, height: 24, marginTop: 3, marginLeft: 5 }}>
<img src="/assets/images/background/user_login.png" alt="" style={{ width: '100%', height: '100%' }} />
</div>
</div>
);
<Form
onSubmit={values => {
dispatch(login(values.username, values.password)).then(res => {
const data = res.payload.user
// dispatch(actions.layout.initWebSocket({ ioUrl: apiRoot, token: data.token }))
})
}}
getFormApi={formApi => form.current = formApi}
>
<Form.Input
className='inputbgc'
field='username'
noLabel={true}
label='用户名'
placeholder='请输入账号'
prefix={<IconUser style={{ color: '#1859C1', marginRight: 14, marginLeft: 8 }} />}
style={{ background: 'rgba(24, 89, 193, 0.08)', height: 40, marginTop: 26 }}
/>
<Form.Input
field='password'
noLabel={true}
mode="password"
autoComplete=""
placeholder='请输入密码'
label='密码'
prefix={<IconLock style={{ color: '#1859C1', marginRight: 14, marginLeft: 8 }} />}
style={{ background: 'rgba(24, 89, 193, 0.08)', height: 40 }}
/>
<Button htmlType='submit' block theme="solid" style={{ marginTop: 17, height: 40, backgroundColor: '#1859C1' }}>立即登录</Button>
</Form>
</div>
</div>
);
}
function mapStateToProps(state) {
const { auth, global } = state;
return {
user: auth.user,
error: auth.error,
actions: global.actions,
apiRoot: global.apiRoot,
isRequesting: auth.isRequesting
}
function mapStateToProps (state) {
const { auth, global } = state;
return {
user: auth.user,
error: auth.error,
actions: global.actions,
apiRoot: global.apiRoot,
isRequesting: auth.isRequesting
}
}
export default connect(mapStateToProps)(Login);

40
code/web/client/src/sections/edition/actions/index.js

@ -36,6 +36,41 @@ export function ableGateway(id, data) {
});
}
export function getVersions() {
return (dispatch) =>
basicAction({
type: "get",
dispatch: dispatch,
actionType: "GET_VERSIONS",
url: `${ApiTable.getVersions}`,
msg: { option: "查询网关版本信息" },
reducer: { name: "", params: { noClear: true } },
});
}
export function deleteVersion(id) {
return dispatch => basicAction({
type: 'delete',
dispatch: dispatch,
actionType: 'DELETE_VERSION',
url: ApiTable.deleteVersion.replace('{versionId}', id),
msg: { option: '删除网关版本' },
reducer: { name: '' }
});
}
export function postVersion(data) {
return (dispatch) =>
basicAction({
type: "post",
dispatch: dispatch,
data,
actionType: "POST_VERSION",
url: `${ApiTable.postVersion}`,
msg: { option: "新增网关版本" },
});
}
export function gatewaySsh(id) {
return dispatch => basicAction({
type: 'post',
@ -76,5 +111,8 @@ export default {
ableGateway,
gatewaySsh,
rebootGateway,
restartGateway
restartGateway,
getVersions,
deleteVersion,
postVersion,
};

284
code/web/client/src/sections/edition/containers/administer.jsx

@ -2,28 +2,294 @@
import React, { useEffect, useRef } from 'react';
import { connect } from 'react-redux';
import { push } from 'react-router-redux';
import { Form, Button, Toast } from '@douyinfe/semi-ui';
import { Form, Button, Table, Pagination, Modal } from '@douyinfe/semi-ui';
import { IconLock, IconUser } from '@douyinfe/semi-icons';
import { useState } from 'react';
import moment from "moment";
const EditionManage = props => {
const { dispatch, user, error, actions, apiRoot, isRequesting } = props
const { edition } = actions
const [versionList, setVersionList] = useState([])
const [addVersion, setaddVersion] = useState(false)
const api = useRef()
useEffect(() => {
requestData()
}, [])
const requestData = () => {
dispatch(edition.getVersions()).then(res => {
console.log(res);
if (res.success) {
setVersionList(res.payload.data)
}
})
}
}, [])
const columns = [
{
title: "序列号",
render: (_, record, index) => {
return index + 1;
},
}, {
title: "主版本号",
dataIndex: "major",
key: "major",
}, {
title: "次版本号",
dataIndex: "minor",
key: "minor",
}, {
title: "补丁版本号",
dataIndex: "patch",
key: "patch",
}, {
title: "构建号",
dataIndex: "build",
key: "build",
}, {
title: "类型",
dataIndex: "type",
key: "type",
}, {
title: "描述",
dataIndex: "desc",
key: "desc",
}, {
title: "基础镜像地址",
dataIndex: "imageBase",
key: "imageBase",
}, {
title: "镜像版本",
dataIndex: "imageVersion",
key: "imageVersion",
render: (_, r, index) => {
return <div style={{ width: 80 }}>{r.imageVersion}</div>
},
}, {
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
render: (_, r, index) => {
return <div style={{ width: 130 }}>{r.createdAt}</div>
},
}, {
title: "操作",
dataIndex: "operation",
render: (_, r, index) => {
return <Button onClick={() => {
dispatch(edition.deleteVersion(r.id)).then(res => {
if (res.success) {
requestData()
}
})
}}>删除</Button>
},
},
]
return (
<div style={{}}>
版本管理
</div>
<>
<div style={{ position: "" }}>
<video
id="cameraBanner"
autoPlay
loop
muted
style={{ width: "100%", objectFit: "cover", height: 171 }}
src="/assets/videos/administer_banner.mp4"
type="video/mp4"
/>
<div style={{ position: "absolute", top: 12 }}>
<div
style={{
fontSize: 22,
paddingTop: 15,
marginLeft: 21,
}}
>
版本管理
</div>
<div
style={{
fontSize: 14,
paddingTop: 18,
marginLeft: 20,
}}
>
对网关版本添加删除的管理页面
</div>
<div
style={{
fontSize: 14,
marginTop: 28,
marginLeft: 21,
width: 89,
height: 32,
lineHeight: 32 + "px",
textAlign: "center",
backgroundColor: "#D9EAFF",
color: "#1859C1",
cursor: "pointer",
}}
onClick={() => {
setaddVersion(true)
}}
>
添加版本
</div>
</div>
</div>
<div style={{
width: "100%",
background: "#FFFFFF",
borderRadius: 3,
padding: "8px 20px",
marginTop: 20,
}}>
<Table
columns={columns}
dataSource={versionList}
bordered={false}
empty="暂无数据"
style={{
padding: "0px 20px",
}}
pagination={false}
/>
{versionList.length > 0 ? <div
style={{
display: "flex",
justifyContent: "flex-end",
padding: "20px 20px",
}}
>
<span style={{ lineHeight: "30px" }}>
{versionList.length}个设备
</span>
<Pagination
total={versionList.length}
showSizeChanger
currentPage={1}
pageSizeOpts={[10, 20, 30, 40]}
onChange={(currentPage, pageSize) => {
// setQuery({ limit: pageSize, page: currentPage - 1 });
// page.current = currentPage - 1
}}
/>
</div> : ""}
</div>
<Modal
title='新增网关版本'
// okText={okText}
visible={addVersion}
onOk={() => {
api.current.validate().then(r => {
console.log(r);
for (let key in r) {
if (['major', 'minor', 'patch', 'build'].includes(key)) {
r[key] = Number(r[key])
}
}
dispatch(edition.postVersion({ ...r, createdAt: moment().format("YYYY-MM-DD HH:MM:SS") })).then(res => {
if (res.success) {
requestData()
setaddVersion(false)
}
})
})
}}
width={610}
onCancel={() => setaddVersion(false)}
>
<Form
getFormApi={(formApi) => (api.current = formApi)}
layout="horizontal"
labelAlign="right"
labelWidth="114px"
style={{ display: 'flex', flexDirection: 'column' }}
>
<Form.Input
field='major'
label='主版本号:'
labelPosition="left"
hideButtons={true}
placeholder='请输入主版本号'
style={{ width: 200, marginBottom: 10 }}
rules={[{ required: true, message: "请输入主版本号" }, { pattern: "^[0-9]+$", message: "只能输入数字" },]}
/>
<Form.Input
field='minor'
label='次版本号:'
labelPosition="left"
hideButtons={true}
placeholder='请输入次版本号'
style={{ width: 200, marginBottom: 10 }}
rules={[{ required: true, message: "请输入次版本号" }, { pattern: "^[0-9]+$", message: "只能输入数字" },]}
/>
<Form.Input
field='patch'
label='补丁版本号:'
labelPosition="left"
hideButtons={true}
placeholder='请输入补丁版本号'
style={{ width: 200, marginBottom: 10 }}
rules={[{ required: true, message: "请输入补丁版本号" }, { pattern: "^[0-9]+$", message: "只能输入数字" },]}
/>
<Form.Input
field='build' label='构建号:'
labelPosition="left"
hideButtons={true}
placeholder='请输入构建号'
style={{ width: 200, marginBottom: 10 }}
rules={[{ required: true, message: "请输入构建号" }, { pattern: "^[0-9]+$", message: "只能输入数字" },]}
/>
<Form.Select
field="type"
label='Type:'
labelPosition="left"
placeholder='请选择类型'
style={{ width: 200, marginBottom: 10 }}
rules={[{ required: true, message: "请选择类型" }]}
>
<Form.Select.Option value="beta">beta</Form.Select.Option>
<Form.Select.Option value="alpha">alpha</Form.Select.Option>
<Form.Select.Option value="release">release</Form.Select.Option>
<Form.Select.Option value="LTS">LTS</Form.Select.Option>
</Form.Select>
<Form.Input
field='imageBase'
label='基础镜像地址:'
labelPosition="left"
placeholder='请输入基础镜像地址'
style={{ width: 440, marginBottom: 10 }}
rules={[{ required: true, message: "请输入基础镜像地址" }]}
/>
<Form.Input
field='imageVersion'
label='镜像版本:'
labelPosition="left"
placeholder='请输入镜像版本'
style={{ width: 200, marginBottom: 10 }}
rules={[{ required: true, message: "请输入镜像版本" }]}
/>
<Form.TextArea
field='desc'
label='描述:'
labelPosition="left"
placeholder='请输入描述'
style={{ width: 440 }}
/>
</Form>
</Modal>
</>
);
}
function mapStateToProps(state) {
function mapStateToProps (state) {
const { auth, global } = state;
return {
user: auth.user,

59
code/web/client/src/sections/edition/containers/gateway.jsx

@ -158,7 +158,62 @@ const GatewayManage = props => {
}
return (
<div style={{}}>
<>
<div>
<video
id="gatewayBanner"
autoPlay
loop
muted
style={{ width: "100%", objectFit: "cover", height: 171 }}
src="/assets/videos/gateway_banner.mp4"
type="video/mp4"
/>
<div style={{ position: "absolute", top: 12 }}>
<div
style={{
fontSize: 22,
paddingTop: 15,
marginLeft: 21,
}}
>
设备管理
</div>
<div
style={{
fontSize: 14,
paddingTop: 18,
marginLeft: 20,
}}
>
对NVR网络硬盘录像机设备节点的管理
</div>
<div
style={{
fontSize: 14,
marginTop: 28,
marginLeft: 21,
width: 89,
height: 32,
lineHeight: "32px",
textAlign: "center",
backgroundColor: "#D9EAFF",
color: "#1859C1",
cursor: "pointer",
}}
onClick={() => {
}}
>
新增设备
</div>
</div>
</div>
<div style={{ width: "100%",
background: "#FFFFFF",
borderRadius: 3,
padding: "8px 20px",
marginTop: 20,}}>
<Skeleton
loading={false}
active={true}
@ -198,6 +253,8 @@ const GatewayManage = props => {
dataToModal={dataToModal} /> : ''
}
</div>
</>
);
}

5
code/web/client/src/utils/webapi.js

@ -8,10 +8,13 @@ export const ApiTable = {
getGateways: 'v1/edges',
ableGateway: 'v1/edge/{id}/enable',
getGatewayStatus: 'v1/edge/{id}/metrics',
gatewaySsh: 'v1/edge/{id}/ssh',
rebootGateway: 'v1/edge/{id}/reboot',
restartGateway: 'v1/edge/{id}/restart',
getVersions: 'v1/versions', //查询网关版本信息
deleteVersion: 'v1/version/{versionId}', //删除网关版本
postVersion: 'v1/version', //新增网关版本
};
export const RouteTable = {

Loading…
Cancel
Save