Split launch out, single vs mcm fixes, update products on schema change

This commit is contained in:
Vincent Fiduccia 2020-07-24 00:56:46 -07:00
parent 3150bdeeff
commit 8e4649010b
No known key found for this signature in database
GPG Key ID: 2B29AD6BB2BB2582
14 changed files with 592 additions and 463 deletions

View File

@ -3,7 +3,7 @@ locale:
en-us: English
product:
apps: App Marketplace
apps: Apps & Marketplace
explorer: Cluster Explorer
gatekeeper: OPA Gatekeeper
istio: Istio
@ -889,7 +889,7 @@ typeLabel:
}
catalog.cattle.io.release: |
{count, plural,
one { App }
one { Apps }
other { Apps }
}
chartInstallAction: |

View File

@ -98,6 +98,9 @@ export default {
if ( !namespace ) {
namespace = this.$store.getters['defaultNamespace'];
if ( metadata ) {
metadata.namespace = namespace;
}
}
}

View File

@ -208,13 +208,16 @@ export default {
}
const params = {
container: this.container,
previous: this.previous,
follow: true,
timestamps: true,
pretty: true,
};
if ( this.container ) {
params.container = this.container;
}
const range = `${ this.range }`.trim().toLowerCase();
const match = range.match(/^(\d+)?\s*(.*?)s?$/);

View File

@ -11,7 +11,8 @@ export function init(store) {
product,
basicType,
headers,
formOnlyType
virtualType,
uncreatableType,
} = DSL(store, NAME);
product({
@ -20,14 +21,26 @@ export function init(store) {
weight: 1000,
});
virtualType({
label: 'Launch',
group: 'Root',
namespaced: false,
name: 'launch',
weight: 100,
route: { name: 'c-cluster-apps' },
exact: true,
});
basicType([
'launch',
CATALOG.REPO,
CATALOG.CLUSTER_REPO,
CATALOG.RELEASE,
CATALOG.OPERATION,
]);
formOnlyType(CATALOG.RELEASE);
uncreatableType(CATALOG.RELEASE);
uncreatableType(CATALOG.OPERATION);
headers(CATALOG.RELEASE, [STATE, NAMESPACE_NAME, CHART, RESOURCES, AGE]);
headers(CATALOG.REPO, [NAMESPACE_NAME, URL, AGE]);

View File

@ -45,7 +45,7 @@ export const ADD_SIDECAR = 'add-sidecar';
export const EDIT_CONTAINER = 'container';
// App launch
export const CLUSTER_REPO = 'cluster-repo';
export const REPO_TYPE = 'repo-type';
export const REPO = 'repo';
export const CHART = 'chart';
export const VERSION = 'version';

View File

@ -2,42 +2,22 @@
import jsyaml from 'js-yaml';
import merge from 'lodash/merge';
import Loading from '@/components/Loading';
import ButtonGroup from '@/components/ButtonGroup';
import Checkbox from '@/components/form/Checkbox';
import NameNsDescription from '@/components/form/NameNsDescription';
import LabeledSelect from '@/components/form/LabeledSelect';
import LazyImage from '@/components/LazyImage';
import Markdown from '@/components/Markdown';
import { CATALOG } from '@/config/types';
import { allHash } from '@/utils/promise';
import { sortBy } from '@/utils/sort';
import { defaultAsyncData } from '@/components/ResourceDetail';
import {
CLUSTER_REPO, REPO, CHART, VERSION, STEP
} from '@/config/query-params';
import { findBy } from '@/utils/array';
import { addParams } from '@/utils/url';
import YamlEditor from '@/components/YamlEditor';
import { REPO_TYPE, REPO, CHART, VERSION } from '@/config/query-params';
import Wizard from '@/components/Wizard';
import { CATALOG as CATALOG_ANNOTATIONS, DESCRIPTION } from '@/config/labels-annotations';
import { ensureRegex } from '@/utils/string';
import YamlEditor from '@/components/YamlEditor';
import { DESCRIPTION } from '@/config/labels-annotations';
import { exceptionToErrorsArray } from '@/utils/error';
const CERTIFIED_SORTS = {
[CATALOG_ANNOTATIONS._RANCHER]: 1,
[CATALOG_ANNOTATIONS._EXPERIMENTAL]: 1,
[CATALOG_ANNOTATIONS._PARTNER]: 2,
other: 3,
};
export default {
name: 'ChartInstall',
components: {
ButtonGroup,
Checkbox,
LabeledSelect,
LazyImage,
Loading,
Markdown,
NameNsDescription,
@ -65,54 +45,47 @@ export default {
async fetch() {
const query = this.$route.query;
if ( !this.clusterRepos ) {
await this.loadReposAndCharts();
}
await this.$store.dispatch('catalog/load');
let repoKey;
let repoName;
let repoType;
const repoType = query[REPO_TYPE];
const repoName = query[REPO];
if ( query[CLUSTER_REPO] ) {
repoKey = CLUSTER_REPO;
repoName = query[CLUSTER_REPO];
repoType = CATALOG.CLUSTER_REPO;
} else if ( query[REPO] ) {
repoKey = REPO;
repoName = query[REPO];
repoType = CATALOG.REPO;
}
this.repo = findBy(this.repos, { type: repoType, 'metadata.name': repoName });
this.repo = this.$store.getters['catalog/repo']({ repoType, repoName });
const chartName = query[CHART];
const versionName = query[VERSION];
const version = query[VERSION];
if ( this.repo && chartName ) {
this.chart = findBy(this.allCharts, { [repoKey]: repoName, name: chartName });
if ( this.chart.targetNamespace ) {
this.forceNamespace = this.chart.targetNamespace;
} else {
this.forceNamespace = null;
}
if ( this.chart.targetName ) {
this.value.metadata.name = this.chart.targetName;
this.nameDisabled = true;
} else {
this.nameDisabled = false;
}
if ( this.repo && !this.chart && chartName ) {
this.chart = this.$store.getters['catalog/chart']({
repoType, repoName, chartName
});
}
let version;
if ( this.chart && versionName ) {
version = findBy(this.chart.versions, 'version', versionName);
if ( !this.chart ) {
throw new Error('Chart not found');
}
if ( version && !this.versionInfo ) {
this.versionInfo = await this.repo.followLink('info', { url: addParams(this.repo.links.info, { chartName, version: versionName }) });
if ( this.chart.targetNamespace ) {
this.forceNamespace = this.chart.targetNamespace;
} else {
this.forceNamespace = null;
}
if ( this.chart.targetName ) {
this.value.metadata.name = this.chart.targetName;
this.nameDisabled = true;
} else {
this.nameDisabled = false;
}
if ( version ) {
this.version = this.$store.getters['catalog/version']({
repoType, repoName, chartName, version
});
this.versionInfo = await this.$store.dispatch('catalog/getVersionInfo', {
repoType, repoName, chartName, version
});
this.mergeValues(this.versionInfo.values);
this.valuesYaml = jsyaml.safeDump(this.value.values);
}
@ -124,27 +97,15 @@ export default {
data() {
return {
clusterRepos: null,
namespacedRepos: null,
allCharts: null,
repo: null,
chart: null,
version: null,
versionInfo: null,
valuesYaml: null,
namespace: null,
description: null,
forceNamespace: null,
nameDisabled: false,
searchQuery: '',
sortField: 'certifiedSort',
showDeprecated: false,
showRancher: true,
showPartner: true,
showOther: true,
errors: null,
};
},
@ -169,158 +130,11 @@ export default {
},
];
},
repos() {
const clustered = this.clusterRepos || [];
const namespaced = this.namespacedRepos || [];
return [...clustered, ...namespaced];
},
filteredCharts() {
return (this.allCharts || []).filter((c) => {
if ( c.deprecated && !this.showDeprecated ) {
return false;
}
if ( ( c.certified === CATALOG_ANNOTATIONS._RANCHER && !this.showRancher ) ||
( c.certified === CATALOG_ANNOTATIONS._PARTNER && !this.showPartner ) ||
( c.certified !== CATALOG_ANNOTATIONS._RANCHER && c.certified !== CATALOG_ANNOTATIONS._PARTNER && !this.showOther )
) {
return false;
}
if ( this.searchQuery ) {
const searchTokens = this.searchQuery.split(/\s*[, ]\s*/).map(x => ensureRegex(x, false));
for ( const token of searchTokens ) {
if ( !c.name.match(token) && !c.description.match(token) ) {
return false;
}
}
}
return true;
});
},
arrangedCharts() {
return sortBy(this.filteredCharts, [this.sortField, 'name']);
},
},
watch: { '$route.query': '$fetch' },
methods: {
async loadReposAndCharts() {
let promises = {
clusterRepos: this.$store.dispatch('cluster/findAll', { type: CATALOG.CLUSTER_REPO }),
namespacedRepos: this.$store.dispatch('cluster/findAll', { type: CATALOG.REPO }),
};
const hash = await allHash(promises);
this.clusterRepos = hash.clusterRepos;
this.namespacedRepos = hash.namespacedRepos;
promises = [];
for ( const repo of this.repos ) {
promises.push(repo.followLink('index'));
}
const indexes = await Promise.all(promises);
const charts = {};
for ( let i = 0 ; i < indexes.length ; i++ ) {
const obj = indexes[i];
const repo = this.repos[i];
for ( const k in obj.entries ) {
for ( const entry of obj.entries[k] ) {
addChart(entry, repo);
}
}
}
this.allCharts = sortBy(Object.values(charts), ['key']);
function addChart(chart, repo) {
const key = `${ repo.type }/${ repo.metadata.name }/${ chart.name }`;
const certifiedAnnotation = chart.annotations?.[CATALOG_ANNOTATIONS.CERTIFIED];
let certified = CATALOG_ANNOTATIONS._OTHER;
let sideLabel = null;
// @TODO remove fake hackery
if ( repo.name === 'dev-charts' ) {
certified = CATALOG_ANNOTATIONS._RANCHER;
} else if ( chart.name.startsWith('f') ) {
certified = CATALOG_ANNOTATIONS._PARTNER;
} else if ( repo.isRancher ) {
// Only charts from a rancher repo can actually set the certified flag
certified = certifiedAnnotation || certified;
}
// @TODO remove fake hackery
if ( chart.name.includes('b') ) {
sideLabel = 'Experimental';
} else if ( chart.annotations?.[CATALOG_ANNOTATIONS.EXPERIMENTAL] ) {
sideLabel = 'Experimental';
} else if ( !repo.isRancher && certifiedAnnotation && certified === CATALOG_ANNOTATIONS._OTHER) {
// But anybody can set the side label
sideLabel = certifiedAnnotation;
}
let icon = chart.icon;
if ( icon ) {
icon = icon.replace(/^(https?:\/\/github.com\/[^/]+\/[^/]+)\/blob/, '$1/raw');
}
let obj = charts[key];
if ( !obj ) {
obj = {
key,
icon,
certified,
sideLabel,
certifiedSort: CERTIFIED_SORTS[certified] || 99,
name: chart.name,
description: chart.description,
repoName: repo.name,
versions: [],
deprecated: !!chart.deprecated,
targetNamespace: chart.annotations?.[CATALOG_ANNOTATIONS.NAMESPACE],
targetName: chart.annotations?.[CATALOG_ANNOTATIONS.RELEASE_NAME],
};
if ( repo.type === CATALOG.CLUSTER_REPO ) {
obj.clusterRepo = repo.metadata.name;
} else {
obj.repo = repo.metadata.name;
}
charts[key] = obj;
}
obj.versions.push(chart);
}
},
selectChart(chart) {
this.chart = chart;
this.$router.applyQuery({
[CLUSTER_REPO]: chart.clusterRepo,
[CHART]: chart.name,
[REPO]: chart.repo,
[VERSION]: chart.versions[0].version,
[STEP]: 2
});
},
selectVersion(version) {
this.$router.applyQuery({ [VERSION]: version });
},
@ -375,7 +189,7 @@ export default {
installInput() {
const out = JSON.parse(JSON.stringify(this.value));
out.chartName = this.chart.name;
out.chartName = this.chart.chartName;
out.version = this.$route.query.version;
out.releaseName = out.metadata.name;
out.namespace = out.metadata.namespace;
@ -387,11 +201,6 @@ export default {
return out;
},
focusSearch() {
this.$refs.searchQuery.focus();
this.$refs.searchQuery.select();
}
}
};
</script>
@ -405,40 +214,7 @@ export default {
:errors="errors"
@finish="finish($event)"
>
<template #chart>
<div class="clearfix">
<div class="pull-left">
<Checkbox v-model="showRancher" label="Rancher" class="check-rancher" />
<Checkbox v-model="showPartner" label="Partner" class="check-partner" />
<Checkbox v-model="showOther" label="Other" class="check-other" />
</div>
<div class="pull-right">
<input ref="searchQuery" v-model="searchQuery" type="search" class="input-sm" placeholder="Filter">
<button v-shortkey.once="['/']" class="hide" @shortkey="focusSearch()" />
</div>
<div class="pull-right pt-5 pr-10">
<ButtonGroup v-model="sortField" :options="[{label: 'By Name', value: 'name'}, {label: 'By Kind', value: 'certifiedSort'}]" />
</div>
</div>
<div class="charts">
<div v-for="c in arrangedCharts" :key="c.key" class="chart" :class="{[c.certified]: true}" @click="selectChart(c)">
<div class="side-label">
<label v-if="c.sideLabel">{{ c.sideLabel }}</label>
</div>
<div class="logo">
<LazyImage :src="c.icon" />
</div>
<h4 class="name">
{{ c.name }}
</h4>
<div class="description">
{{ c.description }}
</div>
</div>
</div>
</template>
<template v-if="chart" #helm>
<template #helm>
<div v-if="versionInfo.readme" class="row">
<div class="col span-12">
<Markdown v-model="versionInfo.readme" class="readme" />
@ -448,7 +224,6 @@ export default {
<NameNsDescription
v-model="value"
:mode="mode"
:direct="true"
:name-disabled="nameDisabled"
:force-namespace="forceNamespace"
/>
@ -484,163 +259,11 @@ export default {
</template>
<style lang="scss" scoped>
$chart: 110px;
$side: 15px;
$margin: 10px;
$logo: 60px;
.yaml-editor {
flex: 1;
min-height: 400px;
}
.check-rancher, .check-partner, .check-other {
border-radius: var(--border-radius);
padding: 3px 0 3px 8px;
margin-right: 5px;
}
.check-rancher {
background: var(--app-rancher-bg);
border: 1px solid var(--app-rancher-accent);
}
.check-partner {
background: var(--app-partner-bg);
border: 1px solid var(--app-partner-accent);
}
.check-other {
background: var(--app-other-bg);
border: 1px solid var(--app-other-accent);
}
.charts {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
margin: 0 -1*$margin;
@media only screen and (min-width: map-get($breakpoints, '--viewport-4')) {
.chart {
width: 100%;
}
}
@media only screen and (min-width: map-get($breakpoints, '--viewport-7')) {
.chart {
width: calc(50% - 2 * #{$margin});
}
}
@media only screen and (min-width: map-get($breakpoints, '--viewport-9')) {
.chart {
width: calc(33.33333% - 2 * #{$margin});
}
}
@media only screen and (min-width: map-get($breakpoints, '--viewport-12')) {
.chart {
width: calc(25% - 2 * #{$margin});
}
}
.chart {
height: $chart;
margin: $margin;
padding: $margin;
position: relative;
border-radius: calc( 3 * var(--border-radius));
&:hover {
box-shadow: 0 0 0 2px var(--body-text);
transition: box-shadow 0.1s ease-in-out;
cursor: pointer;
}
.side-label {
transform: rotate(180deg);
position: absolute;
top: 0;
left: 0;
bottom: 0;
min-width: calc(3 * var(--border-radius));
width: $side;
border-top-right-radius: calc( 3 * var(--border-radius));
border-bottom-right-radius: calc( 3 * var(--border-radius));
label {
text-align: center;
writing-mode: tb;
height: 100%;
padding: 0 1px;
display: block;
white-space: no-wrap;
text-overflow: ellipsis;
}
}
.logo {
text-align: center;
position: absolute;
left: $side+$margin;
top: ($chart - $logo)/2;
width: $logo;
height: $logo;
border-radius: calc(5 * var(--border-radius));
overflow: hidden;
background-color: white;
img {
width: $logo - 4px;
height: $logo - 4px;
object-fit: contain;
position: relative;
top: 2px;
}
}
&.rancher {
background: var(--app-rancher-bg);
.side-label {
background-color: var(--app-rancher-accent);
color: var(--app-rancher-accent-text);
}
}
&.partner {
background: var(--app-partner-bg);
.side-label {
background-color: var(--app-partner-accent);
color: var(--app-partner-accent-text);
}
}
&.other {
background: var(--app-other-bg);
.side-label {
background-color: var(--app-other-accent);
color: var(--app-other-accent-text);
}
}
.name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: $margin;
margin-left: $side+$logo+$margin;
}
.description {
margin-top: $margin;
margin-left: $side+$logo+$margin;
margin-right: $margin;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-muted);
}
}
}
.readme {
max-height: calc(100vh - 520px);
margin-bottom: 20px;

View File

@ -1,26 +1,293 @@
<script>
import { NAME as APPS } from '@/config/product/apps';
import Loading from '@/components/Loading';
import { CATALOG } from '@/config/types';
import {
REPO_TYPE, REPO, CHART, VERSION, STEP
} from '@/config/query-params';
import { CATALOG as CATALOG_ANNOTATIONS } from '@/config/labels-annotations';
import { ensureRegex } from '@/utils/string';
import { sortBy } from '@/utils/sort';
import ButtonGroup from '@/components/ButtonGroup';
import Checkbox from '@/components/form/Checkbox';
import LazyImage from '@/components/LazyImage';
export default {
layout: 'plain',
components: {
Loading,
ButtonGroup,
Checkbox,
LazyImage,
async middleware({ redirect, route, store } ) {
const releases = await store.dispatch('cluster/findAll', { type: CATALOG.RELEASE });
let name = 'c-cluster-product-resource';
},
if ( !releases.length ) {
name = 'c-cluster-product-resource-create';
async fetch() {
await this.$store.dispatch('catalog/load');
this.allCharts = this.$store.getters['catalog/charts'];
},
data() {
return {
allCharts: null,
searchQuery: '',
sortField: 'certifiedSort',
showDeprecated: false,
showRancher: true,
showPartner: true,
showOther: true,
};
},
computed: {
filteredCharts() {
return (this.allCharts || []).filter((c) => {
if ( c.deprecated && !this.showDeprecated ) {
return false;
}
if ( ( c.certified === CATALOG_ANNOTATIONS._RANCHER && !this.showRancher ) ||
( c.certified === CATALOG_ANNOTATIONS._PARTNER && !this.showPartner ) ||
( c.certified !== CATALOG_ANNOTATIONS._RANCHER && c.certified !== CATALOG_ANNOTATIONS._PARTNER && !this.showOther )
) {
return false;
}
if ( this.searchQuery ) {
const searchTokens = this.searchQuery.split(/\s*[, ]\s*/).map(x => ensureRegex(x, false));
for ( const token of searchTokens ) {
if ( !c.chartName.match(token) && !c.description.match(token) ) {
return false;
}
}
}
return true;
});
},
arrangedCharts() {
return sortBy(this.filteredCharts, [this.sortField, 'chartName']);
},
},
methods: {
selectChart(chart) {
this.$router.push({
name: 'c-cluster-product-resource-create',
params: {
cluster: this.$route.params.cluster,
product: this.$store.getters['productId'],
resource: CATALOG.RELEASE,
},
query: {
[REPO_TYPE]: chart.repoType,
[REPO]: chart.repoName,
[CHART]: chart.chartName,
[VERSION]: chart.versions[0].version,
[STEP]: 2
}
});
},
focusSearch() {
this.$refs.searchQuery.focus();
this.$refs.searchQuery.select();
}
return redirect({
name,
params: {
...route.params,
product: APPS,
resource: CATALOG.RELEASE,
}
});
}
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else-if="!allCharts.length" class="m-50 text-center">
<h1>There are no charts available, have you added any repos?</h1>
</div>
<div v-else>
<div class="clearfix">
<div class="pull-left">
<Checkbox v-model="showRancher" label="Rancher" class="check-rancher" />
<Checkbox v-model="showPartner" label="Partner" class="check-partner" />
<Checkbox v-model="showOther" label="Other" class="check-other" />
</div>
<div class="pull-right">
<input ref="searchQuery" v-model="searchQuery" type="search" class="input-sm" placeholder="Filter">
<button v-shortkey.once="['/']" class="hide" @shortkey="focusSearch()" />
</div>
<div class="pull-right pt-5 pr-10">
<ButtonGroup v-model="sortField" :options="[{label: 'By Name', value: 'name'}, {label: 'By Kind', value: 'certifiedSort'}]" />
</div>
</div>
<div class="charts">
<div v-for="c in arrangedCharts" :key="c.key" class="chart" :class="{[c.certified]: true}" @click="selectChart(c)">
<div class="side-label">
<label v-if="c.sideLabel">{{ c.sideLabel }}</label>
</div>
<div class="logo">
<LazyImage :src="c.icon" />
</div>
<h4 class="name">
{{ c.chartName }}
</h4>
<div class="description">
{{ c.description }}
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
$chart: 110px;
$side: 15px;
$margin: 10px;
$logo: 60px;
.check-rancher, .check-partner, .check-other {
border-radius: var(--border-radius);
padding: 3px 0 3px 8px;
margin-right: 5px;
}
.check-rancher {
background: var(--app-rancher-bg);
border: 1px solid var(--app-rancher-accent);
}
.check-partner {
background: var(--app-partner-bg);
border: 1px solid var(--app-partner-accent);
}
.check-other {
background: var(--app-other-bg);
border: 1px solid var(--app-other-accent);
}
.charts {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
margin: 0 -1*$margin;
@media only screen and (min-width: map-get($breakpoints, '--viewport-4')) {
.chart {
width: 100%;
}
}
@media only screen and (min-width: map-get($breakpoints, '--viewport-7')) {
.chart {
width: calc(50% - 2 * #{$margin});
}
}
@media only screen and (min-width: map-get($breakpoints, '--viewport-9')) {
.chart {
width: calc(33.33333% - 2 * #{$margin});
}
}
@media only screen and (min-width: map-get($breakpoints, '--viewport-12')) {
.chart {
width: calc(25% - 2 * #{$margin});
}
}
.chart {
height: $chart;
margin: $margin;
padding: $margin;
position: relative;
border-radius: calc( 3 * var(--border-radius));
&:hover {
box-shadow: 0 0 0 2px var(--body-text);
transition: box-shadow 0.1s ease-in-out;
cursor: pointer;
}
.side-label {
transform: rotate(180deg);
position: absolute;
top: 0;
left: 0;
bottom: 0;
min-width: calc(3 * var(--border-radius));
width: $side;
border-top-right-radius: calc( 3 * var(--border-radius));
border-bottom-right-radius: calc( 3 * var(--border-radius));
label {
text-align: center;
writing-mode: tb;
height: 100%;
padding: 0 1px;
display: block;
white-space: no-wrap;
text-overflow: ellipsis;
}
}
.logo {
text-align: center;
position: absolute;
left: $side+$margin;
top: ($chart - $logo)/2;
width: $logo;
height: $logo;
border-radius: calc(5 * var(--border-radius));
overflow: hidden;
background-color: white;
img {
width: $logo - 4px;
height: $logo - 4px;
object-fit: contain;
position: relative;
top: 2px;
}
}
&.rancher {
background: var(--app-rancher-bg);
.side-label {
background-color: var(--app-rancher-accent);
color: var(--app-rancher-accent-text);
}
}
&.partner {
background: var(--app-partner-bg);
.side-label {
background-color: var(--app-partner-accent);
color: var(--app-partner-accent-text);
}
}
&.other {
background: var(--app-other-bg);
.side-label {
background-color: var(--app-other-accent);
color: var(--app-other-accent-text);
}
}
.name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: $margin;
margin-left: $side+$logo+$margin;
}
.description {
margin-top: $margin;
margin-left: $side+$logo+$margin;
margin-right: $margin;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-muted);
}
}
}
</style>

View File

@ -1,6 +1,5 @@
<script>
import { NAME as LOGGING } from '@/config/product/logging';
import { CATALOG } from '@/config/types';
export default {
layout: 'plain',

View File

@ -256,6 +256,10 @@ export default {
existing
});
if ( type === SCHEMA ) {
commit('type-map/schemaChanged', null, { root: true });
}
return getters['byId'](type, id);
},

View File

@ -21,7 +21,7 @@ function SteveFactory(namespace, baseUrl) {
},
types: {},
socket: null,
wantSocket: false,
wantSocket: true,
pendingSends: [],
started: [],
noWatch: [],

View File

@ -102,7 +102,7 @@ export const actions = {
}
},
watch({ dispatch, getters }, {
watch({ state, dispatch, getters }, {
type, selector, id, revision
}) {
type = getters.normalizeType(type);

220
store/catalog.js Normal file
View File

@ -0,0 +1,220 @@
import { CATALOG } from '@/config/types';
import { allHash } from '@/utils/promise';
import { CATALOG as CATALOG_ANNOTATIONS } from '@/config/labels-annotations';
import { findBy } from '@/utils/array';
import { clone } from '@/utils/object';
import { addParams } from '@/utils/url';
export const state = function() {
const out = {
loaded: false,
clusterRepos: null,
namespacedRepos: null,
charts: null,
versionInfos: {},
};
return out;
};
export const getters = {
isLoaded(state) {
return state.loaded;
},
repos(state) {
const clustered = state.clusterRepos || [];
const namespaced = state.namespacedRepos || [];
return [...clustered, ...namespaced];
},
repo(state, getters) {
return ({ repoType, repoName }) => {
const ary = (repoType === 'cluster' ? state.clusterRepos : state.namespacedRepos);
return findBy(ary, 'metadata.name', repoName);
};
},
charts(state) {
return state.charts.slice();
},
chart(state) {
return ({ repoType, repoName, chartName }) => {
const chart = findBy(state.charts, {
repoType, repoName, chartName
});
if ( chart ) {
return clone(chart);
}
};
},
version(state, getters) {
return ({
repoType, repoName, chartName, versionName
}) => {
const chart = getters['chart']({
repoType, repoName, chartName
});
const version = findBy(chart.versions, 'version', versionName);
if ( version ) {
return clone(version);
}
};
}
};
export const mutations = {
setRepos(state, { cluster, namespaced }) {
state.clusterRepos = cluster;
state.namespacedRepos = namespaced;
},
setCharts(state, charts) {
state.charts = charts;
},
cacheVersion(state, { key, info }) {
state.versionInfos[key] = info;
}
};
export const actions = {
async load({ getters, commit, dispatch }, { force } = {}) {
if ( getters.isLoaded && force !== true ) {
return;
}
let promises = {
cluster: dispatch('cluster/findAll', { type: CATALOG.CLUSTER_REPO }, { root: true }),
namespaced: dispatch('cluster/findAll', { type: CATALOG.REPO }, { root: true }),
};
const hash = await allHash(promises);
commit('setRepos', hash);
const repos = getters['repos'];
promises = [];
for ( const repo of repos ) {
promises.push(repo.followLink('index'));
}
const indexes = await Promise.all(promises);
const charts = {};
for ( let i = 0 ; i < indexes.length ; i++ ) {
const obj = indexes[i];
const repo = repos[i];
for ( const k in obj.entries ) {
for ( const entry of obj.entries[k] ) {
addChart(charts, entry, repo);
}
}
}
commit('setCharts', Object.values(charts));
},
async getVersionInfo({ state, getters, commit }, {
repoType, repoName, chartName, version
}) {
const key = `${ repoType }/${ repoName }/${ chartName }/${ version }`;
let info = state.versionInfos[key];
if ( !info ) {
const repo = getters['repo']({ repoType, repoName });
if ( !repo ) {
throw new Error('Repo not found');
}
info = await repo.followLink('info', { url: addParams(repo.links.info, { chartName, version }) });
commit('cacheVersion', { key, info });
}
return info;
},
};
const CERTIFIED_SORTS = {
[CATALOG_ANNOTATIONS._RANCHER]: 1,
[CATALOG_ANNOTATIONS._EXPERIMENTAL]: 1,
[CATALOG_ANNOTATIONS._PARTNER]: 2,
other: 3,
};
function addChart(list, chart, repo) {
const key = `${ repo.type }/${ repo.metadata.name }/${ chart.name }`;
const certifiedAnnotation = chart.annotations?.[CATALOG_ANNOTATIONS.CERTIFIED];
let certified = CATALOG_ANNOTATIONS._OTHER;
let sideLabel = null;
// @TODO remove fake hackery
if ( repo.name === 'dev-charts' ) {
certified = CATALOG_ANNOTATIONS._RANCHER;
} else if ( chart.name.startsWith('f') ) {
certified = CATALOG_ANNOTATIONS._PARTNER;
} else if ( repo.isRancher ) {
// Only charts from a rancher repo can actually set the certified flag
certified = certifiedAnnotation || certified;
}
// @TODO remove fake hackery
if ( chart.name.includes('b') ) {
sideLabel = 'Experimental';
} else if ( chart.annotations?.[CATALOG_ANNOTATIONS.EXPERIMENTAL] ) {
sideLabel = 'Experimental';
} else if ( !repo.isRancher && certifiedAnnotation && certified === CATALOG_ANNOTATIONS._OTHER) {
// But anybody can set the side label
sideLabel = certifiedAnnotation;
}
let icon = chart.icon;
if ( icon ) {
icon = icon.replace(/^(https?:\/\/github.com\/[^/]+\/[^/]+)\/blob/, '$1/raw');
}
let obj = list[key];
if ( !obj ) {
obj = {
key,
icon,
certified,
sideLabel,
certifiedSort: CERTIFIED_SORTS[certified] || 99,
chartName: chart.name,
description: chart.description,
repoName: repo.name,
versions: [],
deprecated: !!chart.deprecated,
targetNamespace: chart.annotations?.[CATALOG_ANNOTATIONS.NAMESPACE],
targetName: chart.annotations?.[CATALOG_ANNOTATIONS.RELEASE_NAME],
};
if ( repo.type === CATALOG.CLUSTER_REPO ) {
obj.repoType = 'cluster';
} else {
obj.repoType = 'namespace';
}
obj.repoName = repo.metadata.name;
list[key] = obj;
}
obj.versions.push(chart);
}

View File

@ -62,14 +62,7 @@ export const getters = {
},
defaultClusterId(state, getters) {
let all;
if ( state.isMultiCluster ) {
all = getters['management/all'](MANAGEMENT.CLUSTER);
} else {
return null;
}
const all = getters['management/all'](MANAGEMENT.CLUSTER);
const clusters = sortBy(filterBy(all, 'isReady'), 'nameDisplay');
const desired = getters['prefs/get'](CLUSTER_PREF);
@ -288,23 +281,15 @@ export const actions = {
let isMultiCluster = false;
const promises = [];
if ( getters['management/schemaFor'](STEVE.CLUSTER) ) {
isMultiCluster = true;
promises.push(dispatch('management/findAll', {
type: STEVE.CLUSTER,
opt: { url: `${ STEVE.CLUSTER }s` }
}));
}
if ( getters['management/schemaFor'](MANAGEMENT.PROJECT) ) {
isMultiCluster = true;
promises.push(dispatch('management/findAll', {
type: MANAGEMENT.CLUSTER,
opt: { url: `${ MANAGEMENT.CLUSTER }s` }
}));
}
promises.push(dispatch('management/findAll', {
type: MANAGEMENT.CLUSTER,
opt: { url: `${ MANAGEMENT.CLUSTER }s` }
}));
await Promise.all(promises);
commit('managementChanged', {

View File

@ -249,6 +249,7 @@ export const state = function() {
typeToComponentMappings: [],
uncreatable: [],
headers: {},
schemaGeneration: 1,
cache: {
typeMove: {},
groupLabel: {},
@ -859,6 +860,13 @@ export const getters = {
const knownTypes = {};
const knownGroups = {};
if ( state.schemaGeneration < 0 ) {
// This does nothing, but makes activeProducts depend on schemaGeneration
// so that it can be used to update the product list on schema change.
return;
}
return state.products.filter((p) => {
const module = p.inStore;
if ( !knownTypes[module] ) {
@ -899,6 +907,10 @@ export const getters = {
};
export const mutations = {
schemaChanged(state) {
state.schemaGeneration = state.schemaGeneration + 1;
},
product(state, obj) {
const existing = findBy(state.products, 'name', obj.name);