mirror of https://github.com/rancher/dashboard.git
574 lines
17 KiB
Vue
574 lines
17 KiB
Vue
<script lang="ts">
|
|
import Vue, { PropType } from 'vue';
|
|
import jsyaml from 'js-yaml';
|
|
|
|
import Application from '../../models/applications';
|
|
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
|
import FileSelector from '@shell/components/form/FileSelector.vue';
|
|
import GitPicker from '@shell/components/form/GitPicker.vue';
|
|
import RadioGroup from '@components/Form/Radio/RadioGroup.vue';
|
|
import { sortBy } from '@shell/utils/sort';
|
|
import { generateZip } from '@shell/utils/download';
|
|
import Collapse from '@shell/components/Collapse.vue';
|
|
import {
|
|
APPLICATION_SOURCE_TYPE, EpinioApplicationChartResource, EPINIO_TYPES, EpinioInfo, AppSourceArchive, AppSourceContainer, AppSourceGit, AppSourceGitUrl, AppSourceBuilderImage, EpinioAppSource, GitAPIData
|
|
} from '../../types';
|
|
import { EpinioAppInfo } from './AppInfo.vue';
|
|
import { _EDIT } from '@shell/config/query-params';
|
|
|
|
const GIT_BASE_URL = {
|
|
[APPLICATION_SOURCE_TYPE.GIT_HUB]: 'https://github.com',
|
|
[APPLICATION_SOURCE_TYPE.GIT_LAB]: 'https://gitlab.com',
|
|
};
|
|
|
|
export const EPINIO_APP_MANIFEST = 'manifest';
|
|
|
|
interface Data {
|
|
open: boolean,
|
|
archive: AppSourceArchive,
|
|
container: AppSourceContainer,
|
|
git: AppSourceGit,
|
|
gitUrl: AppSourceGitUrl,
|
|
builderImage: AppSourceBuilderImage,
|
|
types: any[],
|
|
type: APPLICATION_SOURCE_TYPE ;// || { } from the select component
|
|
APPLICATION_SOURCE_TYPE: typeof APPLICATION_SOURCE_TYPE
|
|
}
|
|
|
|
interface FileWithRelativePath extends File {
|
|
// For some reason TS throws this as missing at transpile time .. so recreate it
|
|
readonly webkitRelativePath: string;
|
|
}
|
|
|
|
const DEFAULT_BUILD_PACK = 'paketobuildpacks/builder:full';
|
|
|
|
// Data, Methods, Computed, Props
|
|
export default Vue.extend<Data, any, any, any>({
|
|
|
|
components: {
|
|
FileSelector,
|
|
LabeledInput,
|
|
LabeledSelect,
|
|
RadioGroup,
|
|
Collapse,
|
|
GitPicker
|
|
},
|
|
|
|
props: {
|
|
application: {
|
|
type: Object as PropType<Application>,
|
|
required: true
|
|
},
|
|
source: {
|
|
type: Object as PropType<EpinioAppSource>,
|
|
default: null
|
|
},
|
|
info: {
|
|
type: Object as PropType<EpinioInfo>,
|
|
default: null
|
|
},
|
|
mode: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
},
|
|
|
|
data() {
|
|
const defaultBuilderImage = this.info?.default_builder_image || DEFAULT_BUILD_PACK;
|
|
const builderImage = this.source?.builderImage?.value || defaultBuilderImage;
|
|
|
|
const git = {
|
|
usernameOrOrg: this.source?.git?.usernameOrOrg || '',
|
|
repo: this.source?.git?.repo || '',
|
|
commit: this.source?.git?.commit || '',
|
|
branch: this.source?.git?.branch || '',
|
|
url: this.source?.git?.url || '',
|
|
sourceData: this.source?.git?.sourceData || {
|
|
repos: [],
|
|
branches: [],
|
|
commits: []
|
|
}
|
|
};
|
|
|
|
return {
|
|
open: false,
|
|
valid: this.validate(),
|
|
defaultBuilderImage,
|
|
|
|
archive: {
|
|
tarball: this.source?.archive?.tarball || '',
|
|
fileName: this.source?.archive?.fileName || '',
|
|
},
|
|
|
|
container: { url: this.source?.container?.url },
|
|
|
|
gitUrl: {
|
|
url: this.source?.gitUrl?.url || '',
|
|
branch: this.source?.gitUrl?.branch || '',
|
|
validGitUrl: false,
|
|
},
|
|
|
|
git,
|
|
|
|
initValue: {
|
|
type: this.source?.type,
|
|
selectedAccOrOrg: git.usernameOrOrg,
|
|
selectedRepo: git.repo,
|
|
selectedBranch: git.branch,
|
|
selectedCommit: { sha: git.commit },
|
|
repos: git.sourceData.repos,
|
|
branches: git.sourceData.branches,
|
|
commits: git.sourceData.commits,
|
|
},
|
|
|
|
builderImage: {
|
|
value: builderImage,
|
|
default: builderImage === defaultBuilderImage,
|
|
},
|
|
|
|
appChart: this.source?.appChart,
|
|
|
|
types: Object.values(APPLICATION_SOURCE_TYPE).map(value => ({
|
|
label: this.t(`epinio.applications.steps.source.${ value }.label`),
|
|
value
|
|
})),
|
|
|
|
type: this.source?.type || APPLICATION_SOURCE_TYPE.FOLDER,
|
|
APPLICATION_SOURCE_TYPE,
|
|
EDIT: _EDIT
|
|
};
|
|
},
|
|
mounted() {
|
|
if (!this.appChart) {
|
|
if (this.appCharts[0]?.value) {
|
|
Vue.set(this, 'appChart', this.appCharts[0]?.value);
|
|
} else {
|
|
Vue.set(this, 'appChart', this.appCharts[0]);
|
|
}
|
|
}
|
|
|
|
this.update();
|
|
},
|
|
|
|
methods: {
|
|
urlRule() {
|
|
const gitRegex = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gm;
|
|
|
|
if (!this.gitUrl.url) {
|
|
return;
|
|
}
|
|
|
|
const evalUrl = () => {
|
|
const result = gitRegex.exec(this.gitUrl.url);
|
|
|
|
if (result && this.gitUrl.url === result[0]) {
|
|
this.gitUrl.validGitUrl = true;
|
|
} else {
|
|
this.gitUrl.validGitUrl = false;
|
|
|
|
return this.t('epinio.applications.steps.source.git_url.error.label');
|
|
}
|
|
};
|
|
|
|
return evalUrl();
|
|
},
|
|
|
|
onFileSelected(file: File) {
|
|
this.archive.tarball = file;
|
|
this.archive.fileName = file.name;
|
|
|
|
this.update();
|
|
},
|
|
|
|
onManifestFileSelected(file: string) {
|
|
try {
|
|
const parsed: any = jsyaml.load(file);
|
|
|
|
if (parsed.origin?.container) {
|
|
Vue.set(this, 'type', APPLICATION_SOURCE_TYPE.CONTAINER_URL);
|
|
Vue.set(this.container, 'url', parsed.origin.container);
|
|
} else if (parsed.origin.git?.repository && parsed.origin.git?.revision) {
|
|
Vue.set(this, 'type', APPLICATION_SOURCE_TYPE.GIT_URL);
|
|
Vue.set(this.gitUrl, 'url', parsed.origin.git.repository);
|
|
Vue.set(this.gitUrl, 'branch', parsed.origin.git.revision);
|
|
}
|
|
if (parsed.configuration) {
|
|
Vue.set(this, 'appChart', parsed.configuration.appchart);
|
|
}
|
|
|
|
const appInfo: EpinioAppInfo = {
|
|
meta: {
|
|
name: parsed.name || '',
|
|
namespace: this.namespaces?.[0]?.name || ''
|
|
},
|
|
configuration: {
|
|
configurations: parsed.configuration?.configurations as string[] || [],
|
|
instances: parsed.configuration.instances || 1,
|
|
environment: parsed.configuration.environment || {},
|
|
settings: parsed.configuration?.settings || {},
|
|
routes: parsed.configuration.routes || []
|
|
}
|
|
};
|
|
|
|
this.$router.applyQuery({ from: EPINIO_APP_MANIFEST });
|
|
this.update();
|
|
this.updateAppInfo(appInfo);
|
|
this.updateConfigurations(parsed.configuration.configurations || []);
|
|
} catch (e) {
|
|
console.error('Failed to parse or process manifest: ', e); // eslint-disable-line no-console
|
|
}
|
|
},
|
|
|
|
onFolderSelected(files: FileWithRelativePath | FileWithRelativePath[]) {
|
|
const safeFiles = Array.isArray(files) ? files : [files];
|
|
let folderName: string = '';
|
|
|
|
// Determine parent folder name
|
|
for (const f of safeFiles) {
|
|
const paths = f.webkitRelativePath.split('/');
|
|
|
|
if (paths.length > 1) {
|
|
if (!folderName) {
|
|
folderName = paths[0];
|
|
continue;
|
|
}
|
|
if (folderName !== paths[0]) {
|
|
folderName = '';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const filesToZip = safeFiles.reduce((res, f) => {
|
|
let path = f.webkitRelativePath;
|
|
|
|
if (folderName) {
|
|
// Remove parent folder name
|
|
const parts = path.split('/');
|
|
|
|
parts.shift();
|
|
path = parts.join('/');
|
|
}
|
|
|
|
res[path] = f;
|
|
|
|
return res;
|
|
}, {} as { [key: string]: any});
|
|
|
|
generateZip(filesToZip).then((zip) => {
|
|
Vue.set(this.archive, 'tarball', zip);
|
|
Vue.set(this.archive, 'fileName', folderName || 'folder');
|
|
|
|
this.update();
|
|
});
|
|
},
|
|
|
|
update() {
|
|
this.$emit('change', {
|
|
type: this.type,
|
|
archive: this.archive,
|
|
container: this.container,
|
|
gitUrl: this.gitUrl,
|
|
builderImage: this.builderImage,
|
|
appChart: this.appChart,
|
|
git: this.git,
|
|
});
|
|
this.valid = this.validate();
|
|
},
|
|
|
|
updateAppInfo(info: EpinioAppInfo) {
|
|
this.$emit('changeAppInfo', info);
|
|
},
|
|
|
|
updateConfigurations(configs: string[]) {
|
|
this.$emit('changeAppConfig', configs);
|
|
},
|
|
|
|
onImageType(defaultImage: boolean) {
|
|
if (defaultImage) {
|
|
this.builderImage.value = this.defaultBuilderImage;
|
|
}
|
|
|
|
this.builderImage.default = defaultImage;
|
|
|
|
this.update();
|
|
},
|
|
gitUpdate({
|
|
repo, selectedAccOrOrg, branch, commit, sourceData
|
|
}: {
|
|
commit: string,
|
|
selectedAccOrOrg: string,
|
|
repo: { id?: string, name: string },
|
|
branch: string,
|
|
sourceData: GitAPIData
|
|
}) {
|
|
if (!!selectedAccOrOrg && !!repo && !!commit && !!branch) {
|
|
const url = `${ GIT_BASE_URL[this.type as APPLICATION_SOURCE_TYPE.GIT_HUB | APPLICATION_SOURCE_TYPE.GIT_LAB] }/${ selectedAccOrOrg }/${ repo.name }`;
|
|
|
|
this.git.usernameOrOrg = selectedAccOrOrg;
|
|
this.git.url = url;
|
|
this.git.commit = commit;
|
|
this.git.branch = branch;
|
|
this.git.repo = repo;
|
|
this.git.sourceData = sourceData;
|
|
|
|
this.update();
|
|
this.$emit('valid', true);
|
|
} else {
|
|
this.update();
|
|
this.$emit('valid', false);
|
|
}
|
|
},
|
|
|
|
validate() {
|
|
switch (this.type) {
|
|
case APPLICATION_SOURCE_TYPE.ARCHIVE:
|
|
case APPLICATION_SOURCE_TYPE.FOLDER: {
|
|
return !!this.archive.tarball && !!this.builderImage.value;
|
|
}
|
|
case APPLICATION_SOURCE_TYPE.CONTAINER_URL:
|
|
return !!this.container.url;
|
|
case APPLICATION_SOURCE_TYPE.GIT_URL:
|
|
return !!this.gitUrl.url && !!this.gitUrl.branch && !!this.builderImage.value && !!this.gitUrl.validGitUrl;
|
|
case APPLICATION_SOURCE_TYPE.GIT_HUB:
|
|
case APPLICATION_SOURCE_TYPE.GIT_LAB:
|
|
return !!this.git.usernameOrOrg && !!this.git.url && !!this.git.repo && !!this.git.branch && !!this.git.commit;
|
|
}
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
type() {
|
|
this.update();
|
|
},
|
|
valid(neu) {
|
|
this.$emit('valid', neu);
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
showBuilderImage() {
|
|
return [
|
|
APPLICATION_SOURCE_TYPE.ARCHIVE,
|
|
APPLICATION_SOURCE_TYPE.FOLDER,
|
|
APPLICATION_SOURCE_TYPE.GIT_URL,
|
|
APPLICATION_SOURCE_TYPE.GIT_HUB,
|
|
APPLICATION_SOURCE_TYPE.GIT_LAB,
|
|
].includes(this.type);
|
|
},
|
|
|
|
namespaces() {
|
|
return sortBy(this.$store.getters['epinio/all'](EPINIO_TYPES.NAMESPACE), 'name');
|
|
},
|
|
|
|
appCharts() {
|
|
return sortBy(this.$store.getters['epinio/all'](EPINIO_TYPES.APP_CHARTS), 'name').map((ap: EpinioApplicationChartResource) => ({
|
|
value: ap.meta.name,
|
|
label: `${ ap.meta.name } (${ ap.short_description })`
|
|
}));
|
|
},
|
|
|
|
sourceValue() {
|
|
return {
|
|
...this.source.git,
|
|
type: this.type
|
|
};
|
|
},
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="appSource">
|
|
<div class="button-row source">
|
|
<LabeledSelect
|
|
v-model="type"
|
|
data-testid="epinio_app-source_type"
|
|
label="Source Type"
|
|
:options="types"
|
|
:mode="mode"
|
|
:clearable="false"
|
|
/>
|
|
<FileSelector
|
|
v-tooltip="t('epinio.applications.steps.source.manifest.tooltip')"
|
|
data-testid="epinio_app-source_manifest"
|
|
class="role-tertiary add mt-5"
|
|
:label="t('epinio.applications.steps.source.manifest.button')"
|
|
:mode="mode"
|
|
:raw-data="false"
|
|
@selected="onManifestFileSelected"
|
|
/>
|
|
</div>
|
|
|
|
<template v-if="type === APPLICATION_SOURCE_TYPE.ARCHIVE">
|
|
<div class="spacer source">
|
|
<h3>{{ t('epinio.applications.steps.source.archive.file.label') }}</h3>
|
|
<div class="button-row">
|
|
<LabeledInput
|
|
v-model="archive.fileName"
|
|
data-testid="epinio_app-source_archive_name"
|
|
:disabled="true"
|
|
:tooltip="t('epinio.applications.steps.source.archive.file.tooltip')"
|
|
:label="t('epinio.applications.steps.source.archive.file.inputLabel')"
|
|
:required="true"
|
|
/>
|
|
<FileSelector
|
|
data-testid="epinio_app-source_archive_file"
|
|
class="role-tertiary add mt-5"
|
|
:label="t('epinio.applications.steps.source.archive.file.button')"
|
|
:mode="mode"
|
|
:raw-data="true"
|
|
:accept="'.zip, .tar, .gz, .bz2, .xz'"
|
|
@selected="onFileSelected"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template v-else-if="type === APPLICATION_SOURCE_TYPE.FOLDER">
|
|
<div class="spacer source">
|
|
<h3>{{ t('epinio.applications.steps.source.folder.file.label') }}</h3>
|
|
<div class="button-row">
|
|
<LabeledInput
|
|
v-model="archive.fileName"
|
|
data-testid="epinio_app-source_folder_name"
|
|
:disabled="true"
|
|
:tooltip="t('epinio.applications.steps.source.folder.file.tooltip')"
|
|
:label="t('epinio.applications.steps.source.folder.file.inputLabel')"
|
|
:required="true"
|
|
/>
|
|
<FileSelector
|
|
data-testid="epinio_app-source_folder_file"
|
|
class="role-tertiary add mt-5"
|
|
:label="t('epinio.applications.steps.source.folder.file.button')"
|
|
:mode="mode"
|
|
:raw-data="true"
|
|
:directory="true"
|
|
@selected="onFolderSelected"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template v-else-if="type === APPLICATION_SOURCE_TYPE.CONTAINER_URL">
|
|
<div class="spacer source">
|
|
<h3>{{ t('epinio.applications.steps.source.container_url.url.label') }}</h3>
|
|
<LabeledInput
|
|
v-model="container.url"
|
|
data-testid="epinio_app-source_container"
|
|
:tooltip="t('epinio.applications.steps.source.container_url.url.tooltip')"
|
|
:label="t('epinio.applications.steps.source.container_url.url.inputLabel')"
|
|
:required="true"
|
|
@input="update"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<template v-else-if="type === APPLICATION_SOURCE_TYPE.GIT_URL">
|
|
<div class="spacer source">
|
|
<h3>{{ t('epinio.applications.steps.source.git_url.url.label') }}</h3>
|
|
<LabeledInput
|
|
v-model="gitUrl.url"
|
|
v-focus
|
|
data-testid="epinio_app-source_git-url"
|
|
:tooltip="t('epinio.applications.steps.source.git_url.url.tooltip')"
|
|
:label="t('epinio.applications.steps.source.git_url.url.inputLabel')"
|
|
:placeholder="'https://github.com/{user or org}/{repository}'"
|
|
:required="true"
|
|
:rules="[urlRule]"
|
|
@delay="100"
|
|
@input="update"
|
|
/>
|
|
</div>
|
|
<div class="spacer source">
|
|
<h3>{{ t('epinio.applications.steps.source.git_url.branch.label') }}</h3>
|
|
<LabeledInput
|
|
v-model="gitUrl.branch"
|
|
data-testid="epinio_app-source_git-branch"
|
|
:tooltip="t('epinio.applications.steps.source.git_url.branch.tooltip')"
|
|
:label="t('epinio.applications.steps.source.git_url.branch.inputLabel')"
|
|
:required="true"
|
|
:disabled="!gitUrl.validGitUrl"
|
|
@input="update"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<KeepAlive>
|
|
<GitPicker
|
|
:init-value="initValue"
|
|
:value="sourceValue"
|
|
@change="gitUpdate"
|
|
/>
|
|
</KeepAlive>
|
|
</template>
|
|
<Collapse
|
|
:open.sync="open"
|
|
:title="'Advanced Settings'"
|
|
class="mt-30 mb-30 source"
|
|
>
|
|
<template>
|
|
<!-- Unable to change app chart of active app, so disable -->
|
|
<LabeledSelect
|
|
v-model="appChart"
|
|
data-testid="epinio_app-source_appchart"
|
|
:label="t('epinio.applications.steps.source.archive.appchart.label')"
|
|
:options="appCharts"
|
|
:mode="mode"
|
|
:clearable="false"
|
|
:required="true"
|
|
:tooltip="t('typeDescription.appcharts')"
|
|
:reduce="(e) => e.value"
|
|
:disabled="mode === EDIT"
|
|
@input="update"
|
|
/>
|
|
<template v-if="showBuilderImage">
|
|
<RadioGroup
|
|
class="mt-20"
|
|
name="defaultBuilderImage"
|
|
data-testid="epinio_app-source_builder-select"
|
|
:value="builderImage.default"
|
|
:labels="[t('epinio.applications.steps.source.archive.builderimage.default'), t('epinio.applications.steps.source.archive.builderimage.custom')]"
|
|
:options="[true, false]"
|
|
:label-key="'epinio.applications.steps.source.archive.builderimage.label'"
|
|
@input="onImageType"
|
|
/>
|
|
<LabeledInput
|
|
v-model="builderImage.value"
|
|
data-testid="epinio_app-source_builder-value"
|
|
:disabled="builderImage.default"
|
|
:tooltip="t('epinio.applications.steps.source.archive.builderimage.tooltip')"
|
|
:mode="mode"
|
|
@input="update"
|
|
/>
|
|
</template>
|
|
</template>
|
|
</Collapse>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.appSource {
|
|
// max-width: 920px;
|
|
|
|
.source {
|
|
max-width: 700px;
|
|
}
|
|
|
|
.button-row {
|
|
display: flex;
|
|
align-items: center;
|
|
.file-selector {
|
|
margin-top: 0 !important;
|
|
margin-left: 5px;
|
|
}
|
|
}
|
|
|
|
.collapse {
|
|
margin-left: -5px;
|
|
}
|
|
}
|
|
.archive {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
</style>
|