|
|
@@ -3,12 +3,20 @@ import styled from "styled-components";
|
|
|
|
|
|
import { Context } from "shared/Context";
|
|
|
import api from "shared/api";
|
|
|
-import { ClusterType } from "shared/types";
|
|
|
+import {
|
|
|
+ ClusterType,
|
|
|
+ DetailedClusterType,
|
|
|
+ DetailedIngressError,
|
|
|
+} from "shared/types";
|
|
|
import Helper from "components/values-form/Helper";
|
|
|
import { pushFiltered } from "shared/routing";
|
|
|
|
|
|
import { RouteComponentProps, withRouter } from "react-router";
|
|
|
|
|
|
+import CopyToClipboard from "components/CopyToClipboard";
|
|
|
+import Loading from "components/Loading";
|
|
|
+import Modal from "../modals/Modal";
|
|
|
+
|
|
|
type PropsType = RouteComponentProps & {
|
|
|
currentCluster: ClusterType;
|
|
|
};
|
|
|
@@ -16,14 +24,19 @@ type PropsType = RouteComponentProps & {
|
|
|
type StateType = {
|
|
|
loading: boolean;
|
|
|
error: string;
|
|
|
- clusters: ClusterType[];
|
|
|
+ clusters: DetailedClusterType[];
|
|
|
+ showErrorModal?: {
|
|
|
+ clusterId: number;
|
|
|
+ show: boolean;
|
|
|
+ };
|
|
|
};
|
|
|
|
|
|
class Templates extends Component<PropsType, StateType> {
|
|
|
- state = {
|
|
|
+ state: StateType = {
|
|
|
loading: true,
|
|
|
error: "",
|
|
|
- clusters: [] as ClusterType[],
|
|
|
+ clusters: [],
|
|
|
+ showErrorModal: undefined,
|
|
|
};
|
|
|
|
|
|
componentDidMount() {
|
|
|
@@ -36,17 +49,50 @@ class Templates extends Component<PropsType, StateType> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- updateClusterList = () => {
|
|
|
- api
|
|
|
- .getClusters("<token>", {}, { id: this.context.currentProject.id })
|
|
|
- .then((res) => {
|
|
|
- if (res.data) {
|
|
|
- this.setState({ clusters: res.data, loading: false, error: "" });
|
|
|
- } else {
|
|
|
- this.setState({ loading: false, error: "Response data missing" });
|
|
|
- }
|
|
|
- })
|
|
|
- .catch((err) => this.setState(err));
|
|
|
+ updateClusterList = async () => {
|
|
|
+ try {
|
|
|
+ const res = await api.getClusters(
|
|
|
+ "<token>",
|
|
|
+ {},
|
|
|
+ { id: this.context.currentProject.id }
|
|
|
+ );
|
|
|
+
|
|
|
+ if (res.data) {
|
|
|
+ this.setState({ clusters: res.data, loading: false, error: "" });
|
|
|
+
|
|
|
+ this.state.clusters.forEach((cluster) => {
|
|
|
+ this.updateClusterWithDetailedData(cluster.id);
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ this.setState({ loading: false, error: "Response data missing" });
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ this.setState(err);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ updateClusterWithDetailedData = async (clusterId: number) => {
|
|
|
+ try {
|
|
|
+ const currentClusterIndex = this.state.clusters.findIndex(
|
|
|
+ (cluster) => cluster.id === clusterId
|
|
|
+ );
|
|
|
+ const res = await api.getCluster(
|
|
|
+ "<token>",
|
|
|
+ {},
|
|
|
+ { project_id: this.context.currentProject.id, cluster_id: clusterId }
|
|
|
+ );
|
|
|
+ if (res.data) {
|
|
|
+ this.setState((prevState) => {
|
|
|
+ const currentCluster = prevState.clusters[currentClusterIndex];
|
|
|
+ prevState.clusters.splice(currentClusterIndex, 1, {
|
|
|
+ ...currentCluster,
|
|
|
+ ingress_ip: res.data.ingress_ip,
|
|
|
+ ingress_error: res.data.ingress_error,
|
|
|
+ });
|
|
|
+ return prevState;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (error) {}
|
|
|
};
|
|
|
|
|
|
renderIcon = () => {
|
|
|
@@ -57,23 +103,107 @@ class Templates extends Component<PropsType, StateType> {
|
|
|
);
|
|
|
};
|
|
|
|
|
|
- renderClusters = () => {
|
|
|
- return this.state.clusters.map((cluster: ClusterType, i: number) => {
|
|
|
+ renderIngressIp = (
|
|
|
+ clusterId: number,
|
|
|
+ ingressIp: string | undefined,
|
|
|
+ ingressError: DetailedIngressError
|
|
|
+ ) => {
|
|
|
+ if (typeof ingressIp !== "string") {
|
|
|
+ return (
|
|
|
+ <Url onClick={(e) => e.preventDefault()}>
|
|
|
+ <Loading />
|
|
|
+ </Url>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!ingressIp.length && ingressError) {
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <Url
|
|
|
+ onClick={(e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ this.setState({ showErrorModal: { clusterId, show: true } });
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Bolded>Ingress IP:</Bolded>
|
|
|
+ <span>{ingressError.message}</span>
|
|
|
+ <i className="material-icons">launch</i>
|
|
|
+ </Url>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!ingressIp.length) {
|
|
|
return (
|
|
|
- <TemplateBlock
|
|
|
- onClick={() => {
|
|
|
- this.context.setCurrentCluster(cluster);
|
|
|
- pushFiltered(this.props, "/applications", ["project_id"], {
|
|
|
- cluster: cluster.name,
|
|
|
- });
|
|
|
- }}
|
|
|
- key={i}
|
|
|
- >
|
|
|
- {this.renderIcon()}
|
|
|
- <TemplateTitle>{cluster.name}</TemplateTitle>
|
|
|
- </TemplateBlock>
|
|
|
+ <Url>
|
|
|
+ <Bolded>Ingress IP:</Bolded>
|
|
|
+ <span>Ingress IP not available</span>
|
|
|
+ </Url>
|
|
|
);
|
|
|
- });
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <CopyToClipboard
|
|
|
+ as={Url}
|
|
|
+ text={ingressIp}
|
|
|
+ wrapperProps={{ onClick: (e: any) => e.stopPropagation() }}
|
|
|
+ >
|
|
|
+ <Bolded>Ingress IP:</Bolded>
|
|
|
+ <span>{ingressIp}</span>
|
|
|
+ <i className="material-icons-outlined">content_copy</i>
|
|
|
+ </CopyToClipboard>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ renderClusters = () => {
|
|
|
+ return this.state.clusters.map(
|
|
|
+ (cluster: DetailedClusterType, i: number) => {
|
|
|
+ return (
|
|
|
+ <TemplateBlock
|
|
|
+ onClick={() => {
|
|
|
+ this.context.setCurrentCluster(cluster);
|
|
|
+ pushFiltered(this.props, "/applications", ["project_id"], {
|
|
|
+ cluster: cluster.name,
|
|
|
+ });
|
|
|
+ }}
|
|
|
+ key={i}
|
|
|
+ >
|
|
|
+ <TitleContainer>
|
|
|
+ {this.renderIcon()}
|
|
|
+ <TemplateTitle>{cluster.name}</TemplateTitle>
|
|
|
+ </TitleContainer>
|
|
|
+ {this.renderIngressIp(
|
|
|
+ cluster.id,
|
|
|
+ cluster.ingress_ip,
|
|
|
+ cluster.ingress_error
|
|
|
+ )}
|
|
|
+ </TemplateBlock>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ renderErrorModal = () => {
|
|
|
+ const clusterError =
|
|
|
+ this.state.showErrorModal?.show &&
|
|
|
+ this.state.clusters.find(
|
|
|
+ (c) => c.id === this.state.showErrorModal?.clusterId
|
|
|
+ );
|
|
|
+ const ingressError = clusterError?.ingress_error;
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ {clusterError && (
|
|
|
+ <Modal
|
|
|
+ onRequestClose={() => this.setState({ showErrorModal: undefined })}
|
|
|
+ width="665px"
|
|
|
+ height="min-content"
|
|
|
+ >
|
|
|
+ Porter encountered an error. Full error log:
|
|
|
+ <CodeBlock>{ingressError.error}</CodeBlock>
|
|
|
+ </Modal>
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ );
|
|
|
};
|
|
|
|
|
|
render() {
|
|
|
@@ -81,6 +211,7 @@ class Templates extends Component<PropsType, StateType> {
|
|
|
<StyledClusterList>
|
|
|
<Helper>Clusters connected to this project:</Helper>
|
|
|
<TemplateList>{this.renderClusters()}</TemplateList>
|
|
|
+ {this.renderErrorModal()}
|
|
|
</StyledClusterList>
|
|
|
);
|
|
|
}
|
|
|
@@ -90,11 +221,34 @@ Templates.contextType = Context;
|
|
|
|
|
|
export default withRouter(Templates);
|
|
|
|
|
|
+const CodeBlock = styled.span`
|
|
|
+ display: block;
|
|
|
+ background-color: #1b1d26;
|
|
|
+ color: white;
|
|
|
+ border-radius: 5px;
|
|
|
+ font-family: monospace;
|
|
|
+ user-select: text;
|
|
|
+ max-height: 400px;
|
|
|
+ width: 90%;
|
|
|
+ margin-left: 5%;
|
|
|
+ margin-top: 20px;
|
|
|
+ overflow-x: hidden;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 10px;
|
|
|
+ overflow-wrap: break-word;
|
|
|
+`;
|
|
|
+
|
|
|
const StyledClusterList = styled.div`
|
|
|
margin-top: -17px;
|
|
|
padding-left: 2px;
|
|
|
`;
|
|
|
|
|
|
+const TitleContainer = styled.div`
|
|
|
+ display: flex;
|
|
|
+ width: 100%;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+`;
|
|
|
const DashboardIcon = styled.div`
|
|
|
position: relative;
|
|
|
height: 45px;
|
|
|
@@ -106,19 +260,20 @@ const DashboardIcon = styled.div`
|
|
|
justify-content: center;
|
|
|
background: #676c7c;
|
|
|
border: 2px solid #8e94aa;
|
|
|
-
|
|
|
+ margin-bottom: 10px;
|
|
|
> i {
|
|
|
font-size: 22px;
|
|
|
}
|
|
|
`;
|
|
|
|
|
|
const TemplateTitle = styled.div`
|
|
|
- margin-bottom: 26px;
|
|
|
+ margin-bottom: 0px;
|
|
|
width: 80%;
|
|
|
text-align: center;
|
|
|
font-size: 14px;
|
|
|
white-space: nowrap;
|
|
|
overflow: hidden;
|
|
|
+ white-space: nowrap;
|
|
|
text-overflow: ellipsis;
|
|
|
`;
|
|
|
|
|
|
@@ -130,11 +285,11 @@ const TemplateBlock = styled.div`
|
|
|
display: flex;
|
|
|
font-size: 13px;
|
|
|
font-weight: 500;
|
|
|
- padding: 35px 10px 12px;
|
|
|
+ padding: 35px;
|
|
|
flex-direction: column;
|
|
|
align-item: center;
|
|
|
justify-content: space-between;
|
|
|
- height: 165px;
|
|
|
+ height: 192px;
|
|
|
cursor: pointer;
|
|
|
color: #ffffff;
|
|
|
position: relative;
|
|
|
@@ -202,3 +357,29 @@ const TemplatesWrapper = styled.div`
|
|
|
min-width: 300px;
|
|
|
padding-top: 50px;
|
|
|
`;
|
|
|
+
|
|
|
+const Url = styled.a`
|
|
|
+ width: 100%;
|
|
|
+ font-size: 13px;
|
|
|
+ user-select: text;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ > i {
|
|
|
+ margin-left: 10px;
|
|
|
+ font-size: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ > span {
|
|
|
+ overflow: hidden;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+const Bolded = styled.div`
|
|
|
+ font-weight: 500;
|
|
|
+ color: #ffffff44;
|
|
|
+ margin-right: 6px;
|
|
|
+ white-space: nowrap;
|
|
|
+`;
|