Merge remote-tracking branch 'upstream/epinio-dev'

This commit is contained in:
Richard Cox 2022-06-22 13:13:51 +01:00
commit 94e15d3e52
30 changed files with 712 additions and 174 deletions

View File

@ -25,7 +25,7 @@ jobs:
- name: Install & Build - name: Install & Build
run: run:
RANCHER_ENV=epinio ./.github/workflows/scripts/build-dashboard.sh RANCHER_ENV=epinio EXCLUDES_PKG=rancher-components ./.github/workflows/scripts/build-dashboard.sh
- name: Upload Build - name: Upload Build
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2

View File

@ -4,7 +4,9 @@ set -e
echo "GITHUB_SHA: $GITHUB_SHA" echo "GITHUB_SHA: $GITHUB_SHA"
echo "GITHUB_REF_NAME: $GITHUB_REF_NAME" echo "GITHUB_REF_NAME: $GITHUB_REF_NAME"
echo "ROUTER_BASE: $ROUTER_BASE" echo "ROUTER_BASE: $ROUTER_BASE"
echo
echo "RANCHER_ENV: $RANCHER_ENV" echo "RANCHER_ENV: $RANCHER_ENV"
echo "EXCLUDES_PKG: $EXCLUDES_PKG"
echo echo
echo "RELEASE_DIR: $RELEASE_DIR" echo "RELEASE_DIR: $RELEASE_DIR"
RELEASE_LOCATION="$RELEASE_DIR/$ARTIFACT_NAME" RELEASE_LOCATION="$RELEASE_DIR/$ARTIFACT_NAME"
@ -26,7 +28,7 @@ echo Installing dependencies
yarn install --frozen-lockfile yarn install --frozen-lockfile
echo Building echo Building
NUXT_ENV_commit=$GITHUB_SHA NUXT_ENV_version=$GITHUB_REF_NAME OUTPUT_DIR="$ARTIFACT_LOCATION" ROUTER_BASE="$ROUTER_BASE" RANCHER_ENV=$RANCHER_ENV API=$API RESOURCE_BASE=$RESOURCE_BASE yarn run build --spa NUXT_ENV_commit=$GITHUB_SHA NUXT_ENV_version=$GITHUB_REF_NAME OUTPUT_DIR="$ARTIFACT_LOCATION" ROUTER_BASE="$ROUTER_BASE" RANCHER_ENV=$RANCHER_ENV API=$API RESOURCE_BASE=$RESOURCE_BASE EXCLUDES_PKG=$EXCLUDES_PKG yarn run build --spa
echo Creating tar echo Creating tar
tar -czf $RELEASE_LOCATION.tar.gz -C $ARTIFACT_LOCATION . tar -czf $RELEASE_LOCATION.tar.gz -C $ARTIFACT_LOCATION .

View File

@ -0,0 +1,57 @@
<script>
import { EPINIO_TYPES } from '@/pkg/epinio/types';
import { createEpinioRoute } from '@/pkg/epinio/utils/custom-routing';
export default {
name: 'EpinioIntro',
data() {
return {
getStartedLink: createEpinioRoute(`c-cluster-resource`, {
cluster: this.$store.getters['clusterId'],
resource: EPINIO_TYPES.NAMESPACE,
})
};
},
};
</script>
<template>
<div class="epinio-intro">
<i class="icon-epinio mb-30" />
<h1>{{ t('epinio.intro.welcome') }}</h1>
<p class="mb-30">
<span>{{ t('epinio.intro.blurb') }}</span>
<a :href="t('epinio.intro.learnMoreLink')" target="_blank" rel="noopener noreferrer nofollow">
{{ t('epinio.intro.learnMore') }} <i class="icon icon-external-link" />
</a>
</p>
<h3 class="mb-30">
{{ t('epinio.intro.noNamespaces', null, true) }}
</h3>
<n-link
:to="getStartedLink"
class="btn role-secondary"
>
{{ t('epinio.intro.getStarted') }}
</n-link>
</div>
</template>
<style lang="scss" scoped>
.epinio-intro {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
min-height: 100%;
.icon-epinio {
font-size: 100px;
}
> p > span {
color: var(--disabled-text);
}
}
</style>

View File

@ -5,10 +5,11 @@ import NameNsDescription from '@shell/components/form/NameNsDescription.vue';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue'; import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import KeyValue from '@shell/components/form/KeyValue.vue'; import KeyValue from '@shell/components/form/KeyValue.vue';
import ArrayList from '@shell/components/form/ArrayList.vue'; import ArrayList from '@shell/components/form/ArrayList.vue';
import Banner from '@components/Banner/Banner.vue'; import Loading from '@shell/components/Loading.vue';
import { EPINIO_TYPES } from '../../types'; import { EPINIO_TYPES } from '../../types';
import { sortBy } from '@shell/utils/sort'; import { sortBy } from '@shell/utils/sort';
import { validateKubernetesName } from '@shell/utils/validators/kubernetes-name';
export interface EpinioAppInfo { export interface EpinioAppInfo {
meta: { meta: {
@ -24,7 +25,7 @@ export interface EpinioAppInfo {
interface Data { interface Data {
errors: string[], errors: string[],
values: EpinioAppInfo values?: EpinioAppInfo
} }
// Data, Methods, Computed, Props // Data, Methods, Computed, Props
@ -35,7 +36,7 @@ export default Vue.extend<Data, any, any, any>({
NameNsDescription, NameNsDescription,
LabeledInput, LabeledInput,
KeyValue, KeyValue,
Banner Loading
}, },
props: { props: {
@ -52,17 +53,7 @@ export default Vue.extend<Data, any, any, any>({
data() { data() {
return { return {
errors: [], errors: [],
values: { values: undefined
meta: {
name: this.application.meta?.name,
namespace: this.application.meta?.namespace
},
configuration: {
instances: this.application.configuration?.instances || 1,
environment: this.application.configuration?.environment || {},
routes: this.application.configuration?.routes || [],
},
}
}; };
}, },
@ -70,7 +61,7 @@ export default Vue.extend<Data, any, any, any>({
this.values = { this.values = {
meta: { meta: {
name: this.application.meta?.name, name: this.application.meta?.name,
namespace: this.application.meta?.namespace namespace: this.application.meta?.namespace || this.namespaces[0]?.metadata.name
}, },
configuration: { configuration: {
instances: this.application.configuration?.instances || 1, instances: this.application.configuration?.instances || 1,
@ -105,8 +96,13 @@ export default Vue.extend<Data, any, any, any>({
}, },
valid() { valid() {
if (!this.values) {
return false;
}
const validName = !!this.values.meta?.name; const validName = !!this.values.meta?.name;
const validNamespace = !!this.values.meta?.namespace;
const nsErrors = validateKubernetesName(this.values.meta?.namespace || '', '', this.$store.getters, undefined, []);
const validNamespace = nsErrors.length === 0;
const validInstances = typeof this.values.configuration?.instances !== 'string' && this.values.configuration?.instances >= 0; const validInstances = typeof this.values.configuration?.instances !== 'string' && this.values.configuration?.instances >= 0;
return validName && validNamespace && validInstances; return validName && validNamespace && validInstances;
@ -127,11 +123,7 @@ export default Vue.extend<Data, any, any, any>({
</script> </script>
<template> <template>
<div v-if="!namespaces.length"> <Loading v-if="!values" />
<Banner color="warning">
{{ t('epinio.warnings.noNamespace') }}
</Banner>
</div>
<div v-else> <div v-else>
<div class="col"> <div class="col">
<NameNsDescription <NameNsDescription
@ -143,6 +135,7 @@ export default Vue.extend<Data, any, any, any>({
:value="values.meta" :value="values.meta"
:mode="mode" :mode="mode"
@change="update" @change="update"
@createNamespace="ns => values.meta.namespace = ns"
/> />
</div> </div>
<div class="col span-6"> <div class="col span-6">

View File

@ -9,6 +9,7 @@ import { STATE, DESCRIPTION } from '@shell/config/table-headers';
import { EPINIO_TYPES, APPLICATION_ACTION_STATE, APPLICATION_SOURCE_TYPE, EpinioApplication } from '../../types'; import { EPINIO_TYPES, APPLICATION_ACTION_STATE, APPLICATION_SOURCE_TYPE, EpinioApplication } from '../../types';
import { EpinioAppSource } from '../../components/application/AppSource.vue'; import { EpinioAppSource } from '../../components/application/AppSource.vue';
import { EpinioAppBindings } from '../../components/application/AppConfiguration.vue'; import { EpinioAppBindings } from '../../components/application/AppConfiguration.vue';
import EpinioNamespace from '~/pkg/epinio/models/namespaces';
interface Data { interface Data {
running: boolean; running: boolean;
@ -35,7 +36,7 @@ export default Vue.extend<Data, any, any, any>({
}, },
bindings: { bindings: {
type: Object as PropType<EpinioAppBindings>, type: Object as PropType<EpinioAppBindings>,
required: true default: () => null
}, },
mode: { mode: {
type: String, type: String,
@ -48,22 +49,34 @@ export default Vue.extend<Data, any, any, any>({
}, },
async fetch() { async fetch() {
const coreArgs = { const coreArgs: Partial<ApplicationAction & {
application: EpinioApplication,
bindings: EpinioAppBindings,
type: string,
}> = {
application: this.application, application: this.application,
bindings: this.bindings, bindings: this.bindings,
type: EPINIO_TYPES.APP_ACTION, type: EPINIO_TYPES.APP_ACTION,
}; };
if (!this.namespaces.find((ns: EpinioNamespace) => ns.name === coreArgs.application?.meta.namespace)) {
this.actions.push(await this.$store.dispatch('epinio/create', {
action: APPLICATION_ACTION_TYPE.CREATE_NS,
index: 0, // index used for sorting
...coreArgs,
}));
}
this.actions.push(await this.$store.dispatch('epinio/create', { this.actions.push(await this.$store.dispatch('epinio/create', {
action: APPLICATION_ACTION_TYPE.CREATE, action: APPLICATION_ACTION_TYPE.CREATE,
index: 0, // index used for sorting index: 1, // index used for sorting
...coreArgs, ...coreArgs,
})); }));
if (this.bindings?.configurations?.length) { if (this.bindings?.configurations?.length) {
this.actions.push(await this.$store.dispatch('epinio/create', { this.actions.push(await this.$store.dispatch('epinio/create', {
action: APPLICATION_ACTION_TYPE.BIND_CONFIGURATIONS, action: APPLICATION_ACTION_TYPE.BIND_CONFIGURATIONS,
index: 1, index: 2,
...coreArgs, ...coreArgs,
})); }));
} }
@ -71,7 +84,7 @@ export default Vue.extend<Data, any, any, any>({
if (this.bindings?.services?.length) { if (this.bindings?.services?.length) {
this.actions.push(await this.$store.dispatch('epinio/create', { this.actions.push(await this.$store.dispatch('epinio/create', {
action: APPLICATION_ACTION_TYPE.BIND_SERVICES, action: APPLICATION_ACTION_TYPE.BIND_SERVICES,
index: 2, index: 3,
...coreArgs, ...coreArgs,
})); }));
} }
@ -80,7 +93,7 @@ export default Vue.extend<Data, any, any, any>({
this.source.type === APPLICATION_SOURCE_TYPE.FOLDER) { this.source.type === APPLICATION_SOURCE_TYPE.FOLDER) {
this.actions.push(await this.$store.dispatch('epinio/create', { this.actions.push(await this.$store.dispatch('epinio/create', {
action: APPLICATION_ACTION_TYPE.UPLOAD, action: APPLICATION_ACTION_TYPE.UPLOAD,
index: 3, index: 4,
...coreArgs, ...coreArgs,
})); }));
} }
@ -88,7 +101,7 @@ export default Vue.extend<Data, any, any, any>({
if (this.source.type === APPLICATION_SOURCE_TYPE.GIT_URL) { if (this.source.type === APPLICATION_SOURCE_TYPE.GIT_URL) {
this.actions.push(await this.$store.dispatch('epinio/create', { this.actions.push(await this.$store.dispatch('epinio/create', {
action: APPLICATION_ACTION_TYPE.GIT_FETCH, action: APPLICATION_ACTION_TYPE.GIT_FETCH,
index: 3, index: 4,
...coreArgs, ...coreArgs,
})); }));
} }
@ -98,14 +111,14 @@ export default Vue.extend<Data, any, any, any>({
this.source.type === APPLICATION_SOURCE_TYPE.GIT_URL) { this.source.type === APPLICATION_SOURCE_TYPE.GIT_URL) {
this.actions.push(await this.$store.dispatch('epinio/create', { this.actions.push(await this.$store.dispatch('epinio/create', {
action: APPLICATION_ACTION_TYPE.BUILD, action: APPLICATION_ACTION_TYPE.BUILD,
index: 4, index: 5,
...coreArgs, ...coreArgs,
})); }));
} }
this.actions.push(await this.$store.dispatch('epinio/create', { this.actions.push(await this.$store.dispatch('epinio/create', {
action: APPLICATION_ACTION_TYPE.DEPLOY, action: APPLICATION_ACTION_TYPE.DEPLOY,
index: 5, index: 6,
...coreArgs, ...coreArgs,
})); }));
@ -121,7 +134,7 @@ export default Vue.extend<Data, any, any, any>({
labelKey: 'epinio.applications.steps.progress.table.stage.label', labelKey: 'epinio.applications.steps.progress.table.stage.label',
value: 'name', value: 'name',
sort: ['index'], sort: ['index'],
width: 100, width: 150,
}, },
{ {
...DESCRIPTION, ...DESCRIPTION,
@ -144,7 +157,11 @@ export default Vue.extend<Data, any, any, any>({
computed: { computed: {
actionsToRun() { actionsToRun() {
return this.actions.filter((action: ApplicationAction) => action.run); return this.actions.filter((action: ApplicationAction) => action.run);
} },
namespaces() {
return this.$store.getters['epinio/all'](EPINIO_TYPES.NAMESPACE);
},
}, },
watch: { watch: {

View File

@ -10,8 +10,9 @@ import FileSelector from '@shell/components/form/FileSelector.vue';
import RadioGroup from '@components/Form/Radio/RadioGroup.vue'; import RadioGroup from '@components/Form/Radio/RadioGroup.vue';
import { sortBy } from '@shell/utils/sort'; import { sortBy } from '@shell/utils/sort';
import { generateZip } from '@shell/utils/download'; import { generateZip } from '@shell/utils/download';
import Collapse from '@shell/components/Collapse.vue';
import { APPLICATION_SOURCE_TYPE, EPINIO_TYPES } from '../../types'; import { APPLICATION_SOURCE_TYPE, EpinioApplicationChartResource, EPINIO_TYPES } from '../../types';
import { EpinioAppInfo } from './AppInfo.vue'; import { EpinioAppInfo } from './AppInfo.vue';
interface Archive{ interface Archive{
@ -34,6 +35,7 @@ interface BuilderImage {
} }
interface Data { interface Data {
open: boolean,
archive: Archive, archive: Archive,
container: Container, container: Container,
gitUrl: GitUrl, gitUrl: GitUrl,
@ -49,6 +51,12 @@ export interface EpinioAppSource {
container: Container, container: Container,
gitUrl: GitUrl, gitUrl: GitUrl,
builderImage: BuilderImage, builderImage: BuilderImage,
appChart: string,
}
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'; const DEFAULT_BUILD_PACK = 'paketobuildpacks/builder:full';
@ -60,7 +68,8 @@ export default Vue.extend<Data, any, any, any>({
FileSelector, FileSelector,
LabeledInput, LabeledInput,
LabeledSelect, LabeledSelect,
RadioGroup RadioGroup,
Collapse
}, },
props: { props: {
@ -80,6 +89,8 @@ export default Vue.extend<Data, any, any, any>({
data() { data() {
return { return {
open: false,
archive: { archive: {
tarball: this.source?.archive.tarball || '', tarball: this.source?.archive.tarball || '',
fileName: this.source?.archive.fileName || '', fileName: this.source?.archive.fileName || '',
@ -97,6 +108,8 @@ export default Vue.extend<Data, any, any, any>({
default: this.source?.builderImage?.default !== undefined ? this.source.builderImage.default : true, default: this.source?.builderImage?.default !== undefined ? this.source.builderImage.default : true,
}, },
appChart: this.source?.appChart,
types: [{ types: [{
label: this.t('epinio.applications.steps.source.archive.label'), label: this.t('epinio.applications.steps.source.archive.label'),
value: APPLICATION_SOURCE_TYPE.ARCHIVE value: APPLICATION_SOURCE_TYPE.ARCHIVE
@ -116,6 +129,9 @@ export default Vue.extend<Data, any, any, any>({
}, },
mounted() { mounted() {
if (!this.appChart) {
Vue.set(this, 'appChart', this.appCharts[0].value);
}
this.update(); this.update();
}, },
@ -139,6 +155,9 @@ export default Vue.extend<Data, any, any, any>({
Vue.set(this.gitUrl, 'url', parsed.origin.git.url); Vue.set(this.gitUrl, 'url', parsed.origin.git.url);
Vue.set(this.gitUrl, 'branch', parsed.origin.git.revision); Vue.set(this.gitUrl, 'branch', parsed.origin.git.revision);
} }
if (parsed.configuration) {
Vue.set(this, 'appChart', parsed.configuration.appchart);
}
const appInfo: EpinioAppInfo = { const appInfo: EpinioAppInfo = {
meta: { meta: {
@ -160,11 +179,12 @@ export default Vue.extend<Data, any, any, any>({
} }
}, },
onFolderSelected(files: any[]) { onFolderSelected(files: FileWithRelativePath | FileWithRelativePath[]) {
const safeFiles = Array.isArray(files) ? files : [files];
let folderName: string = ''; let folderName: string = '';
// Determine parent folder name // Determine parent folder name
for (const f of files) { for (const f of safeFiles) {
const paths = f.webkitRelativePath.split('/'); const paths = f.webkitRelativePath.split('/');
if (paths.length > 1) { if (paths.length > 1) {
@ -179,7 +199,7 @@ export default Vue.extend<Data, any, any, any>({
} }
} }
const filesToZip = files.reduce((res, f) => { const filesToZip = safeFiles.reduce((res, f) => {
let path = f.webkitRelativePath; let path = f.webkitRelativePath;
if (folderName) { if (folderName) {
@ -211,7 +231,8 @@ export default Vue.extend<Data, any, any, any>({
archive: this.archive, archive: this.archive,
container: this.container, container: this.container,
gitUrl: this.gitUrl, gitUrl: this.gitUrl,
builderImage: this.builderImage builderImage: this.builderImage,
appChart: this.appChart
}); });
}, },
@ -271,6 +292,13 @@ export default Vue.extend<Data, any, any, any>({
return sortBy(this.$store.getters['epinio/all'](EPINIO_TYPES.NAMESPACE), 'name'); 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 })`
}));
},
type() { type() {
// There's a bug in the select component which fires off the option ({ value, label}) instead of the value // There's a bug in the select component which fires off the option ({ value, label}) instead of the value
// (possibly `reduce` related). This the workaround // (possibly `reduce` related). This the workaround
@ -387,27 +415,42 @@ export default Vue.extend<Data, any, any, any>({
/> />
</div> </div>
</template> </template>
<template v-if="showBuilderImage"> <Collapse :open.sync="open" :title="'Advanced Settings'" class="mt-30">
<div class="spacer"> <template>
<RadioGroup <LabeledSelect
name="defaultBuilderImage" v-model="appChart"
data-testid="epinio_app-source_builder-select" data-testid="epinio_app-source_appchart"
:value="builderImage.default" label="Application Chart"
:labels="[t('epinio.applications.steps.source.archive.builderimage.default'), t('epinio.applications.steps.source.archive.builderimage.custom')]" :options="appCharts"
: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" :mode="mode"
:clearable="false"
:required="true"
:tooltip="t('typeDescription.appcharts')"
:reduce="(e) => e.value"
@input="update" @input="update"
/> />
</div> <template v-if="showBuilderImage">
</template> <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> </div>
</template> </template>
@ -423,6 +466,10 @@ export default Vue.extend<Data, any, any, any>({
margin-left: 5px; margin-left: 5px;
} }
} }
.collapse {
margin-left: -5px;
}
} }
.archive { .archive {
display: flex; display: flex;

View File

@ -71,7 +71,6 @@ export function init($plugin: any, store: any) {
configureType(EPINIO_TYPES.INSTANCE, { customRoute: createEpinioRoute('c-cluster-resource', { resource: EPINIO_TYPES.INSTANCE }) }); configureType(EPINIO_TYPES.INSTANCE, { customRoute: createEpinioRoute('c-cluster-resource', { resource: EPINIO_TYPES.INSTANCE }) });
// App resource // App resource
weightType(EPINIO_TYPES.APP, 300, true);
configureType(EPINIO_TYPES.APP, { configureType(EPINIO_TYPES.APP, {
isCreatable: true, isCreatable: true,
isEditable: true, isEditable: true,
@ -81,8 +80,17 @@ export function init($plugin: any, store: any) {
customRoute: createEpinioRoute('c-cluster-applications', { }), customRoute: createEpinioRoute('c-cluster-applications', { }),
}); });
// App Chart resource
configureType(EPINIO_TYPES.APP_CHARTS, {
isCreatable: false,
isEditable: false,
isRemovable: false,
showState: false,
canYaml: false,
customRoute: createEpinioRoute('c-cluster-resource', { resource: EPINIO_TYPES.APP_CHARTS }),
});
// Configuration resource // Configuration resource
weightType(EPINIO_TYPES.CONFIGURATION, 200, true);
configureType(EPINIO_TYPES.CONFIGURATION, { configureType(EPINIO_TYPES.CONFIGURATION, {
isCreatable: true, isCreatable: true,
isEditable: true, isEditable: true,
@ -92,12 +100,11 @@ export function init($plugin: any, store: any) {
customRoute: createEpinioRoute('c-cluster-resource', { resource: EPINIO_TYPES.CONFIGURATION }), customRoute: createEpinioRoute('c-cluster-resource', { resource: EPINIO_TYPES.CONFIGURATION }),
}); });
// Groups
const ADVANCED_GROUP = 'Advanced';
const SERVICE_GROUP = 'Services'; const SERVICE_GROUP = 'Services';
weightGroup(SERVICE_GROUP, 1, true);
// Service Instance // Service Instance
weightType(EPINIO_TYPES.SERVICE_INSTANCE, 151, true);
configureType(EPINIO_TYPES.SERVICE_INSTANCE, { configureType(EPINIO_TYPES.SERVICE_INSTANCE, {
isCreatable: true, isCreatable: true,
isEditable: true, isEditable: true,
@ -108,7 +115,6 @@ export function init($plugin: any, store: any) {
}); });
// Catalog Service // Catalog Service
weightType(EPINIO_TYPES.CATALOG_SERVICE, 150, true);
configureType(EPINIO_TYPES.CATALOG_SERVICE, { configureType(EPINIO_TYPES.CATALOG_SERVICE, {
isCreatable: false, isCreatable: false,
isEditable: false, isEditable: false,
@ -119,7 +125,6 @@ export function init($plugin: any, store: any) {
}); });
// Namespace resource // Namespace resource
weightType(EPINIO_TYPES.NAMESPACE, 100, true);
configureType(EPINIO_TYPES.NAMESPACE, { configureType(EPINIO_TYPES.NAMESPACE, {
isCreatable: true, isCreatable: true,
isEditable: true, isEditable: true,
@ -130,16 +135,30 @@ export function init($plugin: any, store: any) {
showListMasthead: false // Disable default masthead because we provide a custom one. showListMasthead: false // Disable default masthead because we provide a custom one.
}); });
// Side Nav
weightType(EPINIO_TYPES.CATALOG_SERVICE, 150, true);
weightType(EPINIO_TYPES.SERVICE_INSTANCE, 151, true);
basicType([ basicType([
EPINIO_TYPES.SERVICE_INSTANCE, EPINIO_TYPES.SERVICE_INSTANCE,
EPINIO_TYPES.CATALOG_SERVICE, EPINIO_TYPES.CATALOG_SERVICE,
], SERVICE_GROUP); ], SERVICE_GROUP);
weightType(EPINIO_TYPES.CONFIGURATION, 200, true);
weightType(EPINIO_TYPES.APP_CHARTS, 150, true);
basicType([
EPINIO_TYPES.CONFIGURATION,
EPINIO_TYPES.APP_CHARTS
], ADVANCED_GROUP);
weightType(EPINIO_TYPES.APP, 300, true);
weightGroup(SERVICE_GROUP, 2, true);
weightType(EPINIO_TYPES.NAMESPACE, 100, true);
weightGroup(ADVANCED_GROUP, 1, true);
basicType([ basicType([
EPINIO_TYPES.APP, EPINIO_TYPES.APP,
SERVICE_GROUP, SERVICE_GROUP,
EPINIO_TYPES.NAMESPACE, EPINIO_TYPES.NAMESPACE,
EPINIO_TYPES.CONFIGURATION ADVANCED_GROUP
]); ]);
headers(EPINIO_TYPES.APP, [ headers(EPINIO_TYPES.APP, [
@ -150,8 +169,6 @@ export function init($plugin: any, store: any) {
labelKey: 'epinio.tableHeaders.namespace', labelKey: 'epinio.tableHeaders.namespace',
value: 'meta.namespace', value: 'meta.namespace',
sort: ['meta.namespace'], sort: ['meta.namespace'],
formatter: 'LinkDetail',
formatterOpts: { reference: 'nsLocation' }
}, },
{ {
name: 'dep-status', name: 'dep-status',
@ -266,8 +283,6 @@ export function init($plugin: any, store: any) {
labelKey: 'epinio.tableHeaders.namespace', labelKey: 'epinio.tableHeaders.namespace',
value: 'meta.namespace', value: 'meta.namespace',
sort: ['meta.namespace'], sort: ['meta.namespace'],
formatter: 'LinkDetail',
formatterOpts: { reference: 'nsLocation' }
}, },
{ {
name: 'boundApps', name: 'boundApps',
@ -298,20 +313,26 @@ export function init($plugin: any, store: any) {
headers(EPINIO_TYPES.SERVICE_INSTANCE, [ headers(EPINIO_TYPES.SERVICE_INSTANCE, [
STATE, STATE,
SIMPLE_NAME, NAME,
{ {
name: 'namespace', name: 'namespace',
labelKey: 'epinio.tableHeaders.namespace', labelKey: 'epinio.tableHeaders.namespace',
value: 'metadata.namespace', value: 'metadata.namespace',
sort: ['metadata.namespace'], sort: ['metadata.namespace'],
formatter: 'LinkDetail',
formatterOpts: { reference: 'nsLocation' }
}, },
{ // This will be a link once the service instance detail / create / edit pages are created {
name: 'catalog_service', name: 'catalog_service',
labelKey: 'epinio.serviceInstance.tableHeaders.service', labelKey: 'epinio.serviceInstance.tableHeaders.service',
value: 'catalog_service', value: 'catalog_service',
sort: ['catalog_service'], sort: ['catalog_service'],
formatter: 'LinkDetail',
formatterOpts: { reference: 'serviceLocation' }
},
{
name: 'catalog_service_version',
labelKey: 'epinio.serviceInstance.tableHeaders.serviceVersion',
value: 'catalog_service_version',
sort: ['catalog_service_version'],
}, },
{ {
name: 'boundApps', name: 'boundApps',
@ -338,4 +359,21 @@ export function init($plugin: any, store: any) {
}, },
AGE AGE
]); ]);
headers(EPINIO_TYPES.APP_CHARTS, [
SIMPLE_NAME,
{
name: 'description',
labelKey: 'epinio.catalogService.tableHeaders.desc',
value: 'description',
sort: ['description'],
},
{
name: 'helm_chart',
label: 'Helm Chart',
value: 'helm_chart',
sort: ['helm_chart'],
},
AGE
]);
} }

View File

@ -8,6 +8,8 @@ import ResourceTable from '@shell/components/ResourceTable.vue';
import PlusMinus from '@shell/components/form/PlusMinus.vue'; import PlusMinus from '@shell/components/form/PlusMinus.vue';
import { epinioExceptionToErrorsArray } from '../utils/errors'; import { epinioExceptionToErrorsArray } from '../utils/errors';
import ApplicationCard from '@/shell/components/cards/ApplicationCard.vue'; import ApplicationCard from '@/shell/components/cards/ApplicationCard.vue';
import Tabbed from '@shell/components/Tabbed/index.vue';
import Tab from '@shell/components/Tabbed/Tab.vue';
interface Data { interface Data {
} }
@ -19,7 +21,9 @@ export default Vue.extend<Data, any, any, any>({
ConsumptionGauge, ConsumptionGauge,
ResourceTable, ResourceTable,
PlusMinus, PlusMinus,
ApplicationCard ApplicationCard,
Tabbed,
Tab,
}, },
props: { props: {
value: { value: {
@ -35,12 +39,35 @@ export default Vue.extend<Data, any, any, any>({
required: true required: true
}, },
}, },
fetch() {
this.$store.dispatch(`epinio/findAll`, { type: EPINIO_TYPES.SERVICE_INSTANCE });
this.$store.dispatch(`epinio/findAll`, { type: EPINIO_TYPES.CONFIGURATION });
},
data() { data() {
const appInstanceSchema = this.$store.getters[`${ EPINIO_PRODUCT_NAME }/schemaFor`](EPINIO_TYPES.APP_INSTANCE);
const servicesSchema = this.$store.getters[`${ EPINIO_PRODUCT_NAME }/schemaFor`](EPINIO_TYPES.SERVICE_INSTANCE);
const servicesHeaders: [] = this.$store.getters['type-map/headersFor'](servicesSchema);
const configsSchema = this.$store.getters[`${ EPINIO_PRODUCT_NAME }/schemaFor`](EPINIO_TYPES.CONFIGURATION);
const configsHeaders: [] = this.$store.getters['type-map/headersFor'](configsSchema);
return { return {
appInstanceSchema: this.$store.getters[`${ EPINIO_PRODUCT_NAME }/schemaFor`](EPINIO_TYPES.APP_INSTANCE),
saving: false, saving: false,
appInstance: {
schema: appInstanceSchema,
headers: this.$store.getters['type-map/headersFor'](appInstanceSchema),
},
services: {
schema: servicesSchema,
headers: servicesHeaders.filter((h: any) => !['namespace', 'boundApps'].includes(h.name)),
},
configs: {
schema: configsSchema,
headers: configsHeaders.filter((h: any) => !['namespace', 'boundApps', 'service'].includes(h.name)),
}
}; };
}, },
methods: { methods: {
async updateInstances(newInstances: number) { async updateInstances(newInstances: number) {
this.$set(this, 'saving', true); this.$set(this, 'saving', true);
@ -58,6 +85,12 @@ export default Vue.extend<Data, any, any, any>({
return `${ matchGithub?.[4] }/${ matchGithub?.[5] }`; return `${ matchGithub?.[4] }/${ matchGithub?.[5] }`;
} }
},
computed: {
sourceIcon(): string {
return this.value.sourceInfo?.icon || 'icon-epinio';
}
} }
}); });
</script> </script>
@ -68,7 +101,7 @@ export default Vue.extend<Data, any, any, any>({
<ApplicationCard> <ApplicationCard>
<!-- Icon slot --> <!-- Icon slot -->
<template v-slot:cardIcon> <template v-slot:cardIcon>
<i class="icon icon-fw" :class="value.sourceInfo.icon || icon-epinio"></i> <i class="icon icon-fw" :class="sourceIcon"></i>
</template> </template>
<!-- Routes links slot --> <!-- Routes links slot -->
@ -87,25 +120,24 @@ export default Vue.extend<Data, any, any, any>({
<!-- Resources count slot --> <!-- Resources count slot -->
<template v-slot:resourcesCount> <template v-slot:resourcesCount>
<!-- // TODO: Depends on https://github.com/epinio/epinio/issues/1471 -->
<!-- <div>
{{ value.configCount }} {{ t('epinio.applications.detail.counts.config') }}
</div> -->
<div>
{{ value.configCount }} {{ t('epinio.applications.detail.counts.config') }}
</div>
<div> <div>
{{ value.envCount }} {{ t('epinio.applications.detail.counts.envVars') }} {{ value.envCount }} {{ t('epinio.applications.detail.counts.envVars') }}
</div> </div>
<div>
{{ value.serviceConfigurations.length }} {{ t('epinio.applications.detail.counts.services') }}
</div>
<div>
{{ value.baseConfigurations.length }} {{ t('epinio.applications.detail.counts.config') }}
</div>
</template> </template>
</ApplicationCard> </ApplicationCard>
</div> </div>
<h3 v-if="value.deployment" class="mt-20 mb-20"> <h3 v-if="value.deployment" class="mt-20">
{{ t('epinio.applications.detail.deployment.label') }} {{ t('epinio.applications.detail.deployment.label') }}
</h3> </h3>
<div class="deployment"> <div v-if="value.deployment" class="deployment">
<div class="simple-box-row app-instances"> <div class="simple-box-row app-instances">
<SimpleBox> <SimpleBox>
<ConsumptionGauge <ConsumptionGauge
@ -165,28 +197,41 @@ export default Vue.extend<Data, any, any, any>({
</thead> </thead>
<tr> <tr>
<td>{{ t('tableHeaders.memory') }}</td> <td>{{ t('tableHeaders.memory') }}</td>
<td>{{ value.instanceMemory.min }} MiB</td> <td>{{ value.instanceMemory.min }}</td>
<td>{{ value.instanceMemory.max }} MiB</td> <td>{{ value.instanceMemory.max }}</td>
<td>{{ value.instanceMemory.avg }} MiB</td> <td>{{ value.instanceMemory.avg }}</td>
</tr> </tr>
<tr> <tr>
<td>{{ t('tableHeaders.cpu') }}</td> <td>{{ t('tableHeaders.cpu') }}</td>
<td>{{ value.instanceCpu.min }} m</td> <td>{{ value.instanceCpu.min }}</td>
<td>{{ value.instanceCpu.max }} m</td> <td>{{ value.instanceCpu.max }}</td>
<td>{{ value.instanceCpu.avg }} m</td> <td>{{ value.instanceCpu.avg }}</td>
</tr> </tr>
</table> </table>
</div> </div>
</SimpleBox> </SimpleBox>
</div> </div>
</div> </div>
<h3 class="mt-20">
{{ t('epinio.applications.detail.tables.label') }}
</h3>
<div> <div>
<!-- //TODO: Add Services & Configurations as tabs --> <Tabbed>
<ResourceTable :schema="appInstanceSchema" :rows="value.instances" :table-actions="false"> <Tab label-key="epinio.applications.detail.tables.instances" name="instances" :weight="3">
<template #header-left> <ResourceTable :schema="appInstance.schema" :headers="appInstance.headers" :rows="value.instances" :table-actions="false">
<h1>Instances</h1> </ResourceTable>
</template> </Tab>
</ResourceTable> <Tab label-key="epinio.applications.detail.tables.services" name="services" :weight="2">
<ResourceTable :schema="services.schema" :headers="services.headers" :rows="value.services" :namespaced="false" :table-actions="false">
</ResourceTable>
</Tab>
<Tab label-key="epinio.applications.detail.tables.configs" name="configs" :weight="1">
<ResourceTable :schema="configs.schema" :headers="configs.headers" :rows="value.baseConfigurations" :namespaced="false" :table-actions="false">
</ResourceTable>
</Tab>
</Tabbed>
</div> </div>
</div> </div>
</template> </template>
@ -292,7 +337,6 @@ export default Vue.extend<Data, any, any, any>({
} }
.deployment { .deployment {
margin-bottom: 60px;
.simple-box { .simple-box {
width: 100%; width: 100%;
margin-bottom: 0; margin-bottom: 0;

View File

@ -0,0 +1,49 @@
<script lang="ts">
import Vue, { PropType } from 'vue';
import EpinioCatalogServiceModel from '~/pkg/epinio/models/catalogservices';
import { EPINIO_PRODUCT_NAME, EPINIO_TYPES } from '../types';
import ResourceTable from '@shell/components/ResourceTable.vue';
interface Data {
}
export default Vue.extend<Data, any, any, any>({
components: { ResourceTable },
props: {
value: {
type: Object as PropType<EpinioCatalogServiceModel>,
required: true
},
},
async fetch() {
await this.$store.dispatch(`epinio/findAll`, { type: EPINIO_TYPES.SERVICE_INSTANCE });
},
data() {
const servicesSchema = this.$store.getters[`${ EPINIO_PRODUCT_NAME }/schemaFor`](EPINIO_TYPES.SERVICE_INSTANCE);
const servicesHeaders: [] = this.$store.getters['type-map/headersFor'](servicesSchema);
return {
servicesSchema,
servicesHeaders
};
},
});
</script>
<template>
<div>
<h2 class="mt-20">
{{ t('epinio.catalogService.detail.servicesTitle', { catalogService: value.name }) }}
</h2>
<ResourceTable
:schema="servicesSchema"
:rows="value.services"
:loading="$fetchState.pending"
:headers="servicesHeaders"
/>
</div>
</template>

View File

@ -48,7 +48,7 @@ export default Vue.extend<Data, any, any, any>({
async fetch() { async fetch() {
await this.mixinFetch(); await this.mixinFetch();
Vue.set(this.value.meta, 'namespace', this.initialValue.meta.namespace || this.namespaces[0].metadata.name); Vue.set(this.value.meta, 'namespace', this.initialValue.meta.namespace || this.namespaces[0]?.metadata.name);
this.selectedApps = [...this.initialValue.configuration?.boundapps || []]; this.selectedApps = [...this.initialValue.configuration?.boundapps || []];
}, },
@ -105,8 +105,9 @@ export default Vue.extend<Data, any, any, any>({
updateValidation() { updateValidation() {
const nameErrors = validateKubernetesName(this.value?.meta.name || '', this.t('epinio.namespace.name'), this.$store.getters, undefined, []); const nameErrors = validateKubernetesName(this.value?.meta.name || '', this.t('epinio.namespace.name'), this.$store.getters, undefined, []);
const nsErrors = validateKubernetesName(this.value?.meta.namespace || '', '', this.$store.getters, undefined, []);
if (nameErrors.length === 0) { if (nameErrors.length === 0 && nsErrors.length === 0) {
const dataValues = Object.entries(this.value?.data || {}); const dataValues = Object.entries(this.value?.data || {});
if (!!dataValues.length) { if (!!dataValues.length) {
@ -124,6 +125,7 @@ export default Vue.extend<Data, any, any, any>({
watch: { watch: {
'value.meta.namespace'() { 'value.meta.namespace'() {
Vue.set(this, 'selectedApps', []); Vue.set(this, 'selectedApps', []);
this.updateValidation(); // For when a user is supplying their own ns
}, },
'value.meta.name'() { 'value.meta.name'() {
@ -142,14 +144,9 @@ export default Vue.extend<Data, any, any, any>({
</script> </script>
<template> <template>
<Loading v-if="!value || !namespaces" /> <Loading v-if="!value || $fetchState.pending" />
<div v-else-if="!namespaces.length">
<Banner color="warning">
{{ t('epinio.warnings.noNamespace') }}
</Banner>
</div>
<CruResource <CruResource
v-else-if="value && namespaces.length > 0" v-else-if="value"
:min-height="'7em'" :min-height="'7em'"
:mode="mode" :mode="mode"
:done-route="doneRoute" :done-route="doneRoute"
@ -157,10 +154,14 @@ export default Vue.extend<Data, any, any, any>({
:can-yaml="false" :can-yaml="false"
:errors="errors" :errors="errors"
:validation-passed="validationPassed" :validation-passed="validationPassed"
namespace-key="meta.namespace"
@error="(e) => (errors = e)" @error="(e) => (errors = e)"
@finish="save" @finish="save"
@cancel="done" @cancel="done"
> >
<Banner v-if="value.isServiceRelated" color="info">
{{ t('epinio.configurations.tableHeaders.service.tooltip') }}
</Banner>
<NameNsDescription <NameNsDescription
name-key="name" name-key="name"
namespace-key="namespace" namespace-key="namespace"

View File

@ -13,6 +13,8 @@ import NameNsDescription from '@shell/components/form/NameNsDescription.vue';
import EpinioBindAppsMixin from './bind-apps-mixin.js'; import EpinioBindAppsMixin from './bind-apps-mixin.js';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export const EPINIO_SERVICE_PARAM = 'service';
interface Data { interface Data {
} }
@ -22,7 +24,7 @@ export default Vue.extend<Data, any, any, any>({
Loading, Loading,
CruResource, CruResource,
LabeledSelect, LabeledSelect,
NameNsDescription NameNsDescription,
}, },
mixins: [CreateEditView, EpinioBindAppsMixin], mixins: [CreateEditView, EpinioBindAppsMixin],
@ -48,7 +50,8 @@ export default Vue.extend<Data, any, any, any>({
this.mixinFetch() this.mixinFetch()
]); ]);
Vue.set(this.value.meta, 'namespace', this.initialValue.meta.namespace || this.namespaces[0].meta.name); Vue.set(this.value, 'catalog_service', this.$route.query[EPINIO_SERVICE_PARAM]);
Vue.set(this.value.meta, 'namespace', this.initialValue.meta.namespace || this.namespaces[0]?.meta.name);
}, },
data() { data() {
@ -68,12 +71,13 @@ export default Vue.extend<Data, any, any, any>({
} }
const nameErrors = validateKubernetesName(this.value?.name || '', this.t('epinio.namespace.name'), this.$store.getters, undefined, []); const nameErrors = validateKubernetesName(this.value?.name || '', this.t('epinio.namespace.name'), this.$store.getters, undefined, []);
const nsErrors = validateKubernetesName(this.value?.meta.namespace || '', '', this.$store.getters, undefined, []);
if (nameErrors.length > 0) { if (nameErrors.length === 0 && nsErrors.length === 0) {
return false; return !this.failedWaitingForDeploy;
} }
return !this.failedWaitingForDeploy; return false;
}, },
namespaces() { namespaces() {
@ -125,7 +129,7 @@ export default Vue.extend<Data, any, any, any>({
}, },
watch: { watch: {
'value.namespace'() { 'value.meta.namespace'() {
Vue.set(this, 'selectedApps', []); Vue.set(this, 'selectedApps', []);
} }
} }
@ -134,20 +138,16 @@ export default Vue.extend<Data, any, any, any>({
</script> </script>
<template> <template>
<Loading v-if="!value || !namespaces" /> <Loading v-if="!value || $fetchState.pending" />
<div v-else-if="!namespaces.length">
<Banner color="warning">
{{ t('epinio.warnings.noNamespace') }}
</Banner>
</div>
<CruResource <CruResource
v-else-if="value && namespaces.length > 0" v-else-if="value"
:can-yaml="false" :can-yaml="false"
:done-route="doneRoute" :done-route="doneRoute"
:mode="mode" :mode="mode"
:validation-passed="validationPassed" :validation-passed="validationPassed"
:resource="value" :resource="value"
:errors="errors" :errors="errors"
namespace-key="meta.namespace"
@error="e=>errors = e" @error="e=>errors = e"
@finish="save" @finish="save"
> >

View File

@ -9,6 +9,11 @@ typeLabel:
one { Applications } one { Applications }
other { Applications } other { Applications }
} }
appcharts: |-
{count, plural,
one { Application Templates }
other { Application Templates }
}
"services": |- "services": |-
{count, plural, {count, plural,
one { Instances } one { Instances }
@ -24,8 +29,22 @@ typeLabel:
one { Catalog } one { Catalog }
other { Catalog } other { Catalog }
} }
typeDescription:
namespaces: Namespaces group your applications, services and other resources. Deleting a namespace will delete all of it's resources.
applications: Epinio uses Applications to transition your code, through build, to being deployed.
services: Epinio can create instances of your services. Instances can be bound to your applications to provide data, for example a database service bound to an application might provide connection credentials.
configurations: Configurations are a way to provide data to applications. The data becomes available once the configuration is bound to them.
appcharts: Application Templates define kube resources created by your application
catalogservices: Catalog Services provide additional, common functionality to applications. For example an instance of a database Catalog Service can be bound to an application.
epinio: epinio:
label: Epinio label: Epinio
intro:
welcome: Welcome to Epinio
blurb: The Application Development Engine for Kubernetes
learnMoreLink: https://epinio.io/
learnMore: Learn more
noNamespaces: Create a Namespace, then create your Applications
getStarted: Get started
tableHeaders: tableHeaders:
namespace: Namespace namespace: Namespace
instances: instances:
@ -59,6 +78,11 @@ epinio:
instances: Instances instances: Instances
memory: Memory memory: Memory
cpu: CPU cpu: CPU
tables:
label: Resources
instances: Instances
services: Services
configs: Configurations
create: create:
title: Application title: Application
titleSubText: Epinio titleSubText: Epinio
@ -140,8 +164,11 @@ epinio:
run: run:
label: Run label: Run
action: action:
create_namespace:
label: Create Namespace
description: A Namespace will be created to contain your Application
create: create:
label: Create label: Create Application
description: The Application will be created ready to deploy source to description: The Application will be created ready to deploy source to
bind_configurations: bind_configurations:
label: Bind Configurations label: Bind Configurations
@ -206,6 +233,7 @@ epinio:
serviceInstance: serviceInstance:
tableHeaders: tableHeaders:
service: Catalog Service service: Catalog Service
serviceVersion: Catalog Service Version
create: create:
catalogService: catalogService:
label: Catalog Service label: Catalog Service
@ -216,5 +244,10 @@ epinio:
tableHeaders: tableHeaders:
shortDesc: Headline shortDesc: Headline
desc: Description desc: Description
detail:
servicesTitle: "{catalogService} Services"
chartVersion: Chart Version
appVersion: Version
helmChart: Helm Chart
warnings: warnings:
noNamespace: There are no namespaces. Please create one before proceeding noNamespace: There are no namespaces. Please create one before proceeding

View File

@ -0,0 +1,36 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import { EPINIO_TYPES } from '../types';
export default {
name: 'EpinioAppChartsList',
components: { ResourceTable },
async fetch() {
await this.$store.dispatch(`epinio/findAll`, { type: EPINIO_TYPES.APP_CHARTS });
},
props: {
schema: {
type: Object,
required: true,
},
},
computed: {
rows() {
return this.$store.getters['epinio/all'](EPINIO_TYPES.APP_CHARTS);
},
}
};
</script>
<template>
<ResourceTable
v-bind="$attrs"
:rows="rows"
:schema="schema"
:loading="$fetchState.pending"
:table-actions="false"
v-on="$listeners"
>
</ResourceTable>
</template>

View File

@ -0,0 +1,81 @@
<script>
import { EPINIO_TYPES } from '../types';
import Loading from '@shell/components/Loading';
import SelectIconGrid from '@shell/components/SelectIconGrid';
export default {
name: 'EpinioCatalogList',
components: { Loading, SelectIconGrid },
fetch() {
this.$store.dispatch(`epinio/findAll`, { type: EPINIO_TYPES.CATALOG_SERVICE });
},
props: {
schema: {
type: Object,
required: true,
},
},
data() {
return { searchQuery: null };
},
methods: {
showDetails(chart) {
this.$router.push(chart.detailLocation);
},
colorFor() {
return `color-1`;
},
},
computed: {
list() {
const list = this.$store.getters['epinio/all'](EPINIO_TYPES.CATALOG_SERVICE);
if (!this.searchQuery) {
return list;
} else {
const query = this.searchQuery.toLowerCase();
return list.filter(e => e?.chart.toLowerCase().includes(query) || e?.description.toLowerCase().includes(query) || e?.short_description.toLowerCase().includes(query));
}
},
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<div class="filter-block">
<input
ref="searchQuery"
v-model="searchQuery"
type="search"
class="input-sm"
:placeholder="t('catalog.charts.search')"
>
</div>
<SelectIconGrid
:rows="list"
:color-for="colorFor"
name-field="name"
icon-field="serviceIcon"
key-field="name"
description-field="short_description"
@clicked="(row) => showDetails(row)"
/>
</div>
</template>
<style lang="scss" scoped>
.filter-block {
display: flex;
justify-content: flex-end;
input {
width: 315px;
}
}
</style>

View File

@ -32,6 +32,7 @@ export default {
submitted: false, submitted: false,
mode: _CREATE, mode: _CREATE,
touched: false, touched: false,
resource: EPINIO_TYPES.NAMESPACE
}; };
}, },
@ -110,7 +111,7 @@ export default {
<div> <div>
<Masthead <Masthead
:schema="schema" :schema="schema"
:resource="'undefined'" :resource="resource"
> >
<template v-slot:createButton> <template v-slot:createButton>
<button <button

View File

@ -0,0 +1,15 @@
import EpinioMetaResource from '~/pkg/epinio/models/epinio-namespaced-resource';
export default class EpinioAppChartModel extends EpinioMetaResource {
get links() {
return {
update: this.getUrl(),
self: this.getUrl(),
};
}
getUrl(name = this.metadata?.name) {
// Add baseUrl in a generic way
return this.$getters['urlFor'](this.type, this.id, { url: `/api/v1/appcharts/${ name || '' }` });
}
}

View File

@ -1,9 +1,10 @@
import Resource from '@shell/plugins/dashboard-store/resource-class'; import Resource from '@shell/plugins/dashboard-store/resource-class';
import { APPLICATION_ACTION_STATE, APPLICATION_MANIFEST_SOURCE_TYPE, APPLICATION_SOURCE_TYPE } from '../types'; import { APPLICATION_ACTION_STATE, APPLICATION_MANIFEST_SOURCE_TYPE, APPLICATION_SOURCE_TYPE, EPINIO_PRODUCT_NAME } from '../types';
import { epinioExceptionToErrorsArray } from '../utils/errors'; import { epinioExceptionToErrorsArray } from '../utils/errors';
import Vue from 'vue'; import Vue from 'vue';
export const APPLICATION_ACTION_TYPE = { export const APPLICATION_ACTION_TYPE = {
CREATE_NS: 'create_namespace',
CREATE: 'create', CREATE: 'create',
GIT_FETCH: 'gitFetch', GIT_FETCH: 'gitFetch',
UPLOAD: 'upload', UPLOAD: 'upload',
@ -18,6 +19,10 @@ export default class ApplicationActionResource extends Resource {
run = true; run = true;
state = APPLICATION_ACTION_STATE.PENDING; state = APPLICATION_ACTION_STATE.PENDING;
// application; // : EpinioApplication;
// bindings; // : EpinioAppBindings;
// type; // : EPINIO_TYPES / string;
get name() { get name() {
return this.t(`epinio.applications.action.${ this.action }.label`); return this.t(`epinio.applications.action.${ this.action }.label`);
} }
@ -61,6 +66,9 @@ export default class ApplicationActionResource extends Resource {
async innerExecute(params) { async innerExecute(params) {
switch (this.action) { switch (this.action) {
case APPLICATION_ACTION_TYPE.CREATE_NS:
await this.createNamespace(params);
break;
case APPLICATION_ACTION_TYPE.CREATE: case APPLICATION_ACTION_TYPE.CREATE:
await this.create(params); await this.create(params);
break; break;
@ -85,6 +93,12 @@ export default class ApplicationActionResource extends Resource {
} }
} }
async createNamespace() {
const ns = await this.$dispatch(`${ EPINIO_PRODUCT_NAME }/createNamespace`, { name: this.application.meta.namespace }, { root: true });
await ns.create();
}
async create() { async create() {
await this.application.create(); await this.application.create();
} }

View File

@ -1,5 +1,4 @@
import { APPLICATION_MANIFEST_SOURCE_TYPE, EPINIO_PRODUCT_NAME, EPINIO_TYPES } from '../types'; import { APPLICATION_MANIFEST_SOURCE_TYPE, EPINIO_PRODUCT_NAME, EPINIO_TYPES } from '../types';
import { createEpinioRoute } from '../utils/custom-routing';
import { formatSi } from '@shell/utils/units'; import { formatSi } from '@shell/utils/units';
import { classify } from '@shell/plugins/dashboard-store/classify'; import { classify } from '@shell/plugins/dashboard-store/classify';
import EpinioMetaResource from './epinio-namespaced-resource'; import EpinioMetaResource from './epinio-namespaced-resource';
@ -41,18 +40,6 @@ export default class EpinioApplicationModel extends EpinioMetaResource {
return res; return res;
} }
get listLocation() {
return this.$rootGetters['type-map/optionsFor'](this.type).customRoute || createEpinioRoute(`c-cluster-applications`, { cluster: this.$rootGetters['clusterId'] });
}
get parentLocationOverride() {
return this.listLocation;
}
get doneRoute() {
return this.listLocation.name;
}
get state() { get state() {
return STATES_MAPPED[this.status] || STATES_MAPPED.unknown; return STATES_MAPPED[this.status] || STATES_MAPPED.unknown;
} }
@ -152,14 +139,6 @@ export default class EpinioApplicationModel extends EpinioMetaResource {
return res; return res;
} }
get nsLocation() {
return createEpinioRoute(`c-cluster-resource-id`, {
cluster: this.$rootGetters['clusterId'],
resource: EPINIO_TYPES.NAMESPACE,
id: this.meta.namespace
});
}
get links() { get links() {
return { return {
update: this.getUrl(), update: this.getUrl(),
@ -225,10 +204,6 @@ export default class EpinioApplicationModel extends EpinioMetaResource {
return Object.keys(this.configuration?.environment || []).length; return Object.keys(this.configuration?.environment || []).length;
} }
get configCount() {
return this.configuration?.configurations.length;
}
get routeCount() { get routeCount() {
return this.configuration?.routes.length; return this.configuration?.routes.length;
} }
@ -257,30 +232,43 @@ export default class EpinioApplicationModel extends EpinioMetaResource {
if (!this.origin) { if (!this.origin) {
return undefined; return undefined;
} }
const appChart = {
label: 'App Chart',
value: this.configuration.appchart
};
switch (this.origin.Kind) { // APPLICATION_MANIFEST_SOURCE_TYPE switch (this.origin.Kind) { // APPLICATION_MANIFEST_SOURCE_TYPE
case APPLICATION_MANIFEST_SOURCE_TYPE.PATH: case APPLICATION_MANIFEST_SOURCE_TYPE.PATH:
return { label: 'File system', icon: 'icon-file' }; return {
label: 'File system',
icon: 'icon-file',
details: [
appChart
]
};
case APPLICATION_MANIFEST_SOURCE_TYPE.GIT: case APPLICATION_MANIFEST_SOURCE_TYPE.GIT:
return { return {
label: 'Git', label: 'Git',
icon: 'icon-file', icon: 'icon-file',
details: [{ details: [
label: 'Url', appChart, {
value: this.origin.git.repository label: 'Url',
}, { value: this.origin.git.repository
label: 'Revision', }, {
icon: 'icon-github', label: 'Revision',
value: this.origin.git.revision icon: 'icon-github',
}] value: this.origin.git.revision
}]
}; };
case APPLICATION_MANIFEST_SOURCE_TYPE.CONTAINER: case APPLICATION_MANIFEST_SOURCE_TYPE.CONTAINER:
return { return {
label: 'Container', label: 'Container',
icon: 'icon-docker', icon: 'icon-docker',
details: [{ details: [
label: 'Image', appChart, {
value: this.origin.Container || this.origin.container label: 'Image',
}] value: this.origin.Container || this.origin.container
}]
}; };
default: default:
return undefined; return undefined;
@ -370,6 +358,7 @@ export default class EpinioApplicationModel extends EpinioMetaResource {
data: { data: {
name: this.meta.name, name: this.meta.name,
configuration: { configuration: {
appchart: this.configuration.appchart,
instances: this.configuration.instances, instances: this.configuration.instances,
configurations: this.configuration.configurations, configurations: this.configuration.configurations,
environment: this.configuration.environment, environment: this.configuration.environment,

View File

@ -1,6 +1,18 @@
import { EPINIO_TYPES } from '@pkg/types';
import { createEpinioRoute } from '@pkg/utils/custom-routing';
import EpinioMetaResource from './epinio-namespaced-resource'; import EpinioMetaResource from './epinio-namespaced-resource';
import { EPINIO_SERVICE_PARAM } from '../edit/services.vue';
export default class EpinioCatalogServiceModel extends EpinioMetaResource { export default class EpinioCatalogServiceModel extends EpinioMetaResource {
get _availableActions() {
return [{
action: 'createService',
label: this.t('generic.create'),
icon: 'icon icon-fw icon-chevron-up',
enabled: true,
}];
}
get links() { get links() {
return { return {
update: this.getUrl(), update: this.getUrl(),
@ -14,4 +26,45 @@ export default class EpinioCatalogServiceModel extends EpinioMetaResource {
// Add baseUrl in a generic way // Add baseUrl in a generic way
return this.$getters['urlFor'](this.type, this.id, { url: `/api/v1/catalogservices/${ name || '' }` }); return this.$getters['urlFor'](this.type, this.id, { url: `/api/v1/catalogservices/${ name || '' }` });
} }
get details() {
return [
{
label: this.t('epinio.catalogService.detail.appVersion'),
content: this.appVersion,
}
// {
// label: this.t('epinio.catalogService.detail.chartVersion'),
// content: this.chartVersion,
// }, {
// label: this.t('epinio.catalogService.detail.helmChart'),
// content: this.helm_repo.name,
// formatter: `Link`,
// formatterOpts: {
// urlKey: 'helm_repo.url',
// labelKey: 'helm_repo.name',
// row: this,
// }
// }
];
}
get services() {
return this.$getters['all'](EPINIO_TYPES.SERVICE_INSTANCE)
.filter((s) => {
return s.catalog_service === this.name;
});
}
createService() {
const serviceCreateLocation = createEpinioRoute(`c-cluster-resource-create`, {
cluster: this.$rootGetters['clusterId'],
resource: EPINIO_TYPES.SERVICE_INSTANCE,
});
return this.currentRouter().push({
...serviceCreateLocation,
query: { [EPINIO_SERVICE_PARAM]: this.name }
});
}
} }

View File

@ -44,7 +44,7 @@ export default class EpinioConfigurationModel extends EpinioNamespacedResource {
} }
get isServiceRelated() { get isServiceRelated() {
return !!this.configuration.origin; return !!this.configuration?.origin;
} }
get service() { get service() {

View File

@ -10,6 +10,14 @@ export default class EpinioResource extends Resource {
}); });
} }
get parentLocationOverride() {
return this.listLocation;
}
get doneRoute() {
return this.listLocation.name;
}
// ------------------------------------------------------------------ // ------------------------------------------------------------------
get canClone() { get canClone() {

View File

@ -22,10 +22,13 @@ export default class EpinioNamespace extends EpinioMetaResource {
const namespaces = await this.$dispatch('findAll', { type: this.type, opt: { force: true } }); const namespaces = await this.$dispatch('findAll', { type: this.type, opt: { force: true } });
// Find new namespace // Find new namespace
// return new namespace
return namespaces.filter(n => n.name === this.name)?.[0]; return namespaces.filter(n => n.name === this.name)?.[0];
} }
save() {
return this.create();
}
get canClone() { get canClone() {
return false; return false;
} }

View File

@ -1,3 +1,4 @@
import { createEpinioRoute } from '@pkg/utils/custom-routing';
import { EPINIO_TYPES } from '~/pkg/epinio/types'; import { EPINIO_TYPES } from '~/pkg/epinio/types';
import EpinioNamespacedResource from './epinio-namespaced-resource'; import EpinioNamespacedResource from './epinio-namespaced-resource';
@ -36,6 +37,14 @@ export default class EpinioServiceModel extends EpinioNamespacedResource {
return this.status; return this.status;
} }
get serviceLocation() {
return createEpinioRoute(`c-cluster-resource-id`, {
cluster: this.$rootGetters['clusterId'],
resource: EPINIO_TYPES.CATALOG_SERVICE,
id: this.catalog_service
});
}
async create() { async create() {
await this.followLink('create', { await this.followLink('create', {
method: 'post', method: 'post',

View File

@ -38,7 +38,10 @@ export default Vue.extend<Data, any, any, any>({
], ],
async fetch() { async fetch() {
await this.$store.dispatch('epinio/findAll', { type: EPINIO_TYPES.NAMESPACE }); await Promise.all([
this.$store.dispatch('epinio/findAll', { type: EPINIO_TYPES.NAMESPACE }),
this.$store.dispatch('epinio/findAll', { type: EPINIO_TYPES.APP_CHARTS }),
]);
this.originalModel = await this.$store.dispatch(`epinio/create`, { type: EPINIO_TYPES.APP }); this.originalModel = await this.$store.dispatch(`epinio/create`, { type: EPINIO_TYPES.APP });
// Dissassociate the original model & model. This fixes `Create` after refreshing page with SSR on // Dissassociate the original model & model. This fixes `Create` after refreshing page with SSR on
@ -97,7 +100,14 @@ export default Vue.extend<Data, any, any, any>({
updateSource(changes: EpinioAppSource) { updateSource(changes: EpinioAppSource) {
this.source = {}; this.source = {};
this.set(this.source, changes); const { appChart, ...cleanChanges } = changes;
if (appChart) {
// app chart actuall belongs in config, so stick it in there
this.value.configuration = this.value.configuration || {};
this.set(this.value.configuration, { appchart: appChart });
}
this.set(this.source, cleanChanges);
}, },
updateManifestConfigurations(changes: string[]) { updateManifestConfigurations(changes: string[]) {

View File

@ -5,13 +5,15 @@ import Masthead from '@shell/components/ResourceList/Masthead';
import LinkDetail from '@shell/components/formatter/LinkDetail'; import LinkDetail from '@shell/components/formatter/LinkDetail';
import { EPINIO_TYPES } from '../../../../types'; import { EPINIO_TYPES } from '../../../../types';
import { createEpinioRoute } from '../../../../utils/custom-routing'; import { createEpinioRoute } from '../../../../utils/custom-routing';
import EpinioIntro from '../../../../components/EpinioIntro.vue';
export default { export default {
components: { components: {
Loading, Loading,
LinkDetail, LinkDetail,
ResourceTable, ResourceTable,
Masthead Masthead,
EpinioIntro
}, },
async fetch() { async fetch() {
@ -47,6 +49,10 @@ export default {
rows() { rows() {
return this.$store.getters['epinio/all'](this.resource); return this.$store.getters['epinio/all'](this.resource);
}, },
hasNamespaces() {
return !!this.$store.getters['epinio/all'](EPINIO_TYPES.NAMESPACE)?.length;
}
}, },
}; };
@ -54,6 +60,7 @@ export default {
<template> <template>
<Loading v-if="$fetchState.pending" /> <Loading v-if="$fetchState.pending" />
<EpinioIntro v-else-if="!hasNamespaces" />
<div v-else> <div v-else>
<Masthead <Masthead
:schema="schema" :schema="schema"

View File

@ -6,7 +6,7 @@ import { base64Encode } from '@shell/utils/crypto';
import { NAMESPACE_FILTERS } from '@shell/store/prefs'; import { NAMESPACE_FILTERS } from '@shell/store/prefs';
import { createNamespaceFilterKeyWithId } from '@shell/utils/namespace-filter'; import { createNamespaceFilterKeyWithId } from '@shell/utils/namespace-filter';
import { parse as parseUrl, stringify as unParseUrl } from '@shell/utils/url'; import { parse as parseUrl, stringify as unParseUrl } from '@shell/utils/url';
// import https from 'https'; import { classify } from '@shell/plugins/dashboard-store/classify';
const createId = (schema: any, resource: any) => { const createId = (schema: any, resource: any) => {
const name = resource.meta?.name || resource.name; const name = resource.meta?.name || resource.name;
@ -178,6 +178,13 @@ export default {
collectionMethods: ['get', 'post'], collectionMethods: ['get', 'post'],
resourceFields: { }, resourceFields: { },
attributes: { namespaced: true } attributes: { namespaced: true }
}, {
product: EPINIO_PRODUCT_NAME,
id: EPINIO_TYPES.APP_CHARTS,
type: 'schema',
links: { collection: '/api/v1/appcharts' },
collectionMethods: ['get'],
resourceFields: { },
}, { }, {
product: EPINIO_PRODUCT_NAME, product: EPINIO_PRODUCT_NAME,
id: EPINIO_TYPES.NAMESPACE, id: EPINIO_TYPES.NAMESPACE,
@ -264,5 +271,13 @@ export default {
commit('singleProductCNSI', cnsi); commit('singleProductCNSI', cnsi);
return cnsi; return cnsi;
},
createNamespace(ctx: any, obj: { name : string }) {
// Note - created model save --> create
return classify(ctx, {
type: EPINIO_TYPES.NAMESPACE,
meta: { name: obj.name }
});
} }
}; };

View File

@ -42,6 +42,9 @@
"@shell/models/*": [ "@shell/models/*": [
"../../shell/models/*" "../../shell/models/*"
], ],
"@components/*": [
"../../pkg/rancher-components/*"
],
"@pkg/*": [ "@pkg/*": [
"./*" "./*"
] ]

View File

@ -2,6 +2,7 @@ import EpinioApplicationModel from './models/applications';
import EpinioCatalogServiceModel from './models/catalogservices'; import EpinioCatalogServiceModel from './models/catalogservices';
import EpinioConfigurationModel from './models/configurations'; import EpinioConfigurationModel from './models/configurations';
import EpinioServiceModel from './models/services'; import EpinioServiceModel from './models/services';
import EpinioAppChartModel from './models/appcharts';
export const EPINIO_PRODUCT_NAME = 'epinio'; export const EPINIO_PRODUCT_NAME = 'epinio';
@ -13,6 +14,7 @@ export const EPINIO_STANDALONE_CLUSTER_NAME = 'default';
export const EPINIO_TYPES = { export const EPINIO_TYPES = {
// From API // From API
APP: 'applications', APP: 'applications',
APP_CHARTS: 'appcharts',
NAMESPACE: 'namespaces', NAMESPACE: 'namespaces',
CONFIGURATION: 'configurations', CONFIGURATION: 'configurations',
CATALOG_SERVICE: 'catalogservices', CATALOG_SERVICE: 'catalogservices',
@ -65,6 +67,7 @@ export interface EpinioApplicationResource {
configuration: { configuration: {
instances: number, instances: number,
configurations: string[], configurations: string[],
appchart?: string,
environment: Map<string, string>, environment: Map<string, string>,
routes: string[] routes: string[]
}, },
@ -83,6 +86,15 @@ export interface EpinioApplicationResource {
export type EpinioApplication = EpinioApplicationResource & EpinioApplicationModel & EpinioMetaProperty; export type EpinioApplication = EpinioApplicationResource & EpinioApplicationModel & EpinioMetaProperty;
export interface EpinioApplicationChartResource {
meta: EpinioMeta,
description: string,
helm_chart: string, // eslint-disable-line camelcase
short_description: string, // eslint-disable-line camelcase
}
export type EpinioAppChart = EpinioApplicationChartResource & EpinioAppChartModel & EpinioMetaProperty;
export interface EpinioHelmRepoResource { export interface EpinioHelmRepoResource {
name: string, name: string,
url: string, url: string,
@ -116,6 +128,7 @@ export interface EpinioServiceResource {
meta: EpinioMeta meta: EpinioMeta
boundapps: string[], boundapps: string[],
catalog_service: string, // eslint-disable-line camelcase catalog_service: string, // eslint-disable-line camelcase
catalog_service_version: string, // eslint-disable-line camelcase
status: string, status: string,
} }

View File

@ -324,7 +324,7 @@ export default {
}, },
selectNamespace(e) { selectNamespace(e) {
if (e.value === '') { // The blank value in the dropdown is labeled "Create a New Namespace" if (!e || e.value === '') { // The blank value in the dropdown is labeled "Create a New Namespace"
this.createNamespace = true; this.createNamespace = true;
this.$parent.$emit('createNamespace', true); this.$parent.$emit('createNamespace', true);
Vue.nextTick(() => this.$refs.namespace.focus()); Vue.nextTick(() => this.$refs.namespace.focus());

View File

@ -331,7 +331,7 @@ export default {
<div class="rd-header-right"> <div class="rd-header-right">
<HarvesterUpgrade v-if="isVirtualCluster" /> <HarvesterUpgrade v-if="isVirtualCluster" />
<div <div
v-if="currentCluster && !simple && (currentProduct.showNamespaceFilter || currentProduct.showWorkspaceSwitcher)" v-if="(currentCluster || currentProduct.customNamespaceFilter) && !simple && (currentProduct.showNamespaceFilter || currentProduct.showWorkspaceSwitcher)"
class="top" class="top"
> >
<NamespaceFilter v-if="clusterReady && currentProduct && (currentProduct.showNamespaceFilter || isExplorer)" /> <NamespaceFilter v-if="clusterReady && currentProduct && (currentProduct.showNamespaceFilter || isExplorer)" />