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
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
uses: actions/upload-artifact@v2

View File

@ -4,7 +4,9 @@ set -e
echo "GITHUB_SHA: $GITHUB_SHA"
echo "GITHUB_REF_NAME: $GITHUB_REF_NAME"
echo "ROUTER_BASE: $ROUTER_BASE"
echo
echo "RANCHER_ENV: $RANCHER_ENV"
echo "EXCLUDES_PKG: $EXCLUDES_PKG"
echo
echo "RELEASE_DIR: $RELEASE_DIR"
RELEASE_LOCATION="$RELEASE_DIR/$ARTIFACT_NAME"
@ -26,7 +28,7 @@ echo Installing dependencies
yarn install --frozen-lockfile
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
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 KeyValue from '@shell/components/form/KeyValue.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 { sortBy } from '@shell/utils/sort';
import { validateKubernetesName } from '@shell/utils/validators/kubernetes-name';
export interface EpinioAppInfo {
meta: {
@ -24,7 +25,7 @@ export interface EpinioAppInfo {
interface Data {
errors: string[],
values: EpinioAppInfo
values?: EpinioAppInfo
}
// Data, Methods, Computed, Props
@ -35,7 +36,7 @@ export default Vue.extend<Data, any, any, any>({
NameNsDescription,
LabeledInput,
KeyValue,
Banner
Loading
},
props: {
@ -52,17 +53,7 @@ export default Vue.extend<Data, any, any, any>({
data() {
return {
errors: [],
values: {
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 || [],
},
}
values: undefined
};
},
@ -70,7 +61,7 @@ export default Vue.extend<Data, any, any, any>({
this.values = {
meta: {
name: this.application.meta?.name,
namespace: this.application.meta?.namespace
namespace: this.application.meta?.namespace || this.namespaces[0]?.metadata.name
},
configuration: {
instances: this.application.configuration?.instances || 1,
@ -105,8 +96,13 @@ export default Vue.extend<Data, any, any, any>({
},
valid() {
if (!this.values) {
return false;
}
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;
return validName && validNamespace && validInstances;
@ -127,11 +123,7 @@ export default Vue.extend<Data, any, any, any>({
</script>
<template>
<div v-if="!namespaces.length">
<Banner color="warning">
{{ t('epinio.warnings.noNamespace') }}
</Banner>
</div>
<Loading v-if="!values" />
<div v-else>
<div class="col">
<NameNsDescription
@ -143,6 +135,7 @@ export default Vue.extend<Data, any, any, any>({
:value="values.meta"
:mode="mode"
@change="update"
@createNamespace="ns => values.meta.namespace = ns"
/>
</div>
<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 { EpinioAppSource } from '../../components/application/AppSource.vue';
import { EpinioAppBindings } from '../../components/application/AppConfiguration.vue';
import EpinioNamespace from '~/pkg/epinio/models/namespaces';
interface Data {
running: boolean;
@ -35,7 +36,7 @@ export default Vue.extend<Data, any, any, any>({
},
bindings: {
type: Object as PropType<EpinioAppBindings>,
required: true
default: () => null
},
mode: {
type: String,
@ -48,22 +49,34 @@ export default Vue.extend<Data, any, any, any>({
},
async fetch() {
const coreArgs = {
const coreArgs: Partial<ApplicationAction & {
application: EpinioApplication,
bindings: EpinioAppBindings,
type: string,
}> = {
application: this.application,
bindings: this.bindings,
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', {
action: APPLICATION_ACTION_TYPE.CREATE,
index: 0, // index used for sorting
index: 1, // index used for sorting
...coreArgs,
}));
if (this.bindings?.configurations?.length) {
this.actions.push(await this.$store.dispatch('epinio/create', {
action: APPLICATION_ACTION_TYPE.BIND_CONFIGURATIONS,
index: 1,
index: 2,
...coreArgs,
}));
}
@ -71,7 +84,7 @@ export default Vue.extend<Data, any, any, any>({
if (this.bindings?.services?.length) {
this.actions.push(await this.$store.dispatch('epinio/create', {
action: APPLICATION_ACTION_TYPE.BIND_SERVICES,
index: 2,
index: 3,
...coreArgs,
}));
}
@ -80,7 +93,7 @@ export default Vue.extend<Data, any, any, any>({
this.source.type === APPLICATION_SOURCE_TYPE.FOLDER) {
this.actions.push(await this.$store.dispatch('epinio/create', {
action: APPLICATION_ACTION_TYPE.UPLOAD,
index: 3,
index: 4,
...coreArgs,
}));
}
@ -88,7 +101,7 @@ export default Vue.extend<Data, any, any, any>({
if (this.source.type === APPLICATION_SOURCE_TYPE.GIT_URL) {
this.actions.push(await this.$store.dispatch('epinio/create', {
action: APPLICATION_ACTION_TYPE.GIT_FETCH,
index: 3,
index: 4,
...coreArgs,
}));
}
@ -98,14 +111,14 @@ export default Vue.extend<Data, any, any, any>({
this.source.type === APPLICATION_SOURCE_TYPE.GIT_URL) {
this.actions.push(await this.$store.dispatch('epinio/create', {
action: APPLICATION_ACTION_TYPE.BUILD,
index: 4,
index: 5,
...coreArgs,
}));
}
this.actions.push(await this.$store.dispatch('epinio/create', {
action: APPLICATION_ACTION_TYPE.DEPLOY,
index: 5,
index: 6,
...coreArgs,
}));
@ -121,7 +134,7 @@ export default Vue.extend<Data, any, any, any>({
labelKey: 'epinio.applications.steps.progress.table.stage.label',
value: 'name',
sort: ['index'],
width: 100,
width: 150,
},
{
...DESCRIPTION,
@ -144,7 +157,11 @@ export default Vue.extend<Data, any, any, any>({
computed: {
actionsToRun() {
return this.actions.filter((action: ApplicationAction) => action.run);
}
},
namespaces() {
return this.$store.getters['epinio/all'](EPINIO_TYPES.NAMESPACE);
},
},
watch: {

View File

@ -10,8 +10,9 @@ import FileSelector from '@shell/components/form/FileSelector.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, EPINIO_TYPES } from '../../types';
import { APPLICATION_SOURCE_TYPE, EpinioApplicationChartResource, EPINIO_TYPES } from '../../types';
import { EpinioAppInfo } from './AppInfo.vue';
interface Archive{
@ -34,6 +35,7 @@ interface BuilderImage {
}
interface Data {
open: boolean,
archive: Archive,
container: Container,
gitUrl: GitUrl,
@ -49,6 +51,12 @@ export interface EpinioAppSource {
container: Container,
gitUrl: GitUrl,
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';
@ -60,7 +68,8 @@ export default Vue.extend<Data, any, any, any>({
FileSelector,
LabeledInput,
LabeledSelect,
RadioGroup
RadioGroup,
Collapse
},
props: {
@ -80,6 +89,8 @@ export default Vue.extend<Data, any, any, any>({
data() {
return {
open: false,
archive: {
tarball: this.source?.archive.tarball || '',
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,
},
appChart: this.source?.appChart,
types: [{
label: this.t('epinio.applications.steps.source.archive.label'),
value: APPLICATION_SOURCE_TYPE.ARCHIVE
@ -116,6 +129,9 @@ export default Vue.extend<Data, any, any, any>({
},
mounted() {
if (!this.appChart) {
Vue.set(this, 'appChart', this.appCharts[0].value);
}
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, 'branch', parsed.origin.git.revision);
}
if (parsed.configuration) {
Vue.set(this, 'appChart', parsed.configuration.appchart);
}
const appInfo: EpinioAppInfo = {
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 = '';
// Determine parent folder name
for (const f of files) {
for (const f of safeFiles) {
const paths = f.webkitRelativePath.split('/');
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;
if (folderName) {
@ -211,7 +231,8 @@ export default Vue.extend<Data, any, any, any>({
archive: this.archive,
container: this.container,
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');
},
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() {
// 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
@ -387,27 +415,42 @@ export default Vue.extend<Data, any, any, any>({
/>
</div>
</template>
<template v-if="showBuilderImage">
<div class="spacer">
<RadioGroup
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')"
<Collapse :open.sync="open" :title="'Advanced Settings'" class="mt-30">
<template>
<LabeledSelect
v-model="appChart"
data-testid="epinio_app-source_appchart"
label="Application Chart"
:options="appCharts"
:mode="mode"
:clearable="false"
:required="true"
:tooltip="t('typeDescription.appcharts')"
:reduce="(e) => e.value"
@input="update"
/>
</div>
</template>
<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>
@ -423,6 +466,10 @@ export default Vue.extend<Data, any, any, any>({
margin-left: 5px;
}
}
.collapse {
margin-left: -5px;
}
}
.archive {
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 }) });
// App resource
weightType(EPINIO_TYPES.APP, 300, true);
configureType(EPINIO_TYPES.APP, {
isCreatable: true,
isEditable: true,
@ -81,8 +80,17 @@ export function init($plugin: any, store: any) {
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
weightType(EPINIO_TYPES.CONFIGURATION, 200, true);
configureType(EPINIO_TYPES.CONFIGURATION, {
isCreatable: true,
isEditable: true,
@ -92,12 +100,11 @@ export function init($plugin: any, store: any) {
customRoute: createEpinioRoute('c-cluster-resource', { resource: EPINIO_TYPES.CONFIGURATION }),
});
// Groups
const ADVANCED_GROUP = 'Advanced';
const SERVICE_GROUP = 'Services';
weightGroup(SERVICE_GROUP, 1, true);
// Service Instance
weightType(EPINIO_TYPES.SERVICE_INSTANCE, 151, true);
configureType(EPINIO_TYPES.SERVICE_INSTANCE, {
isCreatable: true,
isEditable: true,
@ -108,7 +115,6 @@ export function init($plugin: any, store: any) {
});
// Catalog Service
weightType(EPINIO_TYPES.CATALOG_SERVICE, 150, true);
configureType(EPINIO_TYPES.CATALOG_SERVICE, {
isCreatable: false,
isEditable: false,
@ -119,7 +125,6 @@ export function init($plugin: any, store: any) {
});
// Namespace resource
weightType(EPINIO_TYPES.NAMESPACE, 100, true);
configureType(EPINIO_TYPES.NAMESPACE, {
isCreatable: 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.
});
// Side Nav
weightType(EPINIO_TYPES.CATALOG_SERVICE, 150, true);
weightType(EPINIO_TYPES.SERVICE_INSTANCE, 151, true);
basicType([
EPINIO_TYPES.SERVICE_INSTANCE,
EPINIO_TYPES.CATALOG_SERVICE,
], 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([
EPINIO_TYPES.APP,
SERVICE_GROUP,
EPINIO_TYPES.NAMESPACE,
EPINIO_TYPES.CONFIGURATION
ADVANCED_GROUP
]);
headers(EPINIO_TYPES.APP, [
@ -150,8 +169,6 @@ export function init($plugin: any, store: any) {
labelKey: 'epinio.tableHeaders.namespace',
value: 'meta.namespace',
sort: ['meta.namespace'],
formatter: 'LinkDetail',
formatterOpts: { reference: 'nsLocation' }
},
{
name: 'dep-status',
@ -266,8 +283,6 @@ export function init($plugin: any, store: any) {
labelKey: 'epinio.tableHeaders.namespace',
value: 'meta.namespace',
sort: ['meta.namespace'],
formatter: 'LinkDetail',
formatterOpts: { reference: 'nsLocation' }
},
{
name: 'boundApps',
@ -298,20 +313,26 @@ export function init($plugin: any, store: any) {
headers(EPINIO_TYPES.SERVICE_INSTANCE, [
STATE,
SIMPLE_NAME,
NAME,
{
name: 'namespace',
labelKey: 'epinio.tableHeaders.namespace',
value: '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',
labelKey: 'epinio.serviceInstance.tableHeaders.service',
value: 'catalog_service',
sort: ['catalog_service'],
{
name: 'catalog_service',
labelKey: 'epinio.serviceInstance.tableHeaders.service',
value: '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',
@ -338,4 +359,21 @@ export function init($plugin: any, store: any) {
},
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 { epinioExceptionToErrorsArray } from '../utils/errors';
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 {
}
@ -19,7 +21,9 @@ export default Vue.extend<Data, any, any, any>({
ConsumptionGauge,
ResourceTable,
PlusMinus,
ApplicationCard
ApplicationCard,
Tabbed,
Tab,
},
props: {
value: {
@ -35,12 +39,35 @@ export default Vue.extend<Data, any, any, any>({
required: true
},
},
fetch() {
this.$store.dispatch(`epinio/findAll`, { type: EPINIO_TYPES.SERVICE_INSTANCE });
this.$store.dispatch(`epinio/findAll`, { type: EPINIO_TYPES.CONFIGURATION });
},
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 {
appInstanceSchema: this.$store.getters[`${ EPINIO_PRODUCT_NAME }/schemaFor`](EPINIO_TYPES.APP_INSTANCE),
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: {
async updateInstances(newInstances: number) {
this.$set(this, 'saving', true);
@ -58,6 +85,12 @@ export default Vue.extend<Data, any, any, any>({
return `${ matchGithub?.[4] }/${ matchGithub?.[5] }`;
}
},
computed: {
sourceIcon(): string {
return this.value.sourceInfo?.icon || 'icon-epinio';
}
}
});
</script>
@ -68,7 +101,7 @@ export default Vue.extend<Data, any, any, any>({
<ApplicationCard>
<!-- Icon slot -->
<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>
<!-- Routes links slot -->
@ -87,25 +120,24 @@ export default Vue.extend<Data, any, any, any>({
<!-- Resources count slot -->
<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>
{{ value.envCount }} {{ t('epinio.applications.detail.counts.envVars') }}
</div>
<div>
{{ value.serviceConfigurations.length }} {{ t('epinio.applications.detail.counts.services') }}
</div>
<div>
{{ value.baseConfigurations.length }} {{ t('epinio.applications.detail.counts.config') }}
</div>
</template>
</ApplicationCard>
</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') }}
</h3>
<div class="deployment">
<div v-if="value.deployment" class="deployment">
<div class="simple-box-row app-instances">
<SimpleBox>
<ConsumptionGauge
@ -165,28 +197,41 @@ export default Vue.extend<Data, any, any, any>({
</thead>
<tr>
<td>{{ t('tableHeaders.memory') }}</td>
<td>{{ value.instanceMemory.min }} MiB</td>
<td>{{ value.instanceMemory.max }} MiB</td>
<td>{{ value.instanceMemory.avg }} MiB</td>
<td>{{ value.instanceMemory.min }}</td>
<td>{{ value.instanceMemory.max }}</td>
<td>{{ value.instanceMemory.avg }}</td>
</tr>
<tr>
<td>{{ t('tableHeaders.cpu') }}</td>
<td>{{ value.instanceCpu.min }} m</td>
<td>{{ value.instanceCpu.max }} m</td>
<td>{{ value.instanceCpu.avg }} m</td>
<td>{{ value.instanceCpu.min }}</td>
<td>{{ value.instanceCpu.max }}</td>
<td>{{ value.instanceCpu.avg }}</td>
</tr>
</table>
</div>
</SimpleBox>
</div>
</div>
<h3 class="mt-20">
{{ t('epinio.applications.detail.tables.label') }}
</h3>
<div>
<!-- //TODO: Add Services & Configurations as tabs -->
<ResourceTable :schema="appInstanceSchema" :rows="value.instances" :table-actions="false">
<template #header-left>
<h1>Instances</h1>
</template>
</ResourceTable>
<Tabbed>
<Tab label-key="epinio.applications.detail.tables.instances" name="instances" :weight="3">
<ResourceTable :schema="appInstance.schema" :headers="appInstance.headers" :rows="value.instances" :table-actions="false">
</ResourceTable>
</Tab>
<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>
</template>
@ -292,7 +337,6 @@ export default Vue.extend<Data, any, any, any>({
}
.deployment {
margin-bottom: 60px;
.simple-box {
width: 100%;
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() {
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 || []];
},
@ -105,8 +105,9 @@ export default Vue.extend<Data, any, any, any>({
updateValidation() {
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 || {});
if (!!dataValues.length) {
@ -124,6 +125,7 @@ export default Vue.extend<Data, any, any, any>({
watch: {
'value.meta.namespace'() {
Vue.set(this, 'selectedApps', []);
this.updateValidation(); // For when a user is supplying their own ns
},
'value.meta.name'() {
@ -142,14 +144,9 @@ export default Vue.extend<Data, any, any, any>({
</script>
<template>
<Loading v-if="!value || !namespaces" />
<div v-else-if="!namespaces.length">
<Banner color="warning">
{{ t('epinio.warnings.noNamespace') }}
</Banner>
</div>
<Loading v-if="!value || $fetchState.pending" />
<CruResource
v-else-if="value && namespaces.length > 0"
v-else-if="value"
:min-height="'7em'"
:mode="mode"
:done-route="doneRoute"
@ -157,10 +154,14 @@ export default Vue.extend<Data, any, any, any>({
:can-yaml="false"
:errors="errors"
:validation-passed="validationPassed"
namespace-key="meta.namespace"
@error="(e) => (errors = e)"
@finish="save"
@cancel="done"
>
<Banner v-if="value.isServiceRelated" color="info">
{{ t('epinio.configurations.tableHeaders.service.tooltip') }}
</Banner>
<NameNsDescription
name-key="name"
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 { mapGetters } from 'vuex';
export const EPINIO_SERVICE_PARAM = 'service';
interface Data {
}
@ -22,7 +24,7 @@ export default Vue.extend<Data, any, any, any>({
Loading,
CruResource,
LabeledSelect,
NameNsDescription
NameNsDescription,
},
mixins: [CreateEditView, EpinioBindAppsMixin],
@ -48,7 +50,8 @@ export default Vue.extend<Data, any, any, any>({
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() {
@ -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 nsErrors = validateKubernetesName(this.value?.meta.namespace || '', '', this.$store.getters, undefined, []);
if (nameErrors.length > 0) {
return false;
if (nameErrors.length === 0 && nsErrors.length === 0) {
return !this.failedWaitingForDeploy;
}
return !this.failedWaitingForDeploy;
return false;
},
namespaces() {
@ -125,7 +129,7 @@ export default Vue.extend<Data, any, any, any>({
},
watch: {
'value.namespace'() {
'value.meta.namespace'() {
Vue.set(this, 'selectedApps', []);
}
}
@ -134,20 +138,16 @@ export default Vue.extend<Data, any, any, any>({
</script>
<template>
<Loading v-if="!value || !namespaces" />
<div v-else-if="!namespaces.length">
<Banner color="warning">
{{ t('epinio.warnings.noNamespace') }}
</Banner>
</div>
<Loading v-if="!value || $fetchState.pending" />
<CruResource
v-else-if="value && namespaces.length > 0"
v-else-if="value"
:can-yaml="false"
:done-route="doneRoute"
:mode="mode"
:validation-passed="validationPassed"
:resource="value"
:errors="errors"
namespace-key="meta.namespace"
@error="e=>errors = e"
@finish="save"
>

View File

@ -9,6 +9,11 @@ typeLabel:
one { Applications }
other { Applications }
}
appcharts: |-
{count, plural,
one { Application Templates }
other { Application Templates }
}
"services": |-
{count, plural,
one { Instances }
@ -24,8 +29,22 @@ typeLabel:
one { 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:
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:
namespace: Namespace
instances:
@ -59,6 +78,11 @@ epinio:
instances: Instances
memory: Memory
cpu: CPU
tables:
label: Resources
instances: Instances
services: Services
configs: Configurations
create:
title: Application
titleSubText: Epinio
@ -140,8 +164,11 @@ epinio:
run:
label: Run
action:
create_namespace:
label: Create Namespace
description: A Namespace will be created to contain your Application
create:
label: Create
label: Create Application
description: The Application will be created ready to deploy source to
bind_configurations:
label: Bind Configurations
@ -206,6 +233,7 @@ epinio:
serviceInstance:
tableHeaders:
service: Catalog Service
serviceVersion: Catalog Service Version
create:
catalogService:
label: Catalog Service
@ -216,5 +244,10 @@ epinio:
tableHeaders:
shortDesc: Headline
desc: Description
detail:
servicesTitle: "{catalogService} Services"
chartVersion: Chart Version
appVersion: Version
helmChart: Helm Chart
warnings:
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,
mode: _CREATE,
touched: false,
resource: EPINIO_TYPES.NAMESPACE
};
},
@ -110,7 +111,7 @@ export default {
<div>
<Masthead
:schema="schema"
:resource="'undefined'"
:resource="resource"
>
<template v-slot:createButton>
<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 { 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 Vue from 'vue';
export const APPLICATION_ACTION_TYPE = {
CREATE_NS: 'create_namespace',
CREATE: 'create',
GIT_FETCH: 'gitFetch',
UPLOAD: 'upload',
@ -18,6 +19,10 @@ export default class ApplicationActionResource extends Resource {
run = true;
state = APPLICATION_ACTION_STATE.PENDING;
// application; // : EpinioApplication;
// bindings; // : EpinioAppBindings;
// type; // : EPINIO_TYPES / string;
get name() {
return this.t(`epinio.applications.action.${ this.action }.label`);
}
@ -61,6 +66,9 @@ export default class ApplicationActionResource extends Resource {
async innerExecute(params) {
switch (this.action) {
case APPLICATION_ACTION_TYPE.CREATE_NS:
await this.createNamespace(params);
break;
case APPLICATION_ACTION_TYPE.CREATE:
await this.create(params);
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() {
await this.application.create();
}

View File

@ -1,5 +1,4 @@
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 { classify } from '@shell/plugins/dashboard-store/classify';
import EpinioMetaResource from './epinio-namespaced-resource';
@ -41,18 +40,6 @@ export default class EpinioApplicationModel extends EpinioMetaResource {
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() {
return STATES_MAPPED[this.status] || STATES_MAPPED.unknown;
}
@ -152,14 +139,6 @@ export default class EpinioApplicationModel extends EpinioMetaResource {
return res;
}
get nsLocation() {
return createEpinioRoute(`c-cluster-resource-id`, {
cluster: this.$rootGetters['clusterId'],
resource: EPINIO_TYPES.NAMESPACE,
id: this.meta.namespace
});
}
get links() {
return {
update: this.getUrl(),
@ -225,10 +204,6 @@ export default class EpinioApplicationModel extends EpinioMetaResource {
return Object.keys(this.configuration?.environment || []).length;
}
get configCount() {
return this.configuration?.configurations.length;
}
get routeCount() {
return this.configuration?.routes.length;
}
@ -257,30 +232,43 @@ export default class EpinioApplicationModel extends EpinioMetaResource {
if (!this.origin) {
return undefined;
}
const appChart = {
label: 'App Chart',
value: this.configuration.appchart
};
switch (this.origin.Kind) { // APPLICATION_MANIFEST_SOURCE_TYPE
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:
return {
label: 'Git',
icon: 'icon-file',
details: [{
label: 'Url',
value: this.origin.git.repository
}, {
label: 'Revision',
icon: 'icon-github',
value: this.origin.git.revision
}]
details: [
appChart, {
label: 'Url',
value: this.origin.git.repository
}, {
label: 'Revision',
icon: 'icon-github',
value: this.origin.git.revision
}]
};
case APPLICATION_MANIFEST_SOURCE_TYPE.CONTAINER:
return {
label: 'Container',
icon: 'icon-docker',
details: [{
label: 'Image',
value: this.origin.Container || this.origin.container
}]
details: [
appChart, {
label: 'Image',
value: this.origin.Container || this.origin.container
}]
};
default:
return undefined;
@ -370,6 +358,7 @@ export default class EpinioApplicationModel extends EpinioMetaResource {
data: {
name: this.meta.name,
configuration: {
appchart: this.configuration.appchart,
instances: this.configuration.instances,
configurations: this.configuration.configurations,
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 { EPINIO_SERVICE_PARAM } from '../edit/services.vue';
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() {
return {
update: this.getUrl(),
@ -14,4 +26,45 @@ export default class EpinioCatalogServiceModel extends EpinioMetaResource {
// Add baseUrl in a generic way
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() {
return !!this.configuration.origin;
return !!this.configuration?.origin;
}
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() {

View File

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

View File

@ -1,3 +1,4 @@
import { createEpinioRoute } from '@pkg/utils/custom-routing';
import { EPINIO_TYPES } from '~/pkg/epinio/types';
import EpinioNamespacedResource from './epinio-namespaced-resource';
@ -36,6 +37,14 @@ export default class EpinioServiceModel extends EpinioNamespacedResource {
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() {
await this.followLink('create', {
method: 'post',

View File

@ -38,7 +38,10 @@ export default Vue.extend<Data, any, any, any>({
],
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 });
// 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) {
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[]) {

View File

@ -5,13 +5,15 @@ import Masthead from '@shell/components/ResourceList/Masthead';
import LinkDetail from '@shell/components/formatter/LinkDetail';
import { EPINIO_TYPES } from '../../../../types';
import { createEpinioRoute } from '../../../../utils/custom-routing';
import EpinioIntro from '../../../../components/EpinioIntro.vue';
export default {
components: {
Loading,
LinkDetail,
ResourceTable,
Masthead
Masthead,
EpinioIntro
},
async fetch() {
@ -47,6 +49,10 @@ export default {
rows() {
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>
<Loading v-if="$fetchState.pending" />
<EpinioIntro v-else-if="!hasNamespaces" />
<div v-else>
<Masthead
:schema="schema"

View File

@ -6,7 +6,7 @@ import { base64Encode } from '@shell/utils/crypto';
import { NAMESPACE_FILTERS } from '@shell/store/prefs';
import { createNamespaceFilterKeyWithId } from '@shell/utils/namespace-filter';
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 name = resource.meta?.name || resource.name;
@ -178,6 +178,13 @@ export default {
collectionMethods: ['get', 'post'],
resourceFields: { },
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,
id: EPINIO_TYPES.NAMESPACE,
@ -264,5 +271,13 @@ export default {
commit('singleProductCNSI', 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/*"
],
"@components/*": [
"../../pkg/rancher-components/*"
],
"@pkg/*": [
"./*"
]

View File

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

View File

@ -324,7 +324,7 @@ export default {
},
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.$parent.$emit('createNamespace', true);
Vue.nextTick(() => this.$refs.namespace.focus());

View File

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