mirror of https://github.com/rancher/dashboard.git
Split launch out, single vs mcm fixes, update products on schema change
This commit is contained in:
parent
3150bdeeff
commit
8e4649010b
|
|
@ -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: |
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@ export default {
|
|||
|
||||
if ( !namespace ) {
|
||||
namespace = this.$store.getters['defaultNamespace'];
|
||||
if ( metadata ) {
|
||||
metadata.namespace = namespace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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?$/);
|
||||
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { NAME as LOGGING } from '@/config/product/logging';
|
||||
import { CATALOG } from '@/config/types';
|
||||
|
||||
export default {
|
||||
layout: 'plain',
|
||||
|
|
|
|||
|
|
@ -256,6 +256,10 @@ export default {
|
|||
existing
|
||||
});
|
||||
|
||||
if ( type === SCHEMA ) {
|
||||
commit('type-map/schemaChanged', null, { root: true });
|
||||
}
|
||||
|
||||
return getters['byId'](type, id);
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function SteveFactory(namespace, baseUrl) {
|
|||
},
|
||||
types: {},
|
||||
socket: null,
|
||||
wantSocket: false,
|
||||
wantSocket: true,
|
||||
pendingSends: [],
|
||||
started: [],
|
||||
noWatch: [],
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export const actions = {
|
|||
}
|
||||
},
|
||||
|
||||
watch({ dispatch, getters }, {
|
||||
watch({ state, dispatch, getters }, {
|
||||
type, selector, id, revision
|
||||
}) {
|
||||
type = getters.normalizeType(type);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue