|
|
@@ -0,0 +1,493 @@
|
|
|
+import React, { PureComponent } from 'react';
|
|
|
+import { connect, history } from 'umi';
|
|
|
+import { Empty, PageHeader, Button, Popconfirm, Descriptions, Steps, Divider, Modal, Table, Input } from 'antd';
|
|
|
+import moment from 'moment';
|
|
|
+import {
|
|
|
+ LoadingOutlined,
|
|
|
+} from '@ant-design/icons';
|
|
|
+import styles from './[environmentId].less'
|
|
|
+import ConfigModal from '@/components/ConfigModal'
|
|
|
+import EnvironmentModal from '@/components/EnvironmentModal'
|
|
|
+const { Step } = Steps;
|
|
|
+const buildTypes = ['FROM_BRANCH_OR_COMMIT', 'FROM_IMAGE'];
|
|
|
+const buildTypeOptionName = {
|
|
|
+ FROM_BRANCH_OR_COMMIT: '从分支或commit构建',
|
|
|
+ FROM_IMAGE: '从现有镜像构建',
|
|
|
+}
|
|
|
+const buildTypeValueLabels = {
|
|
|
+ FROM_BRANCH_OR_COMMIT: '分支名/commit',
|
|
|
+ FROM_IMAGE: '镜像名',
|
|
|
+}
|
|
|
+const statusText = {
|
|
|
+ NOT_STARTED: '未开始',
|
|
|
+ SUCCESS: '成功',
|
|
|
+ FAIL: '失败',
|
|
|
+ BUILDING: '正在构建',
|
|
|
+ RESTARTING: '正在重启',
|
|
|
+ DEPLOYING: '正在部署',
|
|
|
+}
|
|
|
+
|
|
|
+const statusColor = {
|
|
|
+ NOT_STARTED: '#cfcfcf',
|
|
|
+ SUCCESS: '#91e12e',
|
|
|
+ FAIL: '#f81d22',
|
|
|
+ BUILDING: '#16d0ff',
|
|
|
+ RESTARTING: '#f89d58',
|
|
|
+ DEPLOYING: '#16d0ff',
|
|
|
+}
|
|
|
+
|
|
|
+const statusStepStatus = {
|
|
|
+ NOT_STARTED: 'wait',
|
|
|
+ SUCCESS: 'finish',
|
|
|
+ FAIL: 'error',
|
|
|
+ BUILDING: 'process',
|
|
|
+ RESTARTING: 'process',
|
|
|
+ DEPLOYING: 'process',
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+@connect(({ environment, loading }) => ({
|
|
|
+ environment, loading
|
|
|
+}))
|
|
|
+export default class AppEnvironment extends PureComponent {
|
|
|
+ state = {
|
|
|
+ currentEnvironment: null,
|
|
|
+ showConfigModal: false,
|
|
|
+ showEnvironmentModal: false,
|
|
|
+ showLogModal: false,
|
|
|
+ currentStep: 0,
|
|
|
+ buildTimeOffset: null,
|
|
|
+ deployTimeOffset: null,
|
|
|
+ log: '',
|
|
|
+ requestFromInitial: false,
|
|
|
+ }
|
|
|
+ fetchData(id) {
|
|
|
+ this.setState({
|
|
|
+ requestFromInitial: true,
|
|
|
+ });
|
|
|
+ this.props.dispatch({
|
|
|
+ type: 'environment/get',
|
|
|
+ payload: {
|
|
|
+ id,
|
|
|
+ callback: (response) => {
|
|
|
+ if (response && response.code == 0 && response.result) {
|
|
|
+ this.props.dispatch({
|
|
|
+ type: 'environment/setCurrentEnvironment',
|
|
|
+ payload: {
|
|
|
+ ...response.result,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ this.setState({
|
|
|
+ currentEnvironment: response.result,
|
|
|
+ currentStep: response.result.buildStatus != 'SUCCESS' ? 0 : 1,
|
|
|
+ });
|
|
|
+ if (response.result.buildStatus.endsWith('ING') || response.result.deployStatus.endsWith('ING')) {
|
|
|
+ this.requestInterval();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ requestAndSetStatus() {
|
|
|
+ this.props.dispatch({
|
|
|
+ type: 'environment/get',
|
|
|
+ payload: {
|
|
|
+ id: this.state.currentEnvironment.id,
|
|
|
+ callback: (response) => {
|
|
|
+ if (response && response.code == 0) {
|
|
|
+ let buildFinished = !response.result.buildStatus.endsWith('ING');
|
|
|
+ let deployFinished = !response.result.deployStatus.endsWith('ING');
|
|
|
+ let buildTimeOffset = !buildFinished ? this.getDuration(this.state.currentEnvironment.buildStartTime) : null;
|
|
|
+ let deployTimeOffset = !deployFinished ? this.getDuration(this.state.currentEnvironment.deployTimeOffset) : null;
|
|
|
+ this.setState({
|
|
|
+ currentEnvironment: response.result,
|
|
|
+ currentStep: response.result.buildStatus != 'SUCCESS' ? 0 : 1,
|
|
|
+ buildTimeOffset,
|
|
|
+ deployTimeOffset,
|
|
|
+ });
|
|
|
+ if (buildFinished && deployFinished && this.interval) {
|
|
|
+ clearInterval(this.interval);
|
|
|
+ this.interval = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ requestInterval() {
|
|
|
+ if (!this.interval) {
|
|
|
+ let count = 0;
|
|
|
+ this.setState({
|
|
|
+ requestFromInitial: false,
|
|
|
+ });
|
|
|
+ this.requestAndSetStatus();
|
|
|
+ this.interval = setInterval(() => {
|
|
|
+ if (this.state.currentEnvironment) {
|
|
|
+ count++;
|
|
|
+ if (count % 5 == 0) {
|
|
|
+ this.requestAndSetStatus();
|
|
|
+ } else {
|
|
|
+ let buildTimeOffset = this.state.currentEnvironment.buildStatus.endsWith('ING') ? this.getDuration(this.state.currentEnvironment.buildStartTime) : null;
|
|
|
+ let deployTimeOffset = this.state.currentEnvironment.deployStatus.endsWith('ING') ? this.getDuration(this.state.currentEnvironment.deployTimeOffset) : null;
|
|
|
+ this.setState({
|
|
|
+ buildTimeOffset,
|
|
|
+ deployTimeOffset,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, 1000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ getDuration(time) {
|
|
|
+ if (time) {
|
|
|
+ return moment.utc(moment().diff(moment(time))).format('HH:mm:ss');
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ componentDidMount() {
|
|
|
+ this.fetchData(this.props.match.params.environmentId);
|
|
|
+ }
|
|
|
+ componentWillReceiveProps(props) {
|
|
|
+ if (this.props.match.params.environmentId != props.match.params.environmentId ) {
|
|
|
+ this.fetchData(props.match.params.environmentId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ showEnvConfig() {
|
|
|
+ this.setState({
|
|
|
+ showConfigModal: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ updateEnvConfig(values) {
|
|
|
+ this.props.dispatch({
|
|
|
+ type: 'config/update',
|
|
|
+ payload: {
|
|
|
+ ...values,
|
|
|
+ callback: (response) => {
|
|
|
+ if (response && response.code == 0) {
|
|
|
+ this.setState({
|
|
|
+ showConfigModal: false,
|
|
|
+ });
|
|
|
+ this.props.dispatch({
|
|
|
+ type: 'environment/get',
|
|
|
+ payload: {
|
|
|
+ id: this.props.match.params.environmentId,
|
|
|
+ callback: (response) => {
|
|
|
+ this.setState({
|
|
|
+ currentEnvironment: response.result,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ hideEnvConfig() {
|
|
|
+ this.setState({
|
|
|
+ showConfigModal: false,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ showEnvUpdate() {
|
|
|
+ this.setState({
|
|
|
+ showEnvironmentModal: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ hideEnvUpdate() {
|
|
|
+ this.setState({
|
|
|
+ showEnvironmentModal: false,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ updateEnv(values) {
|
|
|
+ this.props.dispatch({
|
|
|
+ type: 'environment/update',
|
|
|
+ payload: {
|
|
|
+ ...values,
|
|
|
+ id: this.state.currentEnvironment.id,
|
|
|
+ appId: this.state.currentEnvironment.appId,
|
|
|
+ callback: (response) => {
|
|
|
+ if (response && response.code == 0) {
|
|
|
+ this.setState({
|
|
|
+ showEnvironmentModal: false,
|
|
|
+ });
|
|
|
+ this.props.dispatch({
|
|
|
+ type: 'environment/get',
|
|
|
+ payload: {
|
|
|
+ id: this.props.match.params.environmentId,
|
|
|
+ callback: (response) => {
|
|
|
+ this.setState({
|
|
|
+ currentEnvironment: response.result,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ deleteEnv() {
|
|
|
+ this.props.dispatch({
|
|
|
+ type: 'environment/deleteEnvironment',
|
|
|
+ payload: {
|
|
|
+ id: this.state.currentEnvironment.id,
|
|
|
+ appId: this.props.match.params.id,
|
|
|
+ callback: (response) => {
|
|
|
+ if (response && response.code == 0) {
|
|
|
+ this.props.dispatch({
|
|
|
+ type: 'environment/setCurrentEnvironment',
|
|
|
+ });
|
|
|
+ history.push(`/app/${this.props.match.params.id}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ onChangeStep(current) {
|
|
|
+ this.setState({
|
|
|
+ currentStep: current,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ invokeDeploy() {
|
|
|
+ this.props.dispatch({
|
|
|
+ type: 'environment/deploy',
|
|
|
+ payload: {
|
|
|
+ id: this.state.currentEnvironment.id,
|
|
|
+ callback: (response) => {
|
|
|
+ if (response && response.code == 0) {
|
|
|
+ this.requestInterval();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ requestAndSetLog(logType, id, record) {
|
|
|
+ const type = logType == 'build' || logType == 'deploy' ? 'environment/get' : 'environment/getLog';
|
|
|
+ this.props.dispatch({
|
|
|
+ type,
|
|
|
+ payload: {
|
|
|
+ id,
|
|
|
+ fetchAll: true,
|
|
|
+ podName: record?.name,
|
|
|
+ callback: (response) => {
|
|
|
+ if (response && response.code == 0) {
|
|
|
+ let log = '';
|
|
|
+ switch(logType) {
|
|
|
+ case 'build':
|
|
|
+ log = response.result.buildOutput;
|
|
|
+ break;
|
|
|
+ case 'deploy':
|
|
|
+ log = response.result.deployOutput;
|
|
|
+ break;
|
|
|
+ case 'pod':
|
|
|
+ log = response.result[record.labels[`paas.seecoder.cn/resource`]];
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ this.setState({
|
|
|
+ log,
|
|
|
+ });
|
|
|
+ if (this.elem) {
|
|
|
+ this.elem.scrollIntoView(true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ showLogModal(logType, record) {
|
|
|
+ this.setState({
|
|
|
+ requestFromInitial: false,
|
|
|
+ showLogModal: true,
|
|
|
+ });
|
|
|
+ this.requestAndSetLog(logType, this.state.currentEnvironment.id, record);
|
|
|
+ if (!this.logInterval) {
|
|
|
+ this.logInterval = setInterval(() => {
|
|
|
+ this.requestAndSetLog(logType, this.state.currentEnvironment.id, record);
|
|
|
+ }, 5000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ hideLogModal() {
|
|
|
+ if (this.logInterval) {
|
|
|
+ clearInterval(this.logInterval);
|
|
|
+ this.logInterval = null;
|
|
|
+ }
|
|
|
+ this.setState({
|
|
|
+ showLogModal: false,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ componentWillUnmount() {
|
|
|
+ if (this.interval) {
|
|
|
+ clearInterval(this.interval);
|
|
|
+ this.interval = null;
|
|
|
+ }
|
|
|
+ if (this.logInterval) {
|
|
|
+ clearInterval(this.logInterval);
|
|
|
+ this.logInterval = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ restartPod(record) {
|
|
|
+ this.setState({
|
|
|
+ requestFromInitial: false,
|
|
|
+ });
|
|
|
+ this.props.dispatch({
|
|
|
+ type: 'environment/restart',
|
|
|
+ payload: {
|
|
|
+ id: this.state.currentEnvironment.id,
|
|
|
+ podName: record.name,
|
|
|
+ callback: (response) => {
|
|
|
+ if (response && response.code == 0) {
|
|
|
+ this.requestAndSetStatus();
|
|
|
+ if (!this.interval) {
|
|
|
+ let count = 0;
|
|
|
+ this.interval = setInterval(() => {
|
|
|
+ if (this.state.currentEnvironment) {
|
|
|
+ count++;
|
|
|
+ if (count % 5 == 0) {
|
|
|
+ this.requestAndSetStatus();
|
|
|
+ } else {
|
|
|
+ let buildTimeOffset = this.state.currentEnvironment.buildStatus.endsWith('ING') ? this.getDuration(this.state.currentEnvironment.buildStartTime) : null;
|
|
|
+ let deployTimeOffset = this.state.currentEnvironment.deployStatus.endsWith('ING') ? this.getDuration(this.state.currentEnvironment.deployTimeOffset) : null;
|
|
|
+ this.setState({
|
|
|
+ buildTimeOffset,
|
|
|
+ deployTimeOffset,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, 1000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ render() {
|
|
|
+ const buildType = this.state.currentEnvironment?.buildType;
|
|
|
+ const stepText = this.state.currentStep == 0 ? '构建' : '部署';
|
|
|
+ const stepStatus = this.state.currentStep == 0 ? this.state.currentEnvironment?.buildStatus : this.state.currentEnvironment?.deployStatus;
|
|
|
+ const stepStartTime = this.state.currentStep == 0 ? this.state.currentEnvironment?.buildStartTime : this.state.currentEnvironment?.deployStartTime;
|
|
|
+ const logType = this.state.currentStep == 0 ? 'build' : 'deploy';
|
|
|
+ const columns = [{
|
|
|
+ title: 'pod名称',
|
|
|
+ dataIndex: 'name',
|
|
|
+ }, {
|
|
|
+ title: '状态',
|
|
|
+ dataIndex: ['podStatus', 'phase'],
|
|
|
+ }, {
|
|
|
+ title: 'podIP',
|
|
|
+ dataIndex: ['podStatus', 'podIP'],
|
|
|
+ }, {
|
|
|
+ title: '宿主机IP',
|
|
|
+ dataIndex: ['podStatus', 'hostIP'],
|
|
|
+ }, {
|
|
|
+ title: '操作',
|
|
|
+ render: (text, record, index) => {
|
|
|
+ return (<div key={`action-${record.name}`}>
|
|
|
+ <a key={`action-podlog-${record.name}`} style={{marginRight: 5}} onClick={() => this.showLogModal('pod', record)}>查看日志</a>
|
|
|
+ <Popconfirm title="确认要重启吗?" onConfirm={() => this.restartPod(record)} okText="确定" cancelText="取消">
|
|
|
+ <a key={`action-restart-${record.name}`} style={{marginRight: 5}}>重新启动</a>
|
|
|
+ </Popconfirm>
|
|
|
+ <a key={`action-terminal-${record.name}`} style={{marginRight: 5}} target="_blank" href={`/terminal?namespace=${record.namespace}&podName=${record.name}&token=${sessionStorage.getItem('token')}`}>打开终端</a>
|
|
|
+ </div>)
|
|
|
+ }
|
|
|
+ }];
|
|
|
+ const dataSource = (this.state.currentEnvironment?.instance || []).map(item => ({...item, key: item.name }));
|
|
|
+ const namespace = this.state.currentEnvironment?.instance?.length ? this.state.currentEnvironment.instance[0].namespace : null;
|
|
|
+ const svc = this.state.currentEnvironment?.instance?.length ? this.state.currentEnvironment.instance[0].labels[`paas.seecoder.cn/resource`] : null;
|
|
|
+ return (
|
|
|
+ <div>
|
|
|
+ {this.props.loading.effects['environment/get'] && this.state.requestFromInitial ?
|
|
|
+ <div style={{textAlign: 'center', marginTop: 30}}><LoadingOutlined style={{ fontSize: '36px'}} /></div> :
|
|
|
+ (this.state.currentEnvironment ?
|
|
|
+ <PageHeader
|
|
|
+ backIcon={false}
|
|
|
+ title={this.state.currentEnvironment?.name}
|
|
|
+ extra={this.state.currentEnvironment ? (
|
|
|
+ <div>
|
|
|
+ <Popconfirm title="确认要删除吗?" onConfirm={this.deleteEnv.bind(this)} okText="确定" cancelText="取消">
|
|
|
+ <Button type="danger">删除环境</Button>
|
|
|
+ </Popconfirm>
|
|
|
+ <Button style={{marginLeft: 10}} type="primary" onClick={this.showEnvUpdate.bind(this)} >更新环境</Button>
|
|
|
+ <Button style={{marginLeft: 10}} type="primary" onClick={this.showEnvConfig.bind(this)}>环境配置</Button>
|
|
|
+ <Popconfirm title="确认要触发吗?" onConfirm={this.invokeDeploy.bind(this)} okText="确定" cancelText="取消">
|
|
|
+ <Button style={{marginLeft: 10}} type="primary">触发部署</Button>
|
|
|
+ </Popconfirm>
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+ >
|
|
|
+ <Descriptions size="small"
|
|
|
+ column={2}
|
|
|
+ >
|
|
|
+ <Descriptions.Item label="构建类型">{buildTypeOptionName[buildType]}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label={buildTypeValueLabels[buildType]}>{this.state.currentEnvironment?.buildTypeValue}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label={"内部服务地址"}>{`${svc}.${namespace}.svc.cluster.local`}</Descriptions.Item>
|
|
|
+ </Descriptions>
|
|
|
+ <Divider style={{fontSize: 14, color: '#aaaaaad9', fontWeight: 600}} >部署构建</Divider>
|
|
|
+ <Steps current={this.state.currentStep} onChange={(current) => this.onChangeStep(current)}>
|
|
|
+ <Step
|
|
|
+ title="构建"
|
|
|
+ status={statusStepStatus[this.state.currentEnvironment?.buildStatus]}
|
|
|
+ subTitle={this.state.buildTimeOffset}
|
|
|
+ ></Step>
|
|
|
+ <Step
|
|
|
+ title="部署"
|
|
|
+ status={this.state.currentEnvironment?.buildStatus == 'SUCCESS' ? statusStepStatus[this.state.currentEnvironment?.deployStatus] : 'wait'}
|
|
|
+ subTitle={this.state.deployTimeOffset}
|
|
|
+ ></Step>
|
|
|
+ </Steps>
|
|
|
+ <Descriptions size="small"
|
|
|
+ column={2}
|
|
|
+ style={{marginTop: 10}}
|
|
|
+ >
|
|
|
+ <Descriptions.Item label={`当前${stepText}状态`}>
|
|
|
+ <div><span className={styles.status} style={{background: `${statusColor[stepStatus]}`}}></span>{statusText[stepStatus]}</div>
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label={`${stepText}开始时间`}>{stepStartTime}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label={`镜像`}>{this.state.currentEnvironment?.image}</Descriptions.Item>
|
|
|
+ <Descriptions.Item ><Button type="primary" onClick={() => this.showLogModal(logType)}>查看日志输出</Button></Descriptions.Item>
|
|
|
+ </Descriptions>
|
|
|
+ <Divider style={{fontSize: 14, color: '#aaaaaad9', fontWeight: 600}}>实例维护</Divider>
|
|
|
+ <Table dataSource={dataSource} columns={columns}></Table>
|
|
|
+ <Modal
|
|
|
+ visible={this.state.showLogModal}
|
|
|
+ onOk={this.hideLogModal.bind(this)}
|
|
|
+ onCancel={this.hideLogModal.bind(this)}
|
|
|
+ okText="确认"
|
|
|
+ cancelText="取消"
|
|
|
+ title="查看日志"
|
|
|
+ id="logModal"
|
|
|
+ width="90%"
|
|
|
+ className={styles.logModal}
|
|
|
+ >
|
|
|
+ <div>
|
|
|
+ <Input.TextArea
|
|
|
+ value={this.state.log}
|
|
|
+ bordered={false}
|
|
|
+ readOnly={true}
|
|
|
+ autoSize={true}
|
|
|
+ >
|
|
|
+ </Input.TextArea>
|
|
|
+ <div ref={(elem) => {this.elem = elem;}}></div>
|
|
|
+ </div>
|
|
|
+ </Modal>
|
|
|
+ <ConfigModal
|
|
|
+ visible={this.state.showConfigModal}
|
|
|
+ onOk={this.updateEnvConfig.bind(this)}
|
|
|
+ onCancel={this.hideEnvConfig.bind(this)}
|
|
|
+ config={this.state.currentEnvironment?.config}
|
|
|
+ ></ConfigModal>
|
|
|
+ <EnvironmentModal
|
|
|
+ visible={this.state.showEnvironmentModal}
|
|
|
+ onOk={this.updateEnv.bind(this)}
|
|
|
+ onCancel={this.hideEnvUpdate.bind(this)}
|
|
|
+ environment={this.state.currentEnvironment}
|
|
|
+ >
|
|
|
+ </EnvironmentModal>
|
|
|
+ </PageHeader> :
|
|
|
+ <Empty></Empty>
|
|
|
+ )
|
|
|
+ }
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|