mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
chore(kubernetes): Migrate Helm Templates View to React R8S-239 (#587)
This commit is contained in:
parent
ad89df4d0d
commit
264ff5457b
20 changed files with 635 additions and 372 deletions
|
@ -1,5 +0,0 @@
|
|||
import helm from '@/assets/ico/vendor/helm.svg?c';
|
||||
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
|
||||
export const HelmIcon = <BadgeIcon icon={helm} />;
|
|
@ -1,207 +0,0 @@
|
|||
import _ from 'lodash-es';
|
||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
||||
import { HelmIcon } from './HelmIcon';
|
||||
export default class HelmTemplatesController {
|
||||
/* @ngInject */
|
||||
constructor($analytics, $async, $state, $window, $anchorScroll, Authentication, HelmService, KubernetesResourcePoolService, Notifications) {
|
||||
this.$analytics = $analytics;
|
||||
this.$async = $async;
|
||||
this.$window = $window;
|
||||
this.$state = $state;
|
||||
this.$anchorScroll = $anchorScroll;
|
||||
this.Authentication = Authentication;
|
||||
this.HelmService = HelmService;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.Notifications = Notifications;
|
||||
|
||||
this.fallbackIcon = HelmIcon;
|
||||
|
||||
this.editorUpdate = this.editorUpdate.bind(this);
|
||||
this.uiCanExit = this.uiCanExit.bind(this);
|
||||
this.installHelmchart = this.installHelmchart.bind(this);
|
||||
this.getHelmValues = this.getHelmValues.bind(this);
|
||||
this.selectHelmChart = this.selectHelmChart.bind(this);
|
||||
this.getHelmRepoURLs = this.getHelmRepoURLs.bind(this);
|
||||
this.getLatestCharts = this.getLatestCharts.bind(this);
|
||||
this.getResourcePools = this.getResourcePools.bind(this);
|
||||
this.clearHelmChart = this.clearHelmChart.bind(this);
|
||||
|
||||
$window.onbeforeunload = () => {
|
||||
if (this.state.isEditorDirty) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
clearHelmChart() {
|
||||
this.state.chart = null;
|
||||
this.onSelectHelmChart('');
|
||||
}
|
||||
|
||||
editorUpdate(contentvalues) {
|
||||
if (this.state.originalvalues === contentvalues) {
|
||||
this.state.isEditorDirty = false;
|
||||
} else {
|
||||
this.state.values = contentvalues;
|
||||
this.state.isEditorDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
async uiCanExit() {
|
||||
if (this.state.isEditorDirty) {
|
||||
return confirmWebEditorDiscard();
|
||||
}
|
||||
}
|
||||
|
||||
async installHelmchart() {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
const payload = {
|
||||
Name: this.name,
|
||||
Repo: this.state.chart.repo,
|
||||
Chart: this.state.chart.name,
|
||||
Values: this.state.values,
|
||||
Namespace: this.namespace,
|
||||
};
|
||||
await this.HelmService.install(this.endpoint.Id, payload);
|
||||
this.Notifications.success('Success', 'Helm chart successfully installed');
|
||||
this.$analytics.eventTrack('kubernetes-helm-install', { category: 'kubernetes', metadata: { 'chart-name': this.state.chart.name } });
|
||||
this.state.isEditorDirty = false;
|
||||
this.$state.go('kubernetes.applications');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Installation error', err);
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
async getHelmValues() {
|
||||
this.state.loadingValues = true;
|
||||
try {
|
||||
const { values } = await this.HelmService.values(this.state.chart.repo, this.state.chart.name);
|
||||
this.state.values = values;
|
||||
this.state.originalvalues = values;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve helm chart values.');
|
||||
} finally {
|
||||
this.state.loadingValues = false;
|
||||
}
|
||||
}
|
||||
|
||||
async selectHelmChart(chart) {
|
||||
window.scrollTo(0, 0);
|
||||
this.state.showCustomValues = false;
|
||||
this.state.chart = chart;
|
||||
this.onSelectHelmChart(chart.name);
|
||||
await this.getHelmValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This function is used to get the helm repo urls for the endpoint and user
|
||||
* @returns {Promise<string[]>} list of helm repo urls
|
||||
*/
|
||||
async getHelmRepoURLs() {
|
||||
this.state.reposLoading = true;
|
||||
try {
|
||||
// fetch globally set helm repo and user helm repos (parallel)
|
||||
const { GlobalRepository, UserRepositories } = await this.HelmService.getHelmRepositories(this.user.ID);
|
||||
this.state.globalRepository = GlobalRepository;
|
||||
const userHelmReposUrls = UserRepositories.map((repo) => repo.URL);
|
||||
const uniqueHelmRepos = [...new Set([GlobalRepository, ...userHelmReposUrls])].map((url) => url.toLowerCase()).filter((url) => url); // remove duplicates and blank, to lowercase
|
||||
this.state.repos = uniqueHelmRepos;
|
||||
return uniqueHelmRepos;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve helm repo urls.');
|
||||
} finally {
|
||||
this.state.reposLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This function is used to fetch the respective index.yaml files for the provided helm repo urls
|
||||
* @param {string[]} helmRepos list of helm repositories
|
||||
* @param {bool} append append charts returned from repo to existing list of helm charts
|
||||
*/
|
||||
async getLatestCharts(helmRepos) {
|
||||
this.state.chartsLoading = true;
|
||||
try {
|
||||
const promiseList = helmRepos.map((repo) => this.HelmService.search(repo));
|
||||
// fetch helm charts from all the provided helm repositories (parallel)
|
||||
// Promise.allSettled is used to account for promise failure(s) - in cases the user has provided invalid helm repo
|
||||
const chartPromises = await Promise.allSettled(promiseList);
|
||||
const latestCharts = chartPromises
|
||||
.filter((tp) => tp.status === 'fulfilled') // remove failed promises
|
||||
.map((tp) => ({ entries: tp.value.entries, repo: helmRepos[chartPromises.indexOf(tp)] })) // extract chart entries with respective repo data
|
||||
.flatMap(
|
||||
({ entries, repo }) => Object.values(entries).map((charts) => ({ ...charts[0], repo })) // flatten chart entries to single array with respective repo
|
||||
);
|
||||
|
||||
this.state.charts = latestCharts;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve helm repo charts.');
|
||||
} finally {
|
||||
this.state.chartsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async getResourcePools() {
|
||||
this.state.resourcePoolsLoading = true;
|
||||
try {
|
||||
const resourcePools = await this.KubernetesResourcePoolService.get();
|
||||
|
||||
const nonSystemNamespaces = resourcePools.filter(
|
||||
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && resourcePool.Namespace.Status === 'Active'
|
||||
);
|
||||
this.state.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1));
|
||||
this.state.resourcePool = this.state.resourcePools[0];
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve initial helm data.');
|
||||
} finally {
|
||||
this.state.resourcePoolsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
this.user = this.Authentication.getUserDetails();
|
||||
|
||||
this.state = {
|
||||
appName: '',
|
||||
chart: null,
|
||||
showCustomValues: false,
|
||||
actionInProgress: false,
|
||||
resourcePools: [],
|
||||
resourcePool: '',
|
||||
values: null,
|
||||
originalvalues: null,
|
||||
repos: [],
|
||||
charts: [],
|
||||
loadingValues: false,
|
||||
isEditorDirty: false,
|
||||
chartsLoading: false,
|
||||
resourcePoolsLoading: false,
|
||||
viewReady: false,
|
||||
isAdmin: this.Authentication.isAdmin(),
|
||||
globalRepository: undefined,
|
||||
};
|
||||
|
||||
const helmRepos = await this.getHelmRepoURLs();
|
||||
if (helmRepos) {
|
||||
await Promise.all([this.getLatestCharts(helmRepos), this.getResourcePools()]);
|
||||
}
|
||||
if (this.state.charts.length > 0 && this.$state.params.chartName) {
|
||||
const chart = this.state.charts.find((chart) => chart.name === this.$state.params.chartName);
|
||||
if (chart) {
|
||||
this.selectHelmChart(chart);
|
||||
}
|
||||
}
|
||||
|
||||
this.state.viewReady = true;
|
||||
});
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
this.state.isEditorDirty = false;
|
||||
}
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
<div class="row">
|
||||
<!-- helmchart-form -->
|
||||
<div class="col-sm-12 p-0" ng-if="$ctrl.state.chart">
|
||||
<rd-widget>
|
||||
<div class="flex">
|
||||
<div class="basis-3/4 rounded-[8px] m-2 bg-gray-4 th-highcontrast:bg-black th-highcontrast:text-white th-dark:bg-gray-iron-10 th-dark:text-white">
|
||||
<div class="vertical-center p-5">
|
||||
<fallback-image src="$ctrl.state.chart.icon" fallback-icon="$ctrl.fallbackIcon" class-name="'h-16 w-16'" size="'lg'"></fallback-image>
|
||||
<div class="font-medium ml-4">
|
||||
<div class="toolBarTitle text-[24px] mb-2">
|
||||
{{ $ctrl.state.chart.name }}
|
||||
<span class="space-left text-[14px] vertical-center font-normal">
|
||||
<pr-icon icon="'svg-helm'" mode="'primary'"></pr-icon>
|
||||
Helm
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-muted text-xs" ng-bind-html="$ctrl.state.chart.description"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="basis-1/4">
|
||||
<div class="h-full w-full vertical-center justify-end pr-5">
|
||||
<button type="button" class="btn btn-sm btn-link !text-gray-8 hover:no-underline th-highcontrast:!text-white th-dark:!text-white" ng-click="$ctrl.clearHelmChart()">
|
||||
Clear selection
|
||||
<pr-icon icon="'x'" class="ml-1"></pr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget>
|
||||
|
||||
<form class="form-horizontal" name="$ctrl.helmTemplateCreationForm">
|
||||
<div class="form-group mt-4">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
ng-if="!$ctrl.state.showCustomValues && !$ctrl.state.loadingValues"
|
||||
class="btn btn-xs btn-default vertical-center !ml-0 mr-2"
|
||||
ng-click="$ctrl.state.showCustomValues = true;"
|
||||
>
|
||||
<pr-icon icon="'plus'" class="vertical-center"></pr-icon>
|
||||
Show custom values
|
||||
</button>
|
||||
<span class="small interactive vertical-center" ng-if="$ctrl.state.loadingValues" role="status">
|
||||
<inline-loader children="'Loading values.yaml...'" />
|
||||
</span>
|
||||
<button ng-if="$ctrl.state.showCustomValues" class="btn btn-xs btn-default vertical-center !ml-0 mr-2" ng-click="$ctrl.state.showCustomValues = false;">
|
||||
<pr-icon icon="'minus'" class="vertical-center"></pr-icon>
|
||||
Hide custom values
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- values override -->
|
||||
<div ng-if="$ctrl.state.showCustomValues">
|
||||
<!-- web-editor -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<web-editor-form
|
||||
identifier="helm-app-creation-editor"
|
||||
value="$ctrl.state.values"
|
||||
on-change="($ctrl.editorUpdate)"
|
||||
yml="true"
|
||||
placeholder="Define or paste the content of your values yaml file here"
|
||||
>
|
||||
<editor-description class="vertical-center">
|
||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||
<span>
|
||||
You can get more information about Helm values file format in the
|
||||
<a href="https://helm.sh/docs/chart_template_guide/values_files/" target="_blank" class="hyperlink">official documentation</a>.
|
||||
</span>
|
||||
</editor-description>
|
||||
</web-editor-form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !web-editor -->
|
||||
</div>
|
||||
<!-- !values override -->
|
||||
<!-- helm actions -->
|
||||
<div class="col-sm-12 form-section-title"> Actions </div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm !ml-0"
|
||||
ng-disabled="!$ctrl.state.resourcePool || $ctrl.state.loadingValues || $ctrl.state.actionInProgress || !$ctrl.name"
|
||||
ng-click="$ctrl.installHelmchart()"
|
||||
button-spinner="$ctrl.state.actionInProgress"
|
||||
data-cy="helm-install"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Install</span>
|
||||
<span ng-hide="!$ctrl.state.actionInProgress">Installing Helm chart</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !helm actions -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- helmchart-form -->
|
||||
</div>
|
||||
|
||||
<!-- Helm Charts Component -->
|
||||
<div class="row" ng-if="!$ctrl.state.chart">
|
||||
<div class="col-sm-12 p-0">
|
||||
<helm-templates-list
|
||||
title-text="'Helm chart'"
|
||||
charts="$ctrl.state.charts"
|
||||
table-key="$ctrl.state.charts"
|
||||
select-action="$ctrl.selectHelmChart"
|
||||
loading="$ctrl.state.chartsLoading || $ctrl.state.resourcePoolsLoading"
|
||||
>
|
||||
</helm-templates-list>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Helm Charts Component -->
|
|
@ -1,14 +0,0 @@
|
|||
import angular from 'angular';
|
||||
import controller from './helm-templates.controller';
|
||||
|
||||
angular.module('portainer.kubernetes').component('helmTemplatesView', {
|
||||
templateUrl: './helm-templates.html',
|
||||
controller,
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
namespace: '<',
|
||||
stackName: '<',
|
||||
onSelectHelmChart: '<',
|
||||
name: '<',
|
||||
},
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue