diff --git a/admin/package.json b/admin/package.json index ffa21080e6680d402c18e2dc61f2bcfdb2cebe6f..969f763eafc7d541a57581548fc53f54bace3ee3 100644 --- a/admin/package.json +++ b/admin/package.json @@ -13,6 +13,8 @@ "antd": "^4.20.4", "axios": "^0.27.2", "bulma": "^0.9.4", + "echarts": "^5.3.3", + "echarts-for-react": "^3.0.2", "lodash": "^4.17.21", "react": "^18.1.0", "react-dom": "^18.1.0", diff --git a/admin/src/component/App/Container/ContainerFileManagement/ContainerCategory/index.tsx b/admin/src/component/App/Container/ContainerFileManagement/ContainerCategory/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bea54d3562764f6dead68ee7d8dd3ade1d9b6b39 --- /dev/null +++ b/admin/src/component/App/Container/ContainerFileManagement/ContainerCategory/index.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import WithRouter from "../../../../../router/WithRouter"; + + +interface Props { } + +interface State { } + +class ContainerFile extends React.Component{ + + constructor(props: Props) { + super(props); + this.state = {} + } + + render(){ + return ( +
+
+ 文件夹名称 +
+ ) + } + + +} + +export default WithRouter(ContainerFile) \ No newline at end of file diff --git a/admin/src/component/App/Container/ContainerFileManagement/ContainerFile/index.tsx b/admin/src/component/App/Container/ContainerFileManagement/ContainerFile/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0135370119d8b429d39f2f1b01eb71fc089fae31 --- /dev/null +++ b/admin/src/component/App/Container/ContainerFileManagement/ContainerFile/index.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import WithRouter from "../../../../../router/WithRouter"; + + +interface Props { } + +interface State { } + +class ContainerFile extends React.Component{ + + constructor(props: Props) { + super(props); + this.state = {} + } + + render(){ + return ( +
+
+ 位置文件 +
+ ) + } + + +} + +export default WithRouter(ContainerFile) \ No newline at end of file diff --git a/admin/src/component/App/Container/ContainerFileManagement/index.css b/admin/src/component/App/Container/ContainerFileManagement/index.css new file mode 100644 index 0000000000000000000000000000000000000000..225327061d8f68b6e1ff7aac5bd3332327705a40 --- /dev/null +++ b/admin/src/component/App/Container/ContainerFileManagement/index.css @@ -0,0 +1,7 @@ +.ant-list-item-meta{ + align-items: center !important; +} + +#fmContent{ + margin-top: 10px; +} \ No newline at end of file diff --git a/admin/src/component/App/Container/ContainerFileManagement/index.tsx b/admin/src/component/App/Container/ContainerFileManagement/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f7d66f8a9c19a7b12bbb275f4a60b071b771ccac --- /dev/null +++ b/admin/src/component/App/Container/ContainerFileManagement/index.tsx @@ -0,0 +1,129 @@ +import { Button, Descriptions, Input, List, PageHeader, Space } from "antd"; +import { UploadOutlined, FolderAddOutlined, RollbackOutlined } from '@ant-design/icons' +import React from "react"; +import WithRouter from "../../../../router/WithRouter"; +import './index.css' +import IconFont from "../../../Base/IconFont"; + +interface Props { } + +interface State { + fsList: Array +} + +interface File { + name: string + type?: string +} + +class ContainerFileManagement extends React.Component{ + + constructor(prop: Props) { + super(prop); + this.state = { + fsList: [{ name: "文件1", type: 'category' }, { name: "文件2", type: 'file' }, { name: "文件3", type: 'picture' }, { name: "文件4" }] + } + } + + componentDidMount() { + this.initSocket() + + } + + initSocket = () => { } + + + fsToItem = (fs: File) => { + let { type, name } = fs + if (type === 'category') { + return ( + 删除]}> + } + title={name} + description="Ant Design, a design language for background applications, is refined by Ant UED Team" + /> + + ) + } + + + if (type === 'file') { + return ( + 下载, 删除]}> + } + title={name} + description="Ant Design, a design language for background applications, is refined by Ant UED Team" + /> + ) + } + + if (type === 'picture') { + return ( + 下载, 删除]}> + } + title={name} + description="Ant Design, a design language for background applications, is refined by Ant UED Team" + /> + ) + } + + + return ( + 下载, 删除]}> + } + title={name} + description="Ant Design, a design language for background applications, is refined by Ant UED Team" + /> + + ) + } + + render() { + + + + + return ( +
+
+ + window.history.back()} + extra={[ + , + , + + ]} + > + {/* + Lili Qu + 12 + 20 + 1 + 1 + 0 + */} + +
+ +
+ +
+
+ ) + } +} + +export default WithRouter(ContainerFileManagement); \ No newline at end of file diff --git a/admin/src/component/App/Container/ContainerStat/data.ts b/admin/src/component/App/Container/ContainerStat/data.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f4a2f55726fe856d9673c4bcb6a7040909a3cc5 --- /dev/null +++ b/admin/src/component/App/Container/ContainerStat/data.ts @@ -0,0 +1,268 @@ +const memoryStst = { + title: { + text: '内存使用率', + textALign: 'center' + }, + toolbox: { + feature: { + saveAsImage: {} + } + }, + tooltip: { + formatter: '{a}
{b} : {c}%' + }, + series: [ + { + name: 'Pressure', + type: 'gauge', + max: 1, + progress: { + show: true, + width: 18 + }, + detail: { + formatter: '{value}%' + }, + data: [ + { + value: 0 + } + ] + } + + ] +} + +const cpuCoreStat = +{ + title: { + text: 'CPU 可用核心数', + textALign: 'center' + }, + toolbox: { + feature: { + saveAsImage: {} + } + }, + tooltip: { + formatter: '{a}
{b} : {c}%' + }, + series: [ + { + name: 'Pressure', + type: 'gauge', + max: 20, + detail: { + formatter: '{value}' + }, + progress: { + show: true, + width: 18 + }, + data: [ + { + value: 0, + name: '核心数' + } + ] + } + + ] +} + +const cpuUsageLineStat = { + title: { + text: 'CPU使用曲线图', + textALign: 'center' + }, + toolbox: { + feature: { + saveAsImage: {} + } + }, + xAxis: { + type: 'category', + data: [] + }, + yAxis: { + type: 'value' + }, + series: [ + { + + data: [], + type: 'line', + smooth: true + } + ] +} + + + +const memoryUsageRateStat = { + + title: { + text: '内存使用率', + textALign: 'center' + }, + toolbox: { + feature: { + saveAsImage: {} + } + }, + xAxis: { + type: 'category', + data: [] + }, + yAxis: { + type: 'value' + }, + series: [ + { + + data: [], + type: 'line', + smooth: true, + areaStyle: {} + } + ] +} + +const memoryUsageCountStat = { + + title: { + text: '内存使用量', + textALign: 'center' + }, + legend: { + data: ['内存使用量(Kb)'] + }, + toolbox: { + feature: { + saveAsImage: {} + } + }, + tooltip: { + formatter: '{a}
{b} : {c}KB' + }, + xAxis: { + type: 'category', + data: [] + }, + yAxis: { + type: 'value' + }, + series: [ + { + color:'lightgreen', + data: [], + type: 'line', + smooth: true + } + ] +} + +const netLineStat = { + title: { + text: '网络使用曲线图', + textALign: 'center' + }, + color: ['#5470C6', '#EE6666'], + tooltip: { + trigger: 'none', + axisPointer: { + type: 'cross' + } + }, + legend: {}, + grid: { + top: 70, + bottom: 50 + }, + xAxis: [ + { + type: 'category', + axisTick: { + alignWithLabel: true + }, + axisLine: { + onZero: false, + lineStyle: { + color: '#EE6666' + } + }, + axisPointer: { + label: { + formatter: function (params: any) { + return ( + 'Precipitation ' + + params.value + + (params.seriesData.length ? ':' + params.seriesData[0].data : '') + ); + } + } + }, + // prettier-ignore + data: ['2016-1', '2016-2', '2016-3', '2016-4', '2016-5', '2016-6', '2016-7', '2016-8', '2016-9', '2016-10', '2016-11', '2016-12'] + }, + { + type: 'category', + axisTick: { + alignWithLabel: true + }, + axisLine: { + onZero: false, + lineStyle: { + color: '#5470C6' + } + }, + axisPointer: { + label: { + formatter: function (params: any) { + return ( + 'Precipitation ' + + params.value + + (params.seriesData.length ? ':' + params.seriesData[0].data : '') + ); + } + } + }, + // prettier-ignore + data: ['2015-1', '2015-2', '2015-3', '2015-4', '2015-5', '2015-6', '2015-7', '2015-8', '2015-9', '2015-10', '2015-11', '2015-12'] + } + ], + yAxis: [ + { + type: 'value' + } + ], + series: [ + { + name: 'Precipitation(2015)', + type: 'line', + xAxisIndex: 1, + smooth: true, + emphasis: { + focus: 'series' + }, + data: [ + 2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3 + ] + }, + { + name: 'Precipitation(2016)', + type: 'line', + smooth: true, + emphasis: { + focus: 'series' + }, + data: [ + 3.9, 5.9, 11.1, 18.7, 48.3, 69.2, 231.6, 46.6, 55.4, 18.4, 10.3, 0.7 + ] + } + ] +} + +export { + memoryStst, cpuCoreStat, cpuUsageLineStat, memoryUsageRateStat, memoryUsageCountStat, netLineStat +} \ No newline at end of file diff --git a/admin/src/component/App/Container/ContainerStat/index.css b/admin/src/component/App/Container/ContainerStat/index.css new file mode 100644 index 0000000000000000000000000000000000000000..7fc338a94cb1c358cb0b5a2b969c33bb10f9b152 --- /dev/null +++ b/admin/src/component/App/Container/ContainerStat/index.css @@ -0,0 +1,3 @@ +.box:not(:last-child){ + margin: 0 !important; +} \ No newline at end of file diff --git a/admin/src/component/App/Container/ContainerStat/index.tsx b/admin/src/component/App/Container/ContainerStat/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..583724bba28194ef56175b988760ef1eefad5367 --- /dev/null +++ b/admin/src/component/App/Container/ContainerStat/index.tsx @@ -0,0 +1,290 @@ +import { Button, Col, Empty, PageHeader, Row } from "antd"; +import React from "react"; +import { RouterProps } from "react-router"; +import WithRouter from "../../../../router/WithRouter"; +import './index.css' +import _ from 'lodash' +import * as echarts from 'echarts'; +import { ContainerStatInfo } from "./model"; +import { formatterTime } from "../../../../utils/DateTime"; +import { memoryStst, cpuCoreStat, cpuUsageLineStat, memoryUsageCountStat, memoryUsageRateStat, netLineStat } from './data' +import { FifoCache } from "../../../../utils/FifoArray"; + + +interface Props { + containerId: string + router: RouterProps + param: RouterProps +} + +interface State { + pause: boolean +} + + + +class ContainerStat extends React.Component{ + + private containerId: string = "" + + private charts: Array = [] + + private ws: WebSocket | undefined = undefined + + + private cpuRateData: FifoCache; + private memoryRateData: FifoCache; + private memoryCountData: FifoCache; + + constructor(prop: Props) { + super(prop); + // @ts-ignore + this.containerId = prop.router.params.containerId; + this.memoryRateData = new FifoCache(40); + this.memoryCountData = new FifoCache(40); + this.cpuRateData = new FifoCache(40); + this.state = { + pause: false + } + } + + componentDidMount() { + + // @ts-ignore + let memory = echarts.init(document.getElementById('memoryUsageChart')) + + //@ts-ignore + let cpuCore = echarts.init(document.getElementById('cpuCoreChart')) + + //@ts-ignore + let cpuLineUsage = echarts.init(document.getElementById('cpuUsageChart')) + + //@ts-ignore + let memoryUsageRateChart = echarts.init(document.getElementById('memoryUsageRateChart')) + + //@ts-ignore + let memoryUsageCountChart = echarts.init(document.getElementById('memoryUsageCountChart')) + + //@ts-ignore + let netLineUsage = echarts.init(document.getElementById('networkLineUsageChart')) + + + + memory.setOption(memoryStst) + cpuCore.setOption(cpuCoreStat) + cpuLineUsage.setOption(cpuUsageLineStat) + memoryUsageRateChart.setOption(memoryUsageRateStat) + memoryUsageCountChart.setOption(memoryUsageCountStat) + netLineUsage.setOption(netLineStat) + + this.charts[0] = memory + this.charts[1] = cpuCore + this.charts[2] = cpuLineUsage + this.charts[3] = memoryUsageRateChart + this.charts[4] = memoryUsageCountChart + this.charts[5] = netLineUsage + + + + // 加载数据 + let WS_URL_PREFIX = process.env.REACT_APP_WS_URL + let clientId = localStorage.getItem('clientId') + let wsUrl = `${WS_URL_PREFIX}/api/ws/client/${clientId}/container/${this.containerId}/monitor` + // 创建 socket 连接 + + this.ws = new WebSocket(wsUrl); + this.ws.onmessage = this.onMessage;//WebSorkt通知 + this.ws.onerror = this.onError;//WebSorkt异常 + this.ws.onclose = this.onClose;//WebSorkt关闭 + + + } + + componentWillUnmount() { + if (this.ws) { + this.ws.close() + } + } + + switchStateData = () => { + this.setState({ + pause: !this.state.pause + }) + } + + + onMessage = (wsData: MessageEvent): any => { + if (this.state.pause) { + return + } + + let data = wsData.data + let stats: ContainerStatInfo = JSON.parse(data) + let key = formatterTime(new Date()) + + let cpuTotalUsage = _.get(stats, 'cpu_stats.cpu_usage.total_usage', 10) + let preCpuTotalUsage = _.get(stats, 'precpu_stats.cpu_usage.total_usage', 0) + let cpuDelta = cpuTotalUsage - preCpuTotalUsage + + let systemCpuTotalUsage = _.get(stats, 'cpu_stats.system_cpu_usage', 0) + let preSystemCpuTotalUsage = _.get(stats, 'precpu_stats.system_cpu_usage', 0) + let preSystemCpuDelta = systemCpuTotalUsage - preSystemCpuTotalUsage + + let numberCpu = _.get(stats, "cpu_stats.online_cpus", 1) + + let usageRate = 0; + if (preSystemCpuTotalUsage !== 0) { + usageRate = (cpuDelta / preSystemCpuDelta) * numberCpu * 100 + } + + // 内存使用率 + let memoryUsageRate: number = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; + let memoryUsageCount: number = (stats.memory_stats.usage / 1024); + this.charts[0].setOption({ + series: [ + { + data: [ + { + value: usageRate.toFixed(2), + name: '使用率' + } + ] + } + + ] + }) + + + // CPU 核心数 + this.charts[1].setOption({ + series: [ + { + data: [ + { + value: stats.cpu_stats.online_cpus, + name: '核心数' + } + ] + } + + ] + }) + + // CPU 区域限 + this.cpuRateData.set(key, usageRate + "") + this.charts[2].setOption({ + xAxis: { + data: this.cpuRateData.getKeys() + }, + series: [ + { + data: this.cpuRateData.getValues() + } + ] + }) + + + // 内存曲线记录 + this.memoryCountData.set(key, memoryUsageCount.toFixed(3)) + this.memoryRateData.set(key, memoryUsageRate.toFixed(3)) + this.charts[3].setOption({ + xAxis: { + data: this.memoryRateData.getKeys() + }, + series: [ + { + data: this.memoryRateData.getValues() + } + ] + }) + this.charts[4].setOption({ + xAxis: { + data: this.memoryCountData.getKeys() + }, + series: [ + { + data: this.memoryCountData.getValues() + } + ] + }) + } + + //ws异常监听 + onError = () => { + console.log("websocket发生异常,3秒后重连========"); + } + + onClose = () => { + console.log("websocket关闭========"); + } + + + back = () => { + // @ts-ignore + this.props.router.navigate(-1) + } + + render() { + return ( + 刷新, + , + ]} + > + +
+ + + +
+ + + + +
+ + + + +
+ + + +
+ + + + +
+ + + +
+ +
+ + + +
+ + + +
+ +
+
+
+ ) + } +} + +export default WithRouter(ContainerStat); \ No newline at end of file diff --git a/admin/src/component/App/Container/ContainerStat/model.ts b/admin/src/component/App/Container/ContainerStat/model.ts new file mode 100644 index 0000000000000000000000000000000000000000..af0fd024d01859bb88fa0f0da34480f8d67c78c6 --- /dev/null +++ b/admin/src/component/App/Container/ContainerStat/model.ts @@ -0,0 +1,31 @@ +export interface ContainerStatInfo { + id: string + name: string + read: string + perread: string + cpu_stats: CpuStats + memory_stats: MemoryStats +} + + + +export interface MemoryStats { + // 已使用内存 + usage: number + // 可用内存 + limit: number +} + + + +export interface CpuStats { + cpu_usage: CpuStats + // 系统CPU使用 + system_cpu_usage: number + // 可用核心数 + online_cpus: number +} + +export interface CpuUsage { + total_usage: number +} diff --git a/admin/src/page/ContainerLogPage/index.css b/admin/src/page/ContainerLogPage/index.css index e080be813c2fb8a1f03c7f488d3ff2ddd19944a4..25590c27b00cc91d45476df961ba81b4b77ff289 100644 --- a/admin/src/page/ContainerLogPage/index.css +++ b/admin/src/page/ContainerLogPage/index.css @@ -4,4 +4,8 @@ .xterm .xterm-screen canvas{ padding: 5px; +} + +#terminal div{ + height: 100%; } \ No newline at end of file diff --git a/admin/src/page/ContainerLogPage/index.tsx b/admin/src/page/ContainerLogPage/index.tsx index 71bb53c7008e710cf975f9121a4c6d610227d35b..54a75799ff035ab695c77f22165864daff45464c 100644 --- a/admin/src/page/ContainerLogPage/index.tsx +++ b/admin/src/page/ContainerLogPage/index.tsx @@ -4,6 +4,8 @@ import WithRouter from "../../router/WithRouter"; import {ITheme, Terminal} from "xterm"; import {AttachAddon} from "xterm-addon-attach"; import {FitAddon} from "xterm-addon-fit"; + +import './index.css' import 'xterm/css/xterm.css'; const style: ITheme = { @@ -102,7 +104,7 @@ class ContainerLogPage extends React.Component { render() { return ( -
+
, containers: Array } @@ -47,6 +49,7 @@ class ContainerPage extends React.Component { currentContainerId: '', detailDrawerStatus: false, moreDrawerStatus: false, + fmDrawerStatus: false, containers: [], filterStates: [] } @@ -105,7 +108,7 @@ class ContainerPage extends React.Component { width: 100, render: (State) => { let statusInfo = getStatusInfo(State); - return {statusInfo.stateDesc} + return {statusInfo.stateDesc} } }, { @@ -120,11 +123,11 @@ class ContainerPage extends React.Component { fixed: 'right', width: 240, render: (_, record) => { - let {operateCommon, operateDesc, operatorColor} = getStatusInfo(record.State); - return
+ let { operateCommon, operateDesc, operatorColor } = getStatusInfo(record.State); + return
+ style={{ color: operatorColor }}>{operateDesc} @@ -151,6 +154,15 @@ class ContainerPage extends React.Component { }) } + // 隐藏文件管理器 + hideFm = () => { + this.setState({ fmDrawerStatus: false }) + } + + showFm = () => { + this.setState({ fmDrawerStatus: true }) + } + // 显示更多 showMoreOperatorDrawer = (record: DockerContainer) => { this.setState({ @@ -189,70 +201,81 @@ class ContainerPage extends React.Component { message.error(`加载容器列表失败:${resp.msg}`).then(); return } - this.setState({containers: resp.data}) + this.setState({ containers: resp.data }) }) } openTerminal = () => { - this.navigate(`/terminal/container/${this.state.currentContainerId}/client/DEFAULT`) + let clientId = localStorage.getItem('clientId') + this.navigate(`/terminal/container/${this.state.currentContainerId}/client/${clientId}`) + } + + + openContainerStat = ()=>{ + this.navigate(`/app/container/${this.state.currentContainerId}/stat`) } render() { return ( -
-
-
- - - - -
+
+
+
+ + + +
- - record.Id} - rowSelection={{fixed: 'left', type: 'checkbox'}} - columns={this.columns} dataSource={this.state.containers} - scroll={{x: 1000}}/> - - this.hideDetail()} - visible={this.state.detailDrawerStatus}> - - - - - this.hideMoreOperatorDrawer()} - visible={this.state.moreDrawerStatus}> -
- - - - - - - - -
-
+ +
record.Id} + rowSelection={{ fixed: 'left', type: 'checkbox' }} + columns={this.columns} dataSource={this.state.containers} + scroll={{ x: 1000 }} /> + + this.hideDetail()} + visible={this.state.detailDrawerStatus}> + + + + + + + + + + + this.hideMoreOperatorDrawer()} + visible={this.state.moreDrawerStatus}> +
+ + + + + + + + +
+
+ ); } diff --git a/admin/src/page/ContainerStatPage/index.tsx b/admin/src/page/ContainerStatPage/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8ea29b2ed4e471113b99bd56477aeb9aa7096e33 --- /dev/null +++ b/admin/src/page/ContainerStatPage/index.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import ContainerStat from "../../component/App/Container/ContainerStat"; +import WithRouter from "../../router/WithRouter"; + + +class ContainerStatPage extends React.Component{ + + render() { + return ( +
+
+ +
+ +
+ +
+
+ ) + } +} + +export default WithRouter(ContainerStatPage); \ No newline at end of file diff --git a/admin/src/page/ContainerTerminal/index.jsx b/admin/src/page/ContainerTerminal/index.jsx index de3b3dc7a7b7bee3237c4edd67588ccfe31caee8..d302f57e095c7a9ab12b92559d15b17358f85158 100644 --- a/admin/src/page/ContainerTerminal/index.jsx +++ b/admin/src/page/ContainerTerminal/index.jsx @@ -125,9 +125,9 @@ class ContainerTerminal extends React.Component { this.props.router.navigate(-1)} - subTitle="查看容器日志信息" + subTitle="查看容器终端" extra={ <> @@ -136,6 +136,7 @@ class ContainerTerminal extends React.Component { + }/> diff --git a/admin/src/router/MainPageRouter.tsx b/admin/src/router/MainPageRouter.tsx index 010107ef58666ecf8bdfc48c95f3dfb5a3c96117..744250c851be7d827248dfb8e32c57a39ad9e7af 100644 --- a/admin/src/router/MainPageRouter.tsx +++ b/admin/src/router/MainPageRouter.tsx @@ -11,61 +11,70 @@ import AboutPage from "../page/AboutPage"; import CreateContainerPage from "../page/CreateContainerPage"; import ContainerLogPage from "../page/ContainerLogPage"; import AuthPage from "../page/AuthPage"; +import ContainerStat from "../component/App/Container/ContainerStat"; +import ContainerStatPage from "../page/ContainerStatPage"; export const appRouter: Array = [ { path: '/app/home', - component: , + component: , exact: true }, { path: '/app/image', - component: , + component: , exact: true }, { path: '/app/container', - component: , + component: , exact: true }, { path: '/app/container/:containerId/log', - component: , + component: , exact: true }, + { + path: '/app/container/:containerId/stat', + component: + }, + + + { path: '/app/volume', - component: , + component: , exact: true }, { path: '/app/network', - component: , + component: , exact: true }, { path: '/app/monitor', - component: , + component: , exact: true }, { path: '/app/setting', - component: , + component: , exact: true }, { path: '/app/auth', - component: , + component: , exact: true }, { path: '/app/about', - component: , + component: , exact: true }, { path: '/app/image/:imageId/run', - component: , + component: , exact: true } ] diff --git a/admin/src/utils/DateTime.ts b/admin/src/utils/DateTime.ts index e57d5b5962576c5e6ec29931978cdb0fce741a1a..6221ac20f2f01bd8d04673afcb996203d6e81caf 100644 --- a/admin/src/utils/DateTime.ts +++ b/admin/src/utils/DateTime.ts @@ -1,7 +1,11 @@ export default function dateToStr(timestamp: number): String { let date: Date = new Date(timestamp); - let y = date.getFullYear(); + let y = date.getFullYear(); let m = "0" + (date.getMonth() + 1); let d = "0" + date.getDate(); return y + "-" + m.substring(m.length - 2, m.length) + "-" + d.substring(d.length - 2, d.length); } + +export function formatterTime(date: Date): string { + return date.getMinutes() + ":" + date.getSeconds() +} \ No newline at end of file diff --git a/admin/src/utils/FifoArray.ts b/admin/src/utils/FifoArray.ts new file mode 100644 index 0000000000000000000000000000000000000000..712a3298c85521f2c1ddefd3d11b870f1e77e479 --- /dev/null +++ b/admin/src/utils/FifoArray.ts @@ -0,0 +1,44 @@ + +export class FifoCache{ + + private limit: number = 10 + private keys: Array = [] + private values: Array = [] + private full: boolean = false + + constructor(limit: number) { + this.limit = limit + } + + set(key: K, value: V) { + console.log(`k=${key},v=${value}`) + if (!this.full) { + this.keys.push(key) + this.values.push(value) + if (this.limit === this.keys.length) { + this.full = true + } else { + this.full = false + } + return + } + + + for (let i = 1; i < this.limit; i++) { + this.keys[i - 1] = this.keys[i] + this.values[i - 1] = this.values[i] + } + + this.keys[this.limit - 1] = key + this.values[this.limit - 1] = value + } + + getKeys(): Array { + return this.keys + } + + getValues(): Array { + return this.values + } +} + diff --git a/backend-src/src/main/java/com/taoes/simpledocker/controller/ImageController.java b/backend-src/src/main/java/com/taoes/simpledocker/controller/ImageController.java index ed3edd80884285f1988f1725872d4f8b4f5fab6a..a64db39452c4766495879ba989a5bde338508ca1 100644 --- a/backend-src/src/main/java/com/taoes/simpledocker/controller/ImageController.java +++ b/backend-src/src/main/java/com/taoes/simpledocker/controller/ImageController.java @@ -1,6 +1,7 @@ package com.taoes.simpledocker.controller; import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.InspectImageResponse; @@ -13,6 +14,9 @@ import com.taoes.simpledocker.model.Role; import com.taoes.simpledocker.service.ImageService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; + +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -26,6 +30,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -128,8 +133,21 @@ public class ImageController { @ApiOperation("导出镜像") @SaCheckPermission(value = "image:export",orRole = Role.ADMIN_ROLE_NAME) @GetMapping("/save/{nameTag}") - public void save(@PathVariable String nameTag, HttpServletRequest request,HttpServletResponse response) { - imageService.save(nameTag,request,response); + public void save(@PathVariable String nameTag,HttpServletResponse response) { + try (InputStream input = imageService.save(nameTag); + ServletOutputStream output = response.getOutputStream()) { + String currentTime = DateUtil.format(DateUtil.date(), "yyyyMMdd_HHmmss_"); + response.setContentType("application/x-zip-compressed;charset=UTF-8"); + response.setHeader("Content-Disposition","attachment;filename=" + currentTime + nameTag + ".zip"); + // 循环取出流中的数据 + byte[] b = new byte[1024]; + int len; + while ((len = input.read(b)) > 0) { + output.write(b, 0, len); + } + } catch (IOException e) { + e.printStackTrace(); + } } /** * 批量保存镜像 diff --git a/backend-src/src/main/java/com/taoes/simpledocker/service/ImageService.java b/backend-src/src/main/java/com/taoes/simpledocker/service/ImageService.java index 4223f188a656e546138718449b9be67e4b1f3a04..d60e825e0159c1628916c6309512c3056e584fac 100644 --- a/backend-src/src/main/java/com/taoes/simpledocker/service/ImageService.java +++ b/backend-src/src/main/java/com/taoes/simpledocker/service/ImageService.java @@ -1,5 +1,6 @@ package com.taoes.simpledocker.service; +import java.io.InputStream; import java.util.List; import com.github.dockerjava.api.command.InspectImageResponse; @@ -95,8 +96,10 @@ public interface ImageService { /** * 镜像保存 + * + * @return */ - void save(String nameTag, HttpServletRequest request,HttpServletResponse response); + InputStream save(String nameTag); /** * 批量镜像保存 diff --git a/backend-src/src/main/java/com/taoes/simpledocker/service/imple/ImageServiceImpl.java b/backend-src/src/main/java/com/taoes/simpledocker/service/imple/ImageServiceImpl.java index b6b41281ca674132ec57beb4bedac2a96f624401..3e5ef34805150d4945f2c0d11436d178d6008ea6 100644 --- a/backend-src/src/main/java/com/taoes/simpledocker/service/imple/ImageServiceImpl.java +++ b/backend-src/src/main/java/com/taoes/simpledocker/service/imple/ImageServiceImpl.java @@ -6,7 +6,6 @@ import com.github.dockerjava.api.command.SaveImagesCmd; import com.taoes.simpledocker.model.exception.NotFoundClientException; import java.io.*; -import java.net.URLEncoder; import java.util.List; import com.github.dockerjava.api.DockerClient; @@ -109,25 +108,12 @@ public class ImageServiceImpl implements ImageService { } @Override - public void save(String nameTag, HttpServletRequest request,HttpServletResponse response) { + public InputStream save(String nameTag) { final DockerClient dockerClient = factory.get(); String[] nameTagArr = nameTag.split("\\:"); //docker save SaveImageCmd saveImage = dockerClient.saveImageCmd(nameTagArr[0]).withTag(nameTagArr[1]); - try (InputStream input = saveImage.exec(); - ServletOutputStream output = response.getOutputStream()) { - String curentTime = DateUtil.format(DateUtil.date(), "yyyyMMdd_HHmmss_"); - response.setContentType("application/x-zip-compressed;charset=UTF-8"); - response.setHeader("Content-Disposition","attachment;filename=" + curentTime + nameTag + ".zip"); - // 循环取出流中的数据 - byte[] b = new byte[1024]; - int len; - while ((len = input.read(b)) > 0) { - output.write(b, 0, len); - } - } catch (IOException e) { - e.printStackTrace(); - } + return saveImage.exec(); } @Override diff --git a/backend-src/src/main/java/com/taoes/simpledocker/ws/AbstractSocketStream.java b/backend-src/src/main/java/com/taoes/simpledocker/ws/AbstractSocketStream.java index d8c63384251602ade187ef106b74d5561615a2ec..7969490407d183e1b708496ee7eb88da8796f7cd 100644 --- a/backend-src/src/main/java/com/taoes/simpledocker/ws/AbstractSocketStream.java +++ b/backend-src/src/main/java/com/taoes/simpledocker/ws/AbstractSocketStream.java @@ -39,8 +39,7 @@ public abstract class AbstractSocketStream { log.error("回话:{}已关闭,停止写入数据", sessionId); return; } - final String sessionId = session.getId(); - final OutputStream stream = outs.get(sessionId); + final OutputStream stream = outs.get(session.getId()); if (stream == null) { return; } diff --git a/backend-src/src/main/java/com/taoes/simpledocker/ws/AbstractWebSocket.java b/backend-src/src/main/java/com/taoes/simpledocker/ws/AbstractWebSocket.java index e0462d040ac0f6b7008ee168686aa558a9f94d8f..b6baba28ab747ff4e3d2f9526ccf6ec5ba6e5e4f 100644 --- a/backend-src/src/main/java/com/taoes/simpledocker/ws/AbstractWebSocket.java +++ b/backend-src/src/main/java/com/taoes/simpledocker/ws/AbstractWebSocket.java @@ -1,6 +1,8 @@ package com.taoes.simpledocker.ws; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.stp.StpUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import com.github.dockerjava.api.async.ResultCallbackTemplate; @@ -134,4 +136,6 @@ public abstract class AbstractWebSocket extends AbstractSocketStream { sessionSet.remove(sessionOfExist); } + + } diff --git a/backend-src/src/main/java/com/taoes/simpledocker/ws/ContainerFileWebSocket.java b/backend-src/src/main/java/com/taoes/simpledocker/ws/ContainerFileWebSocket.java index 75a6ba67b762affdfe19266304fb9177c66ae179..bba20b9d36a884adae287c02960457d8372b2b64 100644 --- a/backend-src/src/main/java/com/taoes/simpledocker/ws/ContainerFileWebSocket.java +++ b/backend-src/src/main/java/com/taoes/simpledocker/ws/ContainerFileWebSocket.java @@ -3,6 +3,7 @@ package com.taoes.simpledocker.ws; import com.Ostermiller.util.CircularByteBuffer; import com.github.dockerjava.api.DockerClient; import com.taoes.simpledocker.config.DockerClientFactory; +import com.taoes.simpledocker.ws.callback.FileManagementCallback; import com.taoes.simpledocker.ws.callback.TerminalResultCallback; import java.io.InputStream; @@ -72,14 +73,22 @@ public class ContainerFileWebSocket extends AbstractWebSocket { .withTty(true) .exec().getId(); - // 执行命令 - final TerminalResultCallback callback = client.execStartCmd(execId) + final FileManagementCallback callback = client.execStartCmd(execId) .withStdIn(this.getInput(session)) - .exec(new TerminalResultCallback(session)); + .exec(new FileManagementCallback(session)); this.addCallback(session, callback); } catch (Exception e) { this.onClose(session); } } + + /** + * 收到客户端消息后写入输出流 + */ + @SneakyThrows + @OnMessage + public void onMessage(String message, Session session) { + super.write(session, (message + "\n").getBytes()); + } } diff --git a/backend-src/src/main/java/com/taoes/simpledocker/ws/callback/ContainerMonitorCallback.java b/backend-src/src/main/java/com/taoes/simpledocker/ws/callback/ContainerMonitorCallback.java index 767bd77d79645280e55d4abe6548876932238b48..0cb4afa0d9c127666cad672c3ce747bf4a69223d 100644 --- a/backend-src/src/main/java/com/taoes/simpledocker/ws/callback/ContainerMonitorCallback.java +++ b/backend-src/src/main/java/com/taoes/simpledocker/ws/callback/ContainerMonitorCallback.java @@ -28,7 +28,6 @@ public class ContainerMonitorCallback extends AbstractResultCallback @SneakyThrows @Override public void onNext(Statistics statistics) { - log.debug("接收到消息:{}", JsonUtils.toJsonString(statistics)); sendMessage(JsonUtils.toJsonString(statistics)); } diff --git a/backend-src/src/main/java/com/taoes/simpledocker/ws/callback/FileManagementCallback.java b/backend-src/src/main/java/com/taoes/simpledocker/ws/callback/FileManagementCallback.java new file mode 100644 index 0000000000000000000000000000000000000000..563eb34f94cff7b9fe669907c9127b425e07a2bc --- /dev/null +++ b/backend-src/src/main/java/com/taoes/simpledocker/ws/callback/FileManagementCallback.java @@ -0,0 +1,46 @@ +package com.taoes.simpledocker.ws.callback; + +import com.github.dockerjava.api.model.Frame; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import javax.websocket.Session; +import java.io.Closeable; +import java.nio.ByteBuffer; + +@Slf4j +public class FileManagementCallback extends AbstractResultCallback { + + + public FileManagementCallback(Session session) { + super("容器文件", session); + } + + @SneakyThrows + @Override + public void onStart(Closeable closeable) { + } + + @SneakyThrows + @Override + public void onNext(Frame object) { + System.out.println(new String(object.getPayload())); + sendMessage(new String(object.getPayload())); + } + + @Override + @SneakyThrows + public void onError(Throwable throwable) { + sendMessage("ERROR:" + throwable.getMessage()); + this.close(); + } + + @Override + @SneakyThrows + public void onComplete() { + sendMessage("COMPLETE"); + this.close(); + } + + +}