dashboard/pkg/epinio/components/application/AppSource.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>