mirror of https://github.com/rancher/dashboard.git
713 lines
17 KiB
Vue
713 lines
17 KiB
Vue
<script>
|
|
import AsyncButton from '@shell/components/AsyncButton';
|
|
import Loading from '@shell/components/Loading';
|
|
import { Banner } from '@components/Banner';
|
|
import Carousel from '@shell/components/Carousel';
|
|
import ButtonGroup from '@shell/components/ButtonGroup';
|
|
import SelectIconGrid from '@shell/components/SelectIconGrid';
|
|
import TypeDescription from '@shell/components/TypeDescription';
|
|
import {
|
|
REPO_TYPE, REPO, CHART, VERSION, SEARCH_QUERY, _FLAGGED, CATEGORY, DEPRECATED as DEPRECATED_QUERY, HIDDEN, OPERATING_SYSTEM
|
|
} from '@shell/config/query-params';
|
|
import { lcFirst } from '@shell/utils/string';
|
|
import { sortBy } from '@shell/utils/sort';
|
|
import { mapGetters } from 'vuex';
|
|
import { Checkbox } from '@components/Form/Checkbox';
|
|
import Select from '@shell/components/form/Select';
|
|
import { mapPref, HIDE_REPOS, SHOW_PRE_RELEASE, SHOW_CHART_MODE } from '@shell/store/prefs';
|
|
import { removeObject, addObject, findBy } from '@shell/utils/array';
|
|
import { compatibleVersionsFor, filterAndArrangeCharts } from '@shell/store/catalog';
|
|
import { CATALOG } from '@shell/config/labels-annotations';
|
|
import { isUIPlugin } from '@shell/config/uiplugins';
|
|
import TabTitle from '@shell/components/TabTitle';
|
|
|
|
export default {
|
|
name: 'Charts',
|
|
components: {
|
|
AsyncButton,
|
|
Banner,
|
|
Carousel,
|
|
ButtonGroup,
|
|
Loading,
|
|
Checkbox,
|
|
Select,
|
|
SelectIconGrid,
|
|
TypeDescription,
|
|
TabTitle
|
|
},
|
|
|
|
async fetch() {
|
|
await this.$store.dispatch('catalog/load');
|
|
|
|
const query = this.$route.query;
|
|
|
|
this.searchQuery = query[SEARCH_QUERY] || '';
|
|
this.showDeprecated = query[DEPRECATED_QUERY] === 'true' || query[DEPRECATED_QUERY] === _FLAGGED;
|
|
this.showHidden = query[HIDDEN] === _FLAGGED;
|
|
this.category = query[CATEGORY] || '';
|
|
this.allRepos = this.areAllEnabled();
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
allRepos: null,
|
|
category: null,
|
|
operatingSystem: null,
|
|
searchQuery: null,
|
|
showDeprecated: null,
|
|
showHidden: null,
|
|
chartOptions: [
|
|
{
|
|
label: 'Browse',
|
|
value: 'browse',
|
|
},
|
|
{
|
|
label: 'Featured',
|
|
value: 'featured'
|
|
}
|
|
]
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
...mapGetters(['currentCluster']),
|
|
...mapGetters({ allCharts: 'catalog/charts', loadingErrors: 'catalog/errors' }),
|
|
|
|
chartMode: mapPref(SHOW_CHART_MODE),
|
|
|
|
hideRepos: mapPref(HIDE_REPOS),
|
|
|
|
repoOptions() {
|
|
let nextColor = 0;
|
|
// Colors 3 and 4 match `rancher` and `partner` colors, so just avoid them
|
|
const colors = [1, 2, 5, 6, 7, 8];
|
|
|
|
let out = this.$store.getters['catalog/repos'].map((r) => {
|
|
return {
|
|
_key: r._key,
|
|
label: r.nameDisplay,
|
|
color: r.color,
|
|
weight: ( r.isRancher ? 1 : ( r.isPartner ? 2 : 3 ) ),
|
|
enabled: !this.hideRepos.includes(r._key),
|
|
};
|
|
});
|
|
|
|
out = sortBy(out, ['weight', 'label']);
|
|
|
|
for ( const entry of out ) {
|
|
if ( !entry.color ) {
|
|
entry.color = `color${ colors[nextColor] }`;
|
|
nextColor++;
|
|
if ( nextColor >= colors.length ) {
|
|
nextColor = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return out;
|
|
},
|
|
|
|
repoOptionsForDropdown() {
|
|
return [{
|
|
label: this.t('catalog.repo.all'), all: true, enabled: this.areAllEnabled()
|
|
}, ...this.repoOptions];
|
|
},
|
|
|
|
flattenedRepoNames() {
|
|
const allChecked = this.repoOptionsForDropdown.find((repo) => repo.all && repo.enabled);
|
|
|
|
if (allChecked) {
|
|
return allChecked.label;
|
|
}
|
|
|
|
// None checked
|
|
if (!this.repoOptionsForDropdown.find((repo) => repo.enabled)) {
|
|
return this.t('generic.none');
|
|
}
|
|
|
|
const shownRepos = this.repoOptions.filter((repo) => !this.hideRepos.includes(repo._key));
|
|
const reducedRepos = shownRepos.reduce((acc, c, i) => {
|
|
acc += c.label;
|
|
const length = shownRepos.length;
|
|
|
|
if (i < length - 1) {
|
|
acc += ', ';
|
|
}
|
|
|
|
return acc;
|
|
}, '');
|
|
|
|
return reducedRepos;
|
|
},
|
|
|
|
/**
|
|
* Filter allll charts by invalid entries (deprecated, hidden and ui plugin).
|
|
*
|
|
* This does not include any user provided filters (like selected repos, categories and text query)
|
|
*/
|
|
enabledCharts() {
|
|
return (this.allCharts || []).filter((c) => {
|
|
if ( c.deprecated && !this.showDeprecated ) {
|
|
return false;
|
|
}
|
|
|
|
if ( c.hidden && !this.showHidden ) {
|
|
return false;
|
|
}
|
|
|
|
if (isUIPlugin(c)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Filter enabled charts allll filters. These are what the user will see in the list
|
|
*/
|
|
filteredCharts() {
|
|
return this.filterCharts({
|
|
category: this.category,
|
|
searchQuery: this.searchQuery,
|
|
hideRepos: this.hideRepos
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Filter valid charts (alll filters minus user provided ones) by whether they are featured or not
|
|
*
|
|
* This will power the carousel
|
|
*/
|
|
featuredCharts() {
|
|
const filteredCharts = this.filterCharts({});
|
|
|
|
const featuredCharts = filteredCharts.filter((value) => value.featured).sort((a, b) => a.featured - b.featured);
|
|
|
|
return featuredCharts.slice(0, 5);
|
|
},
|
|
|
|
categories() {
|
|
const map = {};
|
|
|
|
// Filter charts by everything except itself
|
|
const charts = this.filterCharts({
|
|
searchQuery: this.searchQuery,
|
|
hideRepos: this.hideRepos
|
|
});
|
|
|
|
for ( const chart of charts ) {
|
|
for ( const c of chart.categories ) {
|
|
if ( !map[c] ) {
|
|
const labelKey = `catalog.charts.categories.${ lcFirst(c) }`;
|
|
|
|
map[c] = {
|
|
label: this.$store.getters['i18n/withFallback'](labelKey, null, c),
|
|
value: c,
|
|
count: 0
|
|
};
|
|
}
|
|
|
|
map[c].count++;
|
|
}
|
|
}
|
|
|
|
const out = Object.values(map);
|
|
|
|
out.unshift({
|
|
label: this.t('catalog.charts.categories.all'),
|
|
value: '',
|
|
count: charts.length
|
|
});
|
|
|
|
return sortBy(out, ['label']);
|
|
},
|
|
|
|
showCarousel() {
|
|
return this.chartMode === 'featured' && this.featuredCharts.length;
|
|
}
|
|
|
|
},
|
|
|
|
watch: {
|
|
searchQuery(q) {
|
|
this.$router.applyQuery({ [SEARCH_QUERY]: q || undefined });
|
|
},
|
|
|
|
category(cat) {
|
|
this.$router.applyQuery({ [CATEGORY]: cat || undefined });
|
|
},
|
|
|
|
operatingSystem(os) {
|
|
this.$router.applyQuery({ [OPERATING_SYSTEM]: os || undefined });
|
|
},
|
|
|
|
showDeprecated(neu) {
|
|
this.$router.applyQuery({ [DEPRECATED_QUERY]: neu || undefined });
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
colorForChart(chart) {
|
|
const repos = this.repoOptions;
|
|
const repo = findBy(repos, '_key', chart.repoKey);
|
|
|
|
if ( repo ) {
|
|
return repo.color;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
toggleAll(on) {
|
|
for ( const r of this.repoOptions ) {
|
|
this.toggleRepo(r, on, false);
|
|
}
|
|
|
|
this.$nextTick(() => {
|
|
this.allRepos = this.areAllEnabled();
|
|
});
|
|
},
|
|
|
|
areAllEnabled() {
|
|
const all = this.$store.getters['catalog/repos'];
|
|
|
|
for ( const r of all ) {
|
|
if ( this.hideRepos.includes(r._key) ) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
toggleRepo(repo, on, updateAll = true) {
|
|
const hidden = this.hideRepos;
|
|
|
|
if ( on ) {
|
|
removeObject(hidden, repo._key);
|
|
} else {
|
|
addObject(hidden, repo._key);
|
|
}
|
|
|
|
this.hideRepos = hidden;
|
|
|
|
if ( updateAll ) {
|
|
this.allRepos = this.areAllEnabled();
|
|
}
|
|
},
|
|
|
|
selectChart(chart) {
|
|
let version;
|
|
const OSs = this.currentCluster.workerOSs;
|
|
const showPrerelease = this.$store.getters['prefs/get'](SHOW_PRE_RELEASE);
|
|
const compatibleVersions = compatibleVersionsFor(chart, OSs, showPrerelease);
|
|
const versions = chart.versions;
|
|
|
|
if (compatibleVersions.length > 0) {
|
|
version = compatibleVersions[0].version;
|
|
} else {
|
|
version = versions[0].version;
|
|
}
|
|
|
|
const query = {
|
|
[REPO_TYPE]: chart.repoType,
|
|
[REPO]: chart.repoName,
|
|
[CHART]: chart.chartName,
|
|
[VERSION]: version
|
|
};
|
|
|
|
if (chart.deprecated && this.showDeprecated) {
|
|
query[DEPRECATED_QUERY] = 'true';
|
|
}
|
|
|
|
this.$router.push({
|
|
name: 'c-cluster-apps-charts-chart',
|
|
params: {
|
|
cluster: this.$route.params.cluster,
|
|
product: this.$store.getters['productId'],
|
|
},
|
|
query
|
|
});
|
|
},
|
|
|
|
focusSearch() {
|
|
if ( this.$refs.searchQuery ) {
|
|
this.$refs.searchQuery.focus();
|
|
this.$refs.searchQuery.select();
|
|
}
|
|
},
|
|
|
|
async refresh(btnCb) {
|
|
try {
|
|
await this.$store.dispatch('catalog/refresh');
|
|
btnCb(true);
|
|
} catch (e) {
|
|
this.$store.dispatch('growl/fromError', e);
|
|
btnCb(false);
|
|
}
|
|
},
|
|
|
|
filterCharts({ category, searchQuery, hideRepos }) {
|
|
const enabledCharts = (this.enabledCharts || []);
|
|
const clusterProvider = this.currentCluster.status.provider || 'other';
|
|
|
|
return filterAndArrangeCharts(enabledCharts, {
|
|
clusterProvider,
|
|
category,
|
|
searchQuery,
|
|
showDeprecated: this.showDeprecated,
|
|
showHidden: this.showHidden,
|
|
hideRepos,
|
|
hideTypes: [CATALOG._CLUSTER_TPL],
|
|
showPrerelease: this.$store.getters['prefs/get'](SHOW_PRE_RELEASE),
|
|
});
|
|
}
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Loading v-if="$fetchState.pending" />
|
|
<div v-else>
|
|
<header>
|
|
<div class="title">
|
|
<h1
|
|
data-testid="charts-header-title"
|
|
class="m-0"
|
|
>
|
|
<TabTitle>{{ t('catalog.charts.header') }}</TabTitle>
|
|
</h1>
|
|
</div>
|
|
<div
|
|
v-if="featuredCharts.length > 0"
|
|
class="actions-container"
|
|
>
|
|
<ButtonGroup
|
|
v-model:value="chartMode"
|
|
:options="chartOptions"
|
|
/>
|
|
</div>
|
|
</header>
|
|
<div v-if="showCarousel">
|
|
<h3>{{ t('catalog.charts.featuredCharts') }}</h3>
|
|
<Carousel
|
|
:sliders="featuredCharts"
|
|
data-testid="charts-carousel"
|
|
@clicked="(row) => selectChart(row)"
|
|
/>
|
|
</div>
|
|
|
|
<TypeDescription resource="chart" />
|
|
<div class="left-right-split">
|
|
<Select
|
|
:searchable="false"
|
|
:options="repoOptionsForDropdown"
|
|
:value="flattenedRepoNames"
|
|
class="checkbox-select"
|
|
:close-on-select="false"
|
|
data-testid="charts-filter-repos"
|
|
@option:selecting="$event.all ? toggleAll(!$event.enabled) : toggleRepo($event, !$event.enabled) "
|
|
>
|
|
<template #selected-option="selected">
|
|
{{ selected.label }}
|
|
</template>
|
|
<template #option="repo">
|
|
<Checkbox
|
|
:value="repo.enabled"
|
|
:label="repo.label"
|
|
class="pull-left repo in-select"
|
|
:class="{ [repo.color]: true}"
|
|
:color="repo.color"
|
|
>
|
|
<template #label>
|
|
<span>{{ repo.label }}</span><i
|
|
v-if="!repo.all"
|
|
class=" pl-5 icon icon-dot icon-sm"
|
|
:class="{[repo.color]: true}"
|
|
/>
|
|
</template>
|
|
</Checkbox>
|
|
</template>
|
|
</Select>
|
|
|
|
<Select
|
|
v-model:value="category"
|
|
:clearable="false"
|
|
:searchable="false"
|
|
:options="categories"
|
|
placement="bottom"
|
|
label="label"
|
|
style="min-width: 200px;"
|
|
:reduce="opt => opt.value"
|
|
data-testid="charts-filter-category"
|
|
>
|
|
<template #option="opt">
|
|
{{ opt.label }} ({{ opt.count }})
|
|
</template>
|
|
</Select>
|
|
|
|
<div class="filter-block">
|
|
<input
|
|
ref="searchQuery"
|
|
v-model="searchQuery"
|
|
type="search"
|
|
class="input-sm"
|
|
:placeholder="t('catalog.charts.search')"
|
|
data-testid="charts-filter-input"
|
|
>
|
|
|
|
<button
|
|
v-shortkey.once="['/']"
|
|
class="hide"
|
|
@shortkey="focusSearch()"
|
|
/>
|
|
<AsyncButton
|
|
class="refresh-btn"
|
|
mode="refresh"
|
|
size="sm"
|
|
@click="refresh"
|
|
/>
|
|
</div>
|
|
|
|
<div class="mt-10">
|
|
<Checkbox
|
|
v-model:value="showDeprecated"
|
|
:label="t('catalog.charts.deprecatedChartsFilter.label')"
|
|
data-testid="charts-show-deprecated-filter"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Banner
|
|
v-for="(err, i) in loadingErrors"
|
|
:key="i"
|
|
color="error"
|
|
:label="err"
|
|
/>
|
|
|
|
<div v-if="allCharts.length">
|
|
<div
|
|
v-if="filteredCharts.length === 0"
|
|
style="width: 100%;"
|
|
>
|
|
<div class="m-50 text-center">
|
|
<h1>{{ t('catalog.charts.noCharts') }}</h1>
|
|
</div>
|
|
</div>
|
|
<SelectIconGrid
|
|
v-else
|
|
data-testid="chart-selection-grid"
|
|
component-test-id="chart-selection"
|
|
:rows="filteredCharts"
|
|
name-field="chartNameDisplay"
|
|
description-field="chartDescription"
|
|
:color-for="colorForChart"
|
|
@clicked="(row) => selectChart(row)"
|
|
/>
|
|
</div>
|
|
<div
|
|
v-else
|
|
class="m-50 text-center"
|
|
>
|
|
<h1>{{ t('catalog.charts.noCharts') }}</h1>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.left-right-split {
|
|
padding: 0 0 20px 0;
|
|
width: 100%;
|
|
z-index: z-index('fixedTableHeader');
|
|
background: transparent;
|
|
display: grid;
|
|
grid-template-columns: 40% auto auto;
|
|
align-content: center;
|
|
grid-column-gap: 10px;
|
|
|
|
.filter-block {
|
|
display: flex;
|
|
}
|
|
.refresh-btn {
|
|
margin-left: 10px;
|
|
}
|
|
|
|
&.with-os-options {
|
|
grid-template-columns: 40% auto auto auto;
|
|
}
|
|
|
|
@media only screen and (max-width: map-get($breakpoints, '--viewport-12')) {
|
|
&{
|
|
grid-template-columns: auto auto !important;
|
|
grid-template-rows: 40px 40px;
|
|
grid-row-gap: 20px;
|
|
}
|
|
}
|
|
|
|
@media only screen and (max-width: map-get($breakpoints, '--viewport-7')) {
|
|
&{
|
|
&{
|
|
grid-template-columns: auto !important;
|
|
grid-template-rows: 40px 40px 40px !important;
|
|
|
|
&.with-os-options {
|
|
grid-template-rows: 40px 40px 40px 40px !important;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.checkbox-select {
|
|
.vs__search {
|
|
position: absolute;
|
|
right: 0
|
|
}
|
|
|
|
.vs__selected-options {
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
display: inline-block;
|
|
line-height: 2.4rem;
|
|
}
|
|
}
|
|
|
|
.checkbox-outer-container.in-select {
|
|
transform: translateX(-5px);
|
|
padding: 7px 0 6px 13px;
|
|
width: calc(100% + 10px);
|
|
|
|
:deep() .checkbox-label {
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
& i {
|
|
line-height: inherit;
|
|
}
|
|
}
|
|
|
|
&:first-child {
|
|
&:hover {
|
|
background: var(--input-hover-bg);
|
|
}
|
|
}
|
|
|
|
&:hover :deep() .checkbox-label {
|
|
color: var(--body-text);
|
|
}
|
|
|
|
&.rancher {
|
|
&:hover {
|
|
background: var(--app-rancher-accent);
|
|
}
|
|
&:hover :deep() .checkbox-label {
|
|
color: var(--app-rancher-accent-text);
|
|
}
|
|
& i {
|
|
color: var(--app-rancher-accent)
|
|
}
|
|
}
|
|
|
|
&.partner {
|
|
&:hover {
|
|
background: var(--app-partner-accent);
|
|
}
|
|
&:hover :deep() .checkbox-label {
|
|
color: var(--app-partner-accent-text);
|
|
}
|
|
& i {
|
|
color: var(--app-partner-accent)
|
|
}
|
|
}
|
|
|
|
&.color1 {
|
|
&:hover {
|
|
background: var(--app-color1-accent);
|
|
}
|
|
&:hover :deep() .checkbox-label {
|
|
color: var(--app-color1-accent-text);
|
|
}
|
|
& i {
|
|
color: var(--app-color1-accent)
|
|
}
|
|
}
|
|
&.color2 {
|
|
&:hover {
|
|
background: var(--app-color2-accent);
|
|
}
|
|
&:hover :deep() .checkbox-label {
|
|
color: var(--app-color2-accent-text);
|
|
}
|
|
& i {
|
|
color: var(--app-color2-accent)
|
|
}
|
|
}
|
|
&.color3 {
|
|
&:hover {
|
|
background: var(--app-color3-accent);
|
|
}
|
|
&:hover :deep() .checkbox-label {
|
|
color: var(--app-color3-accent-text);
|
|
}
|
|
& i {
|
|
color: var(--app-color3-accent)
|
|
}
|
|
}
|
|
&.color4 {
|
|
&:hover {
|
|
background: var(--app-color4-accent);
|
|
}
|
|
&:hover :deep().checkbox-label {
|
|
color: var(--app-color4-accent-text);
|
|
}
|
|
& i {
|
|
color: var(--app-color4-accent)
|
|
}
|
|
}
|
|
&.color5 {
|
|
&:hover {
|
|
background: var(--app-color5-accent);
|
|
}
|
|
&:hover :deep() .checkbox-label {
|
|
color: var(--app-color5-accent-text);
|
|
}
|
|
& i {
|
|
color: var(--app-color5-accent)
|
|
}
|
|
}
|
|
&.color6 {
|
|
&:hover {
|
|
background: var(--app-color6-accent);
|
|
}
|
|
&:hover :deep() .checkbox-label {
|
|
color: var(--app-color6-accent-text);
|
|
}
|
|
& i {
|
|
color: var(--app-color6-accent)
|
|
}
|
|
}
|
|
&.color7 {
|
|
&:hover {
|
|
background: var(--app-color7-accent);
|
|
}
|
|
&:hover :deep() .checkbox-label {
|
|
color: var(--app-color7-accent-text);
|
|
}
|
|
& i {
|
|
color: var(--app-color7-accent)
|
|
}
|
|
}
|
|
&.color8 {
|
|
&:hover {
|
|
background: var(--app-color8-accent);
|
|
}
|
|
& i {
|
|
color: var(--app-color8-accent)
|
|
}
|
|
}
|
|
}
|
|
|
|
</style>
|