diff --git a/web/src/components/UploadAirgapBundle.jsx b/web/src/components/UploadAirgapBundle.jsx deleted file mode 100644 index fe9c913a3e..0000000000 --- a/web/src/components/UploadAirgapBundle.jsx +++ /dev/null @@ -1,693 +0,0 @@ -import * as React from "react"; -import classNames from "classnames"; -import { withRouter } from "react-router-dom"; -import { KotsPageTitle } from "@components/Head"; -import isEmpty from "lodash/isEmpty"; -import Modal from "react-modal"; -import CodeSnippet from "@src/components/shared/CodeSnippet"; -import MountAware from "@src/components/shared/MountAware"; -import AirgapUploadProgress from "@features/Dashboard/components/AirgapUploadProgress"; -import LicenseUploadProgress from "./LicenseUploadProgress"; -import AirgapRegistrySettings from "./shared/AirgapRegistrySettings"; -import { Utilities } from "../utilities/utilities"; -import { AirgapUploader } from "../utilities/airgapUploader"; - -import "../scss/components/troubleshoot/UploadSupportBundleModal.scss"; -import "../scss/components/Login.scss"; - -const COMMON_ERRORS = { - "HTTP 401": "Registry credentials are invalid", - "invalid username/password": "Registry credentials are invalid", - "no such host": "No such host", -}; - -class UploadAirgapBundle extends React.Component { - state = { - bundleFile: {}, - fileUploading: false, - registryDetails: {}, - preparingOnlineInstall: false, - supportBundleCommand: undefined, - showSupportBundleCommand: false, - onlineInstallErrorMessage: "", - viewOnlineInstallErrorMessage: false, - uploadProgress: 0, - uploadSize: 0, - uploadResuming: false, - }; - - emptyHostnameErrMessage = 'Please enter a value for "Hostname" field'; - - componentDidMount() { - if (!this.state.airgapUploader) { - this.getAirgapConfig(); - } - } - - clearFile = () => { - this.setState({ bundleFile: {} }); - }; - - toggleShowRun = () => { - this.setState({ showSupportBundleCommand: true }); - }; - - getAirgapConfig = async () => { - const { match } = this.props; - const configUrl = `${process.env.API_ENDPOINT}/app/${match.params.slug}/airgap/config`; - let simultaneousUploads = 3; - try { - let res = await fetch(configUrl, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: Utilities.getToken(), - }, - }); - if (res.ok) { - const response = await res.json(); - simultaneousUploads = response.simultaneousUploads; - } - } catch { - // no-op - } - - this.setState({ - airgapUploader: new AirgapUploader( - false, - match.params.slug, - this.onDropBundle, - simultaneousUploads - ), - }); - }; - - uploadAirgapBundle = async () => { - const { match, showRegistry } = this.props; - - // Reset the airgap upload state - const resetUrl = `${process.env.API_ENDPOINT}/app/${match.params.slug}/airgap/reset`; - try { - await fetch(resetUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: Utilities.getToken(), - }, - }); - } catch (error) { - console.error(error); - this.setState({ - fileUploading: false, - uploadProgress: 0, - uploadSize: 0, - uploadResuming: false, - errorMessage: - "An error occurred while uploading your airgap bundle. Please try again", - }); - return; - } - - this.setState({ - fileUploading: true, - errorMessage: "", - showSupportBundleCommand: false, - onlineInstallErrorMessage: "", - }); - - if (showRegistry) { - const { slug } = this.props.match.params; - - if (isEmpty(this.state.registryDetails.hostname)) { - this.setState({ - fileUploading: false, - uploadProgress: 0, - uploadSize: 0, - uploadResuming: false, - errorMessage: this.emptyHostnameErrMessage, - }); - return; - } - - let res; - try { - res = await fetch( - `${process.env.API_ENDPOINT}/app/${slug}/registry/validate`, - { - method: "POST", - headers: { - Authorization: Utilities.getToken(), - "Content-Type": "application/json", - }, - body: JSON.stringify({ - hostname: this.state.registryDetails.hostname, - namespace: this.state.registryDetails.namespace, - username: this.state.registryDetails.username, - password: this.state.registryDetails.password, - isReadOnly: this.state.registryDetails.isReadOnly, - }), - } - ); - } catch (err) { - this.setState({ - fileUploading: false, - uploadProgress: 0, - uploadSize: 0, - uploadResuming: false, - errorMessage: err, - }); - return; - } - - const response = await res.json(); - if (!response.success) { - let msg = - "An error occurred while uploading your airgap bundle. Please try again"; - if (response.error) { - msg = response.error; - } - this.setState({ - fileUploading: false, - uploadProgress: 0, - uploadSize: 0, - uploadResuming: false, - errorMessage: msg, - }); - return; - } - } - - const params = { - registryHost: this.state.registryDetails.hostname, - namespace: this.state.registryDetails.namespace, - username: this.state.registryDetails.username, - password: this.state.registryDetails.password, - isReadOnly: this.state.registryDetails.isReadOnly, - simultaneousUploads: this.state.simultaneousUploads, - }; - this.state.airgapUploader.upload( - params, - this.onUploadProgress, - this.onUploadError - ); - }; - - onUploadProgress = (progress, size, resuming = false) => { - this.setState({ - uploadProgress: progress, - uploadSize: size, - uploadResuming: resuming, - }); - }; - - onUploadError = (message) => { - this.setState({ - fileUploading: false, - uploadProgress: 0, - uploadSize: 0, - uploadResuming: false, - errorMessage: message || "Error uploading bundle, please try again", - }); - }; - - getRegistryDetails = (fields) => { - this.setState({ - ...this.state, - registryDetails: { - hostname: fields.hostname, - username: fields.username, - password: fields.password, - namespace: fields.namespace, - isReadOnly: fields.isReadOnly, - }, - }); - }; - - onDropBundle = async (file) => { - this.setState({ - bundleFile: file, - onlineInstallErrorMessage: "", - errorMessage: "", - }); - }; - - moveBar = (count) => { - const elem = document.getElementById("myBar"); - const percent = count > 3 ? 96 : count * 30; - if (elem) { - elem.style.width = percent + "%"; - } - }; - - handleOnlineInstall = async () => { - const { slug } = this.props.match.params; - - this.setState({ - preparingOnlineInstall: true, - onlineInstallErrorMessage: "", - }); - - let resumeResult; - fetch(`${process.env.API_ENDPOINT}/license/resume`, { - method: "PUT", - headers: { - Authorization: Utilities.getToken(), - "Content-Type": "application/json", - }, - body: JSON.stringify({ - slug, - }), - }) - .then(async (result) => { - resumeResult = await result.json(); - }) - .catch((err) => { - this.setState({ - // TODO: use fewer flags - fileUploading: false, - errorMessage: err, - preparingOnlineInstall: false, - onlineInstallErrorMessage: err, - }); - return; - }); - - let count = 0; - const interval = setInterval(() => { - if (this.state.onlineInstallErrorMessage.length) { - clearInterval(interval); - } - count++; - this.moveBar(count); - if (count > 3) { - if (!resumeResult) { - return; - } - - clearInterval(interval); - - if (resumeResult.error) { - this.setState({ - // TODO: use fewer flags - fileUploading: false, - errorMessage: resumeResult.error, - preparingOnlineInstall: false, - onlineInstallErrorMessage: resumeResult.error, - }); - return; - } - - this.props.onUploadSuccess().then(() => { - // When successful, refetch all the user's apps with onUploadSuccess - const hasPreflight = resumeResult.hasPreflight; - const isConfigurable = resumeResult.isConfigurable; - if (isConfigurable) { - this.props.history.replace(`/${slug}/config`); - } else if (hasPreflight) { - this.props.history.replace(`/${slug}/preflight`); - } else { - this.props.history.replace(`/app/${slug}`); - } - }); - } - }, 1000); - }; - - getSupportBundleCommand = async (slug) => { - const res = await fetch( - `${process.env.API_ENDPOINT}/troubleshoot/app/${slug}/supportbundlecommand`, - { - method: "POST", - headers: { - Authorization: Utilities.getToken(), - "Content-Type": "application/json", - }, - body: JSON.stringify({ - origin: window.location.origin, - }), - } - ); - if (!res.ok) { - throw new Error(`Unexpected status code: ${res.status}`); - } - const response = await res.json(); - return response.command; - }; - - onProgressError = async (errorMessage) => { - const { slug } = this.props.match.params; - - let supportBundleCommand = []; - try { - supportBundleCommand = await this.getSupportBundleCommand(slug); - } catch (err) { - console.log(err); - } - - // Push this setState call to the end of the call stack - setTimeout(() => { - Object.entries(COMMON_ERRORS).forEach(([errorString, message]) => { - if (errorMessage.includes(errorString)) { - errorMessage = message; - } - }); - - this.setState({ - errorMessage, - fileUploading: false, - uploadProgress: 0, - uploadSize: 0, - uploadResuming: false, - supportBundleCommand: supportBundleCommand, - }); - }, 0); - }; - - onProgressSuccess = async () => { - const { onUploadSuccess, match } = this.props; - - await onUploadSuccess(); - - const app = await this.getApp(match.params.slug); - - if (app?.isConfigurable) { - this.props.history.replace(`/${app.slug}/config`); - } else if (app?.hasPreflight) { - this.props.history.replace(`/${app.slug}/preflight`); - } else { - this.props.history.replace(`/app/${app.slug}`); - } - }; - - getApp = async (slug) => { - try { - const res = await fetch(`${process.env.API_ENDPOINT}/app/${slug}`, { - headers: { - Authorization: Utilities.getToken(), - "Content-Type": "application/json", - }, - method: "GET", - }); - if (res.ok && res.status == 200) { - const app = await res.json(); - return app; - } - } catch (err) { - console.log(err); - } - return null; - }; - - toggleViewOnlineInstallErrorMessage = () => { - this.setState({ - viewOnlineInstallErrorMessage: !this.state.viewOnlineInstallErrorMessage, - }); - }; - - toggleErrorModal = () => { - this.setState({ displayErrorModal: !this.state.displayErrorModal }); - }; - - render() { - const { appName, logo, fetchingMetadata, showRegistry, appsListLength } = - this.props; - - const { slug } = this.props.match.params; - - const { - bundleFile, - fileUploading, - uploadProgress, - uploadSize, - uploadResuming, - errorMessage, - registryDetails, - preparingOnlineInstall, - onlineInstallErrorMessage, - viewOnlineInstallErrorMessage, - supportBundleCommand, - } = this.state; - - const hasFile = bundleFile && !isEmpty(bundleFile); - - if (fileUploading) { - return ( - - ); - } - - let logoUri; - let applicationName; - if (appsListLength && appsListLength > 1) { - logoUri = - "https://cdn2.iconfinder.com/data/icons/mixd/512/16_kubernetes-512.png"; - applicationName = ""; - } else { - logoUri = logo; - applicationName = appName; - } - - return ( -
- -
-
-
-
- {logo ? ( - - ) : !fetchingMetadata ? ( - - ) : ( - - )} - -
-
- {preparingOnlineInstall ? ( -
- -
- ) : ( -
-

- Install in airgapped environment -

-

- {showRegistry - ? `To install on an airgapped network, you will need to provide access to a Docker registry. The images ${ - applicationName?.length > 0 - ? `in ${applicationName}` - : "" - } will be retagged and pushed to the registry that you provide here.` - : `To install on an airgapped network, the images ${ - applicationName?.length > 0 - ? `in ${applicationName}` - : "" - } will be uploaded from the bundle you provide to the cluster.`} -

- {showRegistry && ( -
- -
- )} -
- {this.state.airgapUploader ? ( - - this.state.airgapUploader.assignElement(el) - } - className={classNames("FileUpload-wrapper", "flex1", { - "has-file": hasFile, - "has-error": errorMessage, - })} - > - {hasFile ? ( -
-

- {bundleFile.name} -

-
- ) : ( -
-

- Drag your airgap bundle here or{" "} - - choose a bundle to upload - -

-

- This will be a .airgap file - {applicationName?.length > 0 - ? ` ${applicationName} provided` - : ""} - . Please contact your account rep if you are unable - to locate your .airgap file. -

-
- )} -
- ) : null} - {hasFile && ( -
- -
- )} -
- {errorMessage && ( -
- {errorMessage} - {this.state.showSupportBundleCommand ? ( -
-

- Run this command in your cluster -

- - Command has been copied to your clipboard - - } - > - {supportBundleCommand} - -
- ) : supportBundleCommand ? ( -
-
- - Click here - {" "} - to get a command to generate a support bundle. -
-
- ) : null} -
- )} - {hasFile && ( -
- - Select a different bundle - -
- )} -
- )} -
-
-
- - Optionally you can{" "} - - download{" "} - {applicationName?.length > 0 - ? applicationName - : "this application"}{" "} - from the Internet - - -
- {onlineInstallErrorMessage && ( -
- - Unable to install license - - - view more - -
- )} - - -
-
-

- Error description -

-

- {onlineInstallErrorMessage} -

-

- Run this command to generate a support bundle -

- - Command has been copied to your clipboard - - } - > - kubectl support-bundle https://kots.io - -
- -
-
-
- ); - } -} - -export default withRouter(UploadAirgapBundle); diff --git a/web/src/components/UploadAirgapBundle.tsx b/web/src/components/UploadAirgapBundle.tsx new file mode 100644 index 0000000000..dd22c97354 --- /dev/null +++ b/web/src/components/UploadAirgapBundle.tsx @@ -0,0 +1,738 @@ +import React, { useEffect, useReducer, useRef } from "react"; +import { useHistory, useRouteMatch } from "react-router"; +import classNames from "classnames"; +import { KotsPageTitle } from "@components/Head"; +import isEmpty from "lodash/isEmpty"; +import Modal from "react-modal"; +import CodeSnippet from "@src/components/shared/CodeSnippet"; +import MountAware from "@src/components/shared/MountAware"; +import AirgapUploadProgress from "@features/Dashboard/components/AirgapUploadProgress"; +import LicenseUploadProgress from "./LicenseUploadProgress"; +import AirgapRegistrySettings from "./shared/AirgapRegistrySettings"; +import { Utilities } from "../utilities/utilities"; +import { AirgapUploader } from "../utilities/airgapUploader"; + +import "../scss/components/troubleshoot/UploadSupportBundleModal.scss"; +import "../scss/components/Login.scss"; + +const COMMON_ERRORS = { + "HTTP 401": "Registry credentials are invalid", + "invalid username/password": "Registry credentials are invalid", + "no such host": "No such host", +}; + +import { KotsParams } from "@types"; + +type Props = { + appName: string | null; + appsListLength: number; + logo: string | null; + fetchingMetadata: boolean; + onUploadSuccess: () => Promise; + showRegistry: boolean; +}; + +type RegistryDetails = { + hostname: string; + isReadOnly: boolean; + namespace: string; + password: string; + username: string; +}; + +type ResumeResult = { + error?: string; + hasPreflight: boolean; + isConfigurable: boolean; +}; + +type State = { + airgapUploader: AirgapUploader | null; + bundleFile: { + name: string; + } | null; + displayErrorModal?: boolean; + errorMessage: string; + fileUploading: boolean; + registryDetails: RegistryDetails | null; + preparingOnlineInstall: boolean; + supportBundleCommand?: string | string[]; + showSupportBundleCommand: boolean; + simultaneousUploads?: number; + uploadProgress: number; + uploadSize: number; + uploadResuming: boolean; + viewOnlineInstallErrorMessage: boolean; +}; +const UploadAirgapBundle = (props: Props) => { + const [state, setState] = useReducer( + (currentState: State, newState: Partial) => ({ + ...currentState, + ...newState, + }), + { + airgapUploader: null, + bundleFile: null, + errorMessage: "", + fileUploading: false, + registryDetails: null, + preparingOnlineInstall: false, + showSupportBundleCommand: false, + uploadProgress: 0, + uploadSize: 0, + uploadResuming: false, + viewOnlineInstallErrorMessage: false, + } + ); + + const emptyHostnameErrMessage = 'Please enter a value for "Hostname" field'; + const match = useRouteMatch(); + const history = useHistory(); + const appSlug = match.params.slug; + // TODO: refactor the /resume fetching so this isn't necessary + const onlineInstallErrorMessage = useRef(""); + + const onDropBundle = async (file: { name: string }) => { + setState({ + bundleFile: file, + errorMessage: "", + }); + onlineInstallErrorMessage.current = ""; + }; + + const getAirgapConfig = async () => { + const configUrl = `${process.env.API_ENDPOINT}/app/${appSlug}/airgap/config`; + let simultaneousUploads = 3; + try { + let res = await fetch(configUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: Utilities.getToken(), + }, + }); + if (res.ok) { + const response = await res.json(); + simultaneousUploads = response.simultaneousUploads; + } + } catch { + // no-op + } + + setState({ + airgapUploader: new AirgapUploader( + false, + appSlug, + onDropBundle, + simultaneousUploads + ), + }); + }; + + useEffect(() => { + getAirgapConfig(); + }, []); + + const clearFile = () => { + setState({ bundleFile: null }); + }; + + const toggleShowRun = () => { + setState({ showSupportBundleCommand: true }); + }; + + const onUploadProgress = ( + progress: number, + size: number, + resuming = false + ) => { + setState({ + uploadProgress: progress, + uploadSize: size, + uploadResuming: resuming, + }); + }; + + const onUploadError = (message?: string) => { + setState({ + fileUploading: false, + uploadProgress: 0, + uploadSize: 0, + uploadResuming: false, + errorMessage: message || "Error uploading bundle, please try again", + }); + }; + + const uploadAirgapBundle = async () => { + const { showRegistry } = props; + + // Reset the airgap upload state + const resetUrl = `${process.env.API_ENDPOINT}/app/${appSlug}/airgap/reset`; + try { + await fetch(resetUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: Utilities.getToken(), + }, + }); + } catch (error) { + console.error(error); + setState({ + fileUploading: false, + uploadProgress: 0, + uploadSize: 0, + uploadResuming: false, + errorMessage: + "An error occurred while uploading your airgap bundle. Please try again", + }); + return; + } + + setState({ + fileUploading: true, + errorMessage: "", + showSupportBundleCommand: false, + }); + onlineInstallErrorMessage.current = ""; + + if (showRegistry) { + // TODO: remove isEmpty + if (isEmpty(state.registryDetails?.hostname)) { + setState({ + fileUploading: false, + uploadProgress: 0, + uploadSize: 0, + uploadResuming: false, + errorMessage: emptyHostnameErrMessage, + }); + return; + } + + let res; + try { + res = await fetch( + `${process.env.API_ENDPOINT}/app/${appSlug}/registry/validate`, + { + method: "POST", + headers: { + Authorization: Utilities.getToken(), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + hostname: state.registryDetails?.hostname, + namespace: state.registryDetails?.namespace, + username: state.registryDetails?.username, + password: state.registryDetails?.password, + isReadOnly: state.registryDetails?.isReadOnly, + }), + } + ); + } catch (err) { + if (err instanceof Error) { + setState({ + fileUploading: false, + uploadProgress: 0, + uploadSize: 0, + uploadResuming: false, + errorMessage: err.message, + }); + return; + } + + setState({ + fileUploading: false, + uploadProgress: 0, + uploadSize: 0, + uploadResuming: false, + errorMessage: "Something went wrong when uploading Airgap bundle.", + }); + } + + const response = await res?.json(); + if (!response.success) { + let msg = + "An error occurred while uploading your airgap bundle. Please try again"; + if (response.error) { + msg = response.error; + } + setState({ + fileUploading: false, + uploadProgress: 0, + uploadSize: 0, + uploadResuming: false, + errorMessage: msg, + }); + return; + } + } + + const params = { + registryHost: state.registryDetails?.hostname, + namespace: state.registryDetails?.namespace, + username: state.registryDetails?.username, + password: state.registryDetails?.password, + isReadOnly: state.registryDetails?.isReadOnly, + simultaneousUploads: state.simultaneousUploads, + }; + state?.airgapUploader?.upload(params, onUploadProgress, onUploadError); + }; + + const getRegistryDetails = (fields: RegistryDetails) => { + setState({ + ...state, + registryDetails: { + hostname: fields.hostname, + username: fields.username, + password: fields.password, + namespace: fields.namespace, + isReadOnly: fields.isReadOnly, + }, + }); + }; + + const moveBar = (count: number) => { + const elem = document.getElementById("myBar"); + const percent = count > 3 ? 96 : count * 30; + if (elem) { + elem.style.width = percent + "%"; + } + }; + + const handleOnlineInstall = async () => { + setState({ + preparingOnlineInstall: true, + }); + onlineInstallErrorMessage.current = ""; + + let resumeResult: ResumeResult; + fetch(`${process.env.API_ENDPOINT}/license/resume`, { + method: "PUT", + headers: { + Authorization: Utilities.getToken(), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + slug: appSlug, + }), + }) + .then(async (result) => { + resumeResult = await result.json(); + }) + .catch((err) => { + setState({ + // TODO: use fewer flags + fileUploading: false, + errorMessage: err, + preparingOnlineInstall: false, + }); + onlineInstallErrorMessage.current = err; + return; + }); + + let count = 0; + const interval = setInterval(() => { + if (onlineInstallErrorMessage.current.length) { + clearInterval(interval); + } + count += 1; + moveBar(count); + if (count > 3) { + if (!resumeResult) { + return; + } + + clearInterval(interval); + + if (resumeResult.error) { + setState({ + // TODO: use fewer flags + fileUploading: false, + errorMessage: resumeResult.error, + preparingOnlineInstall: false, + }); + + onlineInstallErrorMessage.current = resumeResult.error; + return; + } + + props.onUploadSuccess().then(() => { + // When successful, refetch all the user's apps with onUploadSuccess + const hasPreflight = resumeResult.hasPreflight; + const isConfigurable = resumeResult.isConfigurable; + if (isConfigurable) { + history.replace(`/${appSlug}/config`); + } else if (hasPreflight) { + history.replace(`/${appSlug}/preflight`); + } else { + history.replace(`/app/${appSlug}`); + } + }); + } + }, 1000); + }; + + const getSupportBundleCommand = async () => { + const res = await fetch( + `${process.env.API_ENDPOINT}/troubleshoot/app/${appSlug}/supportbundlecommand`, + { + method: "POST", + headers: { + Authorization: Utilities.getToken(), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + origin: window.location.origin, + }), + } + ); + if (!res.ok) { + throw new Error(`Unexpected status code: ${res.status}`); + } + const response = await res.json(); + return response.command; + }; + + const onProgressError = async (errorMessage: string) => { + let supportBundleCommand: string[] = []; + try { + supportBundleCommand = await getSupportBundleCommand(); + } catch (err) { + console.log(err); + } + + // Push this setState call to the end of the call stack + setTimeout(() => { + Object.entries(COMMON_ERRORS).forEach(([errorString, message]) => { + if (errorMessage.includes(errorString)) { + errorMessage = message; + } + }); + + setState({ + errorMessage, + fileUploading: false, + uploadProgress: 0, + uploadSize: 0, + uploadResuming: false, + supportBundleCommand, + }); + }, 0); + }; + + const getApp = async () => { + try { + const res = await fetch(`${process.env.API_ENDPOINT}/app/${appSlug}`, { + headers: { + Authorization: Utilities.getToken(), + "Content-Type": "application/json", + }, + method: "GET", + }); + if (res.ok && res.status == 200) { + const app = await res.json(); + return app; + } + } catch (err) { + console.log(err); + } + return null; + }; + + const onProgressSuccess = async () => { + const { onUploadSuccess } = props; + + await onUploadSuccess(); + + // TODO: refactor to use app hook + const app = await getApp(); + + if (app?.isConfigurable) { + history.replace(`/${app.slug}/config`); + } else if (app?.hasPreflight) { + history.replace(`/${app.slug}/preflight`); + } else { + history.replace(`/app/${app.slug}`); + } + }; + + const toggleViewOnlineInstallErrorMessage = () => { + setState({ + viewOnlineInstallErrorMessage: !state.viewOnlineInstallErrorMessage, + }); + }; + + const { appName, logo, fetchingMetadata, showRegistry, appsListLength } = + props; + + const { slug } = match.params; + + const { + bundleFile, + fileUploading, + uploadProgress, + uploadSize, + uploadResuming, + errorMessage, + registryDetails, + preparingOnlineInstall, + viewOnlineInstallErrorMessage, + supportBundleCommand, + } = state; + + const hasFile = bundleFile && !isEmpty(bundleFile); + + if (fileUploading) { + return ( + + ); + } + + let logoUri; + let applicationName; + if (appsListLength && appsListLength > 1) { + logoUri = + "https://cdn2.iconfinder.com/data/icons/mixd/512/16_kubernetes-512.png"; + applicationName = ""; + } else { + logoUri = logo; + applicationName = appName || ""; + } + + return ( +
+ +
+
+
+
+ {logo ? ( + + ) : !fetchingMetadata ? ( + + ) : ( + + )} + +
+
+ {preparingOnlineInstall ? ( +
+ +
+ ) : ( +
+

+ Install in airgapped environment +

+

+ {showRegistry + ? `To install on an airgapped network, you will need to provide access to a Docker registry. The images ${ + applicationName?.length > 0 ? `in ${applicationName}` : "" + } will be retagged and pushed to the registry that you provide here.` + : `To install on an airgapped network, the images ${ + applicationName?.length > 0 ? `in ${applicationName}` : "" + } will be uploaded from the bundle you provide to the cluster.`} +

+ {showRegistry && ( +
+ +
+ )} +
+ {state.airgapUploader ? ( + + state.airgapUploader?.assignElement(el) + } + className={classNames("FileUpload-wrapper", "flex1", { + "has-file": hasFile, + "has-error": errorMessage, + })} + > + {hasFile ? ( +
+

+ {bundleFile.name} +

+
+ ) : ( +
+

+ Drag your airgap bundle here or{" "} + + choose a bundle to upload + +

+

+ This will be a .airgap file + {applicationName?.length > 0 + ? ` ${applicationName} provided` + : ""} + . Please contact your account rep if you are unable to + locate your .airgap file. +

+
+ )} +
+ ) : null} + {hasFile && ( +
+ +
+ )} +
+ {errorMessage && ( +
+ {errorMessage} + {state.showSupportBundleCommand ? ( +
+

+ Run this command in your cluster +

+ + Command has been copied to your clipboard + + } + > + {supportBundleCommand} + +
+ ) : supportBundleCommand ? ( +
+
+ + Click here + {" "} + to get a command to generate a support bundle. +
+
+ ) : null} +
+ )} + {hasFile && ( +
+ + Select a different bundle + +
+ )} +
+ )} +
+
+
+ + Optionally you can{" "} + + download{" "} + {applicationName?.length > 0 ? applicationName : "this application"}{" "} + from the Internet + + +
+ {onlineInstallErrorMessage.current && ( +
+ + Unable to install license + + + view more + +
+ )} + + +
+
+

+ Error description +

+

+ {onlineInstallErrorMessage.current} +

+

+ Run this command to generate a support bundle +

+ + Command has been copied to your clipboard + + } + > + kubectl support-bundle https://kots.io + +
+ +
+
+
+ ); +}; + +export default UploadAirgapBundle;