1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge pull request #2218 from lensapp/release/v4.1.3

Release v4.1.3
This commit is contained in:
Jari Kolehmainen 2021-02-24 19:00:15 +02:00 committed by GitHub
commit 39947aea5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 423 additions and 135 deletions

View File

@ -5,7 +5,7 @@ Lens is lightweight and simple to install. You'll be up and running in just a fe
## System Requirements
Review the [System Requirements](/supporting/requirements/) to check if your computer configuration is supported.
Review the [System Requirements](../supporting/requirements.md) to check if your computer configuration is supported.
## macOS

View File

@ -4,7 +4,7 @@ Lens has integration to Helm making it easy to install and manage Helm charts an
![Helm Charts](images/helm-charts.png)
## Managing Helm Reporistories
## Managing Helm Repositories
Used Helm repositories are possible to configure in the [Preferences](/getting-started/preferences). Lens app will fetch available Helm repositories from the [Artifact HUB](https://artifacthub.io/) and automatically add `bitnami` repository by default if no other repositories are already configured. If any other repositories are needed to add, those can be added manually via command line. **Note!** Configured Helm repositories are added globally to user's computer, so other processes can see those as well.
@ -18,4 +18,4 @@ Lens will list all charts from configured Helm repositries on Apps section. To i
To update a Helm release, you can open the release details and modify the release values and click "Save" button. To upgrade or downgrade the release, click "Upgrade" button in the release details. In the release editor you can select a new chart version and edit the release values if needed and then click "Upgrade" or "Upgrade and Close" button.
## Deleting a Helm Release
To delete existing Helm release open the release details and click trash can icon on the top of the panel. Deletion removes all Kubernetes resources created by the Helm release. **Note!** If the release included any persistent volumes, those are required to remove manually!
To delete existing Helm release open the release details and click trash can icon on the top of the panel. Deletion removes all Kubernetes resources created by the Helm release. **Note!** If the release included any persistent volumes, those are required to remove manually!

View File

@ -2,7 +2,7 @@
"name": "kontena-lens",
"productName": "Lens",
"description": "Lens - The Kubernetes IDE",
"version": "4.1.2",
"version": "4.1.3",
"main": "static/build/main.js",
"copyright": "© 2020, Mirantis, Inc.",
"license": "MIT",

View File

@ -8,7 +8,7 @@ interface StatusBarComponents {
}
interface StatusBarRegistrationV2 {
components: StatusBarComponents;
components?: StatusBarComponents; // has to be optional for backwards compatability
}
export interface StatusBarRegistration extends StatusBarRegistrationV2 {

View File

@ -5,6 +5,8 @@ import { delay } from "../common/utils";
import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
import { ipcMain } from "electron";
let installVersion: null | string = null;
function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: UpdateAvailableToBackchannel) {
if (arg.doUpdate) {
if (arg.now) {
@ -37,6 +39,22 @@ export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void {
autoUpdater
.on("update-available", (args: UpdateInfo) => {
if (autoUpdater.autoInstallOnAppQuit) {
// a previous auto-update loop was completed with YES+LATER, check if same version
if (installVersion === args.version) {
// same version, don't broadcast
return;
}
}
/**
* This should be always set to false here because it is the reasonable
* default. Namely, if a don't auto update to a version that the user
* didn't ask for.
*/
autoUpdater.autoInstallOnAppQuit = false;
installVersion = args.version;
try {
const backchannel = `auto-update:${args.version}`;
@ -53,6 +71,7 @@ export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void {
broadcastMessage(UpdateAvailableChannel, backchannel, args);
} catch (error) {
logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error });
installVersion = undefined;
}
});

View File

@ -0,0 +1,108 @@
import { HelmRepo, HelmRepoManager } from "../helm-repo-manager";
export class HelmChartManager {
private cache: any = {};
private repo: HelmRepo;
constructor(repo: HelmRepo){
this.cache = HelmRepoManager.cache;
this.repo = repo;
}
public async charts(): Promise<any> {
switch (this.repo.name) {
case "stable":
return Promise.resolve({
"apm-server": [
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.7",
repo: "stable",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.6",
repo: "stable",
digest: "test"
}
],
"redis": [
{
apiVersion: "3.0.0",
name: "apm-server",
version: "1.0.0",
repo: "stable",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "0.0.9",
repo: "stable",
digest: "test"
}
]
});
case "experiment":
return Promise.resolve({
"fairwind": [
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.1",
repo: "experiment",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.2",
repo: "experiment",
digest: "test",
deprecated: true
}
]
});
case "bitnami":
return Promise.resolve({
"hotdog": [
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.1",
repo: "bitnami",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.2",
repo: "bitnami",
digest: "test",
}
],
"pretzel": [
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0",
repo: "bitnami",
digest: "test",
},
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0.1",
repo: "bitnami",
digest: "test"
}
]
});
default:
return Promise.resolve({});
}
}
}

View File

@ -0,0 +1,104 @@
import { helmService } from "../helm-service";
import { repoManager } from "../helm-repo-manager";
jest.spyOn(repoManager, "init").mockImplementation();
jest.mock("../helm-chart-manager");
describe("Helm Service tests", () => {
test("list charts without deprecated ones", async () => {
jest.spyOn(repoManager, "repositories").mockImplementation(async () => {
return [
{ name: "stable", url: "stableurl" },
{ name: "experiment", url: "experimenturl" }
];
});
const charts = await helmService.listCharts();
expect(charts).toEqual({
stable: {
"apm-server": [
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.7",
repo: "stable",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.6",
repo: "stable",
digest: "test"
}
],
"redis": [
{
apiVersion: "3.0.0",
name: "apm-server",
version: "1.0.0",
repo: "stable",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "0.0.9",
repo: "stable",
digest: "test"
}
]
},
experiment: {}
});
});
test("list charts sorted by version in descending order", async () => {
jest.spyOn(repoManager, "repositories").mockImplementation(async () => {
return [
{ name: "bitnami", url: "bitnamiurl" }
];
});
const charts = await helmService.listCharts();
expect(charts).toEqual({
bitnami: {
"hotdog": [
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.2",
repo: "bitnami",
digest: "test",
},
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.1",
repo: "bitnami",
digest: "test"
},
],
"pretzel": [
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0.1",
repo: "bitnami",
digest: "test",
},
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0",
repo: "bitnami",
digest: "test"
}
]
}
});
});
});

View File

@ -4,9 +4,10 @@ import { HelmRepo, HelmRepoManager } from "./helm-repo-manager";
import logger from "../logger";
import { promiseExec } from "../promise-exec";
import { helmCli } from "./helm-cli";
import type { RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
type CachedYaml = {
entries: any; // todo: types
entries: RepoHelmChartList
};
export class HelmChartManager {
@ -24,15 +25,15 @@ export class HelmChartManager {
return charts[name];
}
public async charts(): Promise<any> {
public async charts(): Promise<RepoHelmChartList> {
try {
const cachedYaml = await this.cachedYaml();
return cachedYaml["entries"];
} catch(error) {
logger.error(error);
logger.error("HELM-CHART-MANAGER]: failed to list charts", { error });
return [];
return {};
}
}

View File

@ -1,8 +1,10 @@
import semver from "semver";
import { Cluster } from "../cluster";
import logger from "../logger";
import { repoManager } from "./helm-repo-manager";
import { HelmChartManager } from "./helm-chart-manager";
import { releaseManager } from "./helm-release-manager";
import { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
class HelmService {
public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) {
@ -10,7 +12,7 @@ class HelmService {
}
public async listCharts() {
const charts: any = {};
const charts: HelmChartList = {};
await repoManager.init();
const repositories = await repoManager.repositories();
@ -18,14 +20,10 @@ class HelmService {
for (const repo of repositories) {
charts[repo.name] = {};
const manager = new HelmChartManager(repo);
let entries = await manager.charts();
const sortedCharts = this.sortChartsByVersion(await manager.charts());
const enabledCharts = this.excludeDeprecatedChartGroups(sortedCharts);
entries = this.excludeDeprecated(entries);
for (const key in entries) {
entries[key] = entries[key][0];
}
charts[repo.name] = entries;
charts[repo.name] = enabledCharts;
}
return charts;
@ -96,20 +94,30 @@ class HelmService {
return { message: output };
}
protected excludeDeprecated(entries: any) {
for (const key in entries) {
entries[key] = entries[key].filter((entry: any) => {
if (Array.isArray(entry)) {
return entry[0]["deprecated"] != true;
}
private excludeDeprecatedChartGroups(chartGroups: RepoHelmChartList) {
const groups = new Map(Object.entries(chartGroups));
return entry["deprecated"] != true;
for (const [chartName, charts] of groups) {
if (charts[0].deprecated) {
groups.delete(chartName);
}
}
return Object.fromEntries(groups);
}
private sortChartsByVersion(chartGroups: RepoHelmChartList) {
for (const key in chartGroups) {
chartGroups[key] = chartGroups[key].sort((first, second) => {
const firstVersion = semver.coerce(first.version || 0);
const secondVersion = semver.coerce(second.version || 0);
return semver.compare(secondVersion, firstVersion);
});
}
return entries;
return chartGroups;
}
}
export const helmService = new HelmService();

View File

@ -3,11 +3,8 @@ import { apiBase } from "../index";
import { stringify } from "querystring";
import { autobind } from "../../utils";
interface IHelmChartList {
[repo: string]: {
[name: string]: HelmChart;
};
}
export type RepoHelmChartList = Record<string, HelmChart[]>;
export type HelmChartList = Record<string, RepoHelmChartList>;
export interface IHelmChartDetails {
readme: string;
@ -22,12 +19,12 @@ const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: {
export const helmChartsApi = {
list() {
return apiBase
.get<IHelmChartList>(endpoint())
.get<HelmChartList>(endpoint())
.then(data => {
return Object
.values(data)
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), [])
.map(HelmChart.create);
.map(([chart]) => HelmChart.create(chart));
});
},

View File

@ -272,6 +272,7 @@ export class KubeApi<T extends KubeObject = any> {
}
protected parseResponse(data: KubeJsonApiData | KubeJsonApiData[] | KubeJsonApiDataList, namespace?: string): any {
if (!data) return;
const KubeObjectConstructor = this.objectConstructor;
if (KubeObject.isJsonApiData(data)) {

View File

@ -82,9 +82,9 @@ export class HelmChartDetails extends Component<Props> {
<div className="intro-contents box grow">
<div className="description flex align-center justify-space-between">
{selectedChart.getDescription()}
<Button primary label={`Install`} onClick={this.install} />
<Button primary label="Install" onClick={this.install} />
</div>
<DrawerItem name={`Version`} className="version" onClick={stopPropagation}>
<DrawerItem name="Version" className="version" onClick={stopPropagation}>
<Select
themeName="outlined"
menuPortalTarget={null}
@ -93,16 +93,16 @@ export class HelmChartDetails extends Component<Props> {
onChange={onVersionChange}
/>
</DrawerItem>
<DrawerItem name={`Home`}>
<DrawerItem name="Home">
<a href={selectedChart.getHome()} target="_blank" rel="noreferrer">{selectedChart.getHome()}</a>
</DrawerItem>
<DrawerItem name={`Maintainers`} className="maintainers">
<DrawerItem name="Maintainers" className="maintainers">
{selectedChart.getMaintainers().map(({ name, email, url }) =>
<a key={name} href={url || `mailto:${email}`} target="_blank" rel="noreferrer">{name}</a>
)}
</DrawerItem>
{selectedChart.getKeywords().length > 0 && (
<DrawerItem name={`Keywords`} labelsOnly>
<DrawerItem name="Keywords" labelsOnly>
{selectedChart.getKeywords().map(key => <Badge key={key} label={key} />)}
</DrawerItem>
)}

View File

@ -72,11 +72,8 @@ export class HelmCharts extends Component<Props> {
(chart: HelmChart) => chart.getAppVersion(),
(chart: HelmChart) => chart.getKeywords(),
]}
filterItems={[
(items: HelmChart[]) => items.filter(item => !item.deprecated)
]}
customizeHeader={() => (
<SearchInputUrl placeholder={`Search Helm Charts`} />
<SearchInputUrl placeholder="Search Helm Charts" />
)}
renderTableHeader={[
{ className: "icon", showWithColumn: columnId.name },

View File

@ -111,7 +111,7 @@ export class ReleaseDetails extends Component<Props> {
return (
<div className="values">
<DrawerTitle title={`Values`}/>
<DrawerTitle title="Values"/>
<div className="flex column gaps">
<AceEditor
mode="yaml"
@ -120,7 +120,7 @@ export class ReleaseDetails extends Component<Props> {
/>
<Button
primary
label={`Save`}
label="Save"
waiting={saving}
onClick={this.updateValues}
/>
@ -200,7 +200,7 @@ export class ReleaseDetails extends Component<Props> {
<span>{release.getChart()}</span>
<Button
primary
label={`Upgrade`}
label="Upgrade"
className="box right upgrade"
onClick={this.upgradeVersion}
/>
@ -226,9 +226,9 @@ export class ReleaseDetails extends Component<Props> {
/>
</DrawerItem>
{this.renderValues()}
<DrawerTitle title={`Notes`}/>
<DrawerTitle title="Notes"/>
{this.renderNotes()}
<DrawerTitle title={`Resources`}/>
<DrawerTitle title="Resources"/>
{this.renderResources()}
</div>
);

View File

@ -42,7 +42,7 @@ export class HelmReleaseMenu extends React.Component<Props> {
<>
{hasRollback && (
<MenuItem onClick={this.rollback}>
<Icon material="history" interactive={toolbar} title={`Rollback`}/>
<Icon material="history" interactive={toolbar} title="Rollback"/>
<span className="title">Rollback</span>
</MenuItem>
)}

View File

@ -143,7 +143,7 @@ export const ClusterPieCharts = observer(() => {
<div className="chart flex column align-center box grow">
<PieChart
data={cpuData}
title={`CPU`}
title="CPU"
legendColors={["#c93dce", "#4caf50", "#3d90ce", defaultColor]}
/>
{cpuLimitsOverload && renderLimitWarning()}
@ -151,7 +151,7 @@ export const ClusterPieCharts = observer(() => {
<div className="chart flex column align-center box grow">
<PieChart
data={memoryData}
title={`Memory`}
title="Memory"
legendColors={["#c93dce", "#4caf50", "#3d90ce", defaultColor]}
/>
{memoryLimitsOverload && renderLimitWarning()}
@ -159,7 +159,7 @@ export const ClusterPieCharts = observer(() => {
<div className="chart flex column align-center box grow">
<PieChart
data={podsData}
title={`Pods`}
title="Pods"
legendColors={["#4caf50", defaultColor]}
/>
</div>

View File

@ -63,21 +63,21 @@ export class LimitRangeDetails extends React.Component<Props> {
<KubeObjectMeta object={limitRange}/>
{containerLimits.length > 0 &&
<DrawerItem name={`Container Limits`} labelsOnly>
<DrawerItem name="Container Limits" labelsOnly>
{
renderLimitDetails(containerLimits, [Resource.CPU, Resource.MEMORY, Resource.EPHEMERAL_STORAGE])
}
</DrawerItem>
}
{podLimits.length > 0 &&
<DrawerItem name={`Pod Limits`} labelsOnly>
<DrawerItem name="Pod Limits" labelsOnly>
{
renderLimitDetails(podLimits, [Resource.CPU, Resource.MEMORY, Resource.EPHEMERAL_STORAGE])
}
</DrawerItem>
}
{pvcLimits.length > 0 &&
<DrawerItem name={`Persistent Volume Claim Limits`} labelsOnly>
<DrawerItem name="Persistent Volume Claim Limits" labelsOnly>
{
renderLimitDetails(pvcLimits, [Resource.STORAGE])
}

View File

@ -146,7 +146,7 @@ export class AddQuotaDialog extends React.Component<Props> {
<div className="flex gaps">
<Input
required autoFocus
placeholder={`ResourceQuota name`}
placeholder="ResourceQuota name"
validators={systemName}
value={this.quotaName} onChange={v => this.quotaName = v.toLowerCase()}
className="box grow"
@ -156,7 +156,7 @@ export class AddQuotaDialog extends React.Component<Props> {
<SubTitle title="Namespace" />
<NamespaceSelect
value={this.namespace}
placeholder={`Namespace`}
placeholder="Namespace"
themeName="light"
className="box grow"
onChange={({ value }) => this.namespace = value}
@ -167,14 +167,14 @@ export class AddQuotaDialog extends React.Component<Props> {
<Select
className="quota-select"
themeName="light"
placeholder={`Select a quota..`}
placeholder="Select a quota.."
options={this.quotaOptions}
value={this.quotaSelectValue}
onChange={({ value }) => this.quotaSelectValue = value}
/>
<Input
maxLength={10}
placeholder={`Value`}
placeholder="Value"
value={this.quotaInputValue}
onChange={v => this.quotaInputValue = v}
onKeyDown={this.onInputQuota}
@ -183,7 +183,7 @@ export class AddQuotaDialog extends React.Component<Props> {
<Button round primary onClick={this.setQuota}>
<Icon
material={this.quotas[this.quotaSelectValue] ? "edit" : "add"}
tooltip={`Set quota`}
tooltip="Set quota"
/>
</Button>
</div>

View File

@ -133,7 +133,7 @@ export class AddSecretDialog extends React.Component<Props> {
<SubTitle compact className="fields-title" title={upperFirst(field.toString())}>
<Icon
small
tooltip={`Add field`}
tooltip="Add field"
material="add_circle_outline"
onClick={() => this.addField(field)}
/>
@ -146,7 +146,7 @@ export class AddSecretDialog extends React.Component<Props> {
<div key={index} className="secret-field flex gaps auto align-center">
<Input
className="key"
placeholder={`Name`}
placeholder="Name"
title={key}
tabIndex={required ? -1 : 0}
readOnly={required}
@ -156,7 +156,7 @@ export class AddSecretDialog extends React.Component<Props> {
multiLine maxRows={5}
required={required}
className="value"
placeholder={`Value`}
placeholder="Value"
value={value} onChange={v => item.value = v}
/>
<Icon
@ -194,7 +194,7 @@ export class AddSecretDialog extends React.Component<Props> {
<SubTitle title="Secret name" />
<Input
autoFocus required
placeholder={`Name`}
placeholder="Name"
validators={systemName}
value={name} onChange={v => this.name = v}
/>

View File

@ -69,7 +69,7 @@ export class SecretDetails extends React.Component<Props> {
</DrawerItem>
{!isEmpty(this.data) && (
<>
<DrawerTitle title={`Data`}/>
<DrawerTitle title="Data"/>
{
Object.entries(this.data).map(([name, value]) => {
const revealSecret = this.revealSecret[name];
@ -107,7 +107,7 @@ export class SecretDetails extends React.Component<Props> {
}
<Button
primary
label={`Save`} waiting={this.isSaving}
label="Save" waiting={this.isSaving}
className="save-btn"
onClick={this.saveSecret}
/>

View File

@ -72,7 +72,7 @@ export class AddNamespaceDialog extends React.Component<Props> {
<Input
required autoFocus
iconLeft="layers"
placeholder={`Namespace`}
placeholder="Namespace"
validators={systemName}
value={namespace} onChange={v => this.namespace = v.toLowerCase()}
/>

View File

@ -60,7 +60,7 @@ export class NamespaceDetails extends React.Component<Props> {
);
})}
</DrawerItem>
<DrawerItem name={`Limit Ranges`}>
<DrawerItem name="Limit Ranges">
{!this.limitranges && limitRangeStore.isLoading && <Spinner/>}
{this.limitranges.map(limitrange => {
return (

View File

@ -56,7 +56,7 @@ export class NamespaceSelectFilter extends React.Component {
if (namespace) {
namespaceStore.toggleContext(namespace);
} else {
namespaceStore.resetContext(); // "All namespaces" clicked, empty list considered as "all"
namespaceStore.toggleAll(false); // "All namespaces" clicked
}
}

View File

@ -29,6 +29,7 @@ export class NamespaceSelect extends React.Component<Props> {
disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([namespaceStore], {
preload: true,
loadOnce: true, // skip reloading namespaces on every render / page visit
})
]);
}

View File

@ -5,7 +5,7 @@ import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
import { createPageParam } from "../../navigation";
import { apiManager } from "../../api/api-manager";
const storage = createStorage<string[]>("context_namespaces", []);
const storage = createStorage<string[]>("context_namespaces");
export const namespaceUrlParam = createPageParam<string[]>({
name: "namespaces",
@ -74,11 +74,11 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
@computed
private get initialNamespaces(): string[] {
const namespaces = new Set(this.allowedNamespaces);
const prevSelected = storage.get().filter(namespace => namespaces.has(namespace));
const prevSelectedNamespaces = storage.get();
// return previously saved namespaces from local-storage
if (prevSelected.length > 0) {
return prevSelected;
// return previously saved namespaces from local-storage (if any)
if (prevSelectedNamespaces) {
return prevSelectedNamespaces.filter(namespace => namespaces.has(namespace));
}
// otherwise select "default" or first allowed namespace
@ -120,7 +120,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
protected async loadItems(params: KubeObjectStoreLoadingParams) {
const { allowedNamespaces } = this;
let namespaces = await super.loadItems(params);
let namespaces = (await super.loadItems(params)) || [];
namespaces = namespaces.filter(namespace => allowedNamespaces.includes(namespace.getName()));
@ -166,7 +166,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
if (showAll) {
this.setContext(this.allowedNamespaces);
} else {
this.contextNs.clear();
this.resetContext(); // empty context considered as "All namespaces"
}
} else {
this.toggleAll(!this.hasAllContexts);

View File

@ -117,7 +117,7 @@ export class NetworkPolicyDetails extends React.Component<Props> {
{ingress && (
<>
<DrawerTitle title={`Ingress`}/>
<DrawerTitle title="Ingress"/>
{ingress.map((ingress, i) => {
const { ports } = ingress;

View File

@ -50,7 +50,7 @@ export class ServiceDetails extends React.Component<Props> {
{spec.sessionAffinity}
</DrawerItem>
<DrawerTitle title={`Connection`}/>
<DrawerTitle title="Connection"/>
<DrawerItem name="Cluster IP">
{spec.clusterIP}
@ -77,7 +77,7 @@ export class ServiceDetails extends React.Component<Props> {
{spec.loadBalancerIP}
</DrawerItem>
)}
<DrawerTitle title={`Endpoint`}/>
<DrawerTitle title="Endpoint"/>
<ServiceDetailsEndpoint endpoint={endpoint}/>
</div>

View File

@ -37,7 +37,7 @@ export class ServicePortComponent extends React.Component<Props> {
return (
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
<span title={`Open in a browser`} onClick={() => this.portForward() }>
<span title="Open in a browser" onClick={() => this.portForward() }>
{port.toString()}
{this.waiting && (
<Spinner />

View File

@ -111,7 +111,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
<>
<SubTitle title="Security settings" />
<Checkbox
label={`Skip TLS certificate checks for the repository`}
label="Skip TLS certificate checks for the repository"
value={this.helmRepo.insecureSkipTlsVerify}
onChange={v => this.helmRepo.insecureSkipTlsVerify = v}
/>
@ -120,12 +120,12 @@ export class AddHelmRepoDialog extends React.Component<Props> {
{this.renderFileInput(`Cerificate file`, FileType.CertFile, AddHelmRepoDialog.certExtensions)}
<SubTitle title="Chart Repository Credentials" />
<Input
placeholder={`Username`}
placeholder="Username"
value={this.helmRepo.username} onChange= {v => this.helmRepo.username = v}
/>
<Input
type="password"
placeholder={`Password`}
placeholder="Password"
value={this.helmRepo.password} onChange={v => this.helmRepo.password = v}
/>
</>);
@ -148,13 +148,13 @@ export class AddHelmRepoDialog extends React.Component<Props> {
<div className="flex column gaps">
<Input
autoFocus required
placeholder={`Helm repo name`}
placeholder="Helm repo name"
validators={systemName}
value={this.helmRepo.name} onChange={v => this.helmRepo.name = v}
/>
<Input
required
placeholder={`URL`}
placeholder="URL"
validators={isUrl}
value={this.helmRepo.url} onChange={v => this.helmRepo.url = v}
/>
@ -162,7 +162,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
More
<Icon
small
tooltip={`More`}
tooltip="More"
material={this.showOptions ? "remove" : "add"}
/>
</Button>

View File

@ -122,7 +122,7 @@ export class Preferences extends React.Component {
<h2>HTTP Proxy</h2>
<Input
theme="round-black"
placeholder={`Type HTTP proxy url (example: http://proxy.acme.org:8080)`}
placeholder="Type HTTP proxy url (example: http://proxy.acme.org:8080)"
value={this.httpProxy}
onChange={v => this.httpProxy = v}
onBlur={() => preferences.httpsProxy = this.httpProxy}

View File

@ -45,7 +45,7 @@ export class StorageClassDetails extends React.Component<Props> {
)}
{parameters && (
<>
<DrawerTitle title={`Parameters`}/>
<DrawerTitle title="Parameters"/>
{
Object.entries(parameters).map(([name, value]) => (
<DrawerItem key={name + value} name={startCase(name)}>

View File

@ -71,7 +71,7 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
{volumeClaim.getStatus()}
</DrawerItem>
<DrawerTitle title={`Selector`}/>
<DrawerTitle title="Selector"/>
<DrawerItem name="Match Labels" labelsOnly>
{volumeClaim.getMatchLabels().map(label => <Badge key={label} label={label}/>)}

View File

@ -205,7 +205,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
<Select
key={this.selectedRoleId}
themeName="light"
placeholder={`Select role..`}
placeholder="Select role.."
isDisabled={this.isEditing}
options={this.roleOptions}
value={this.selectedRoleId}
@ -224,7 +224,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
!this.useRoleForBindingName && (
<Input
autoFocus
placeholder={`Name`}
placeholder="Name"
disabled={this.isEditing}
value={this.bindingName}
onChange={v => this.bindingName = v}
@ -239,7 +239,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
<Select
isMulti
themeName="light"
placeholder={`Select service accounts`}
placeholder="Select service accounts"
autoConvertOptions={false}
options={this.serviceAccountOptions}
onChange={(opts: BindingSelectOption[]) => {

View File

@ -117,8 +117,8 @@ export class RoleBindingDetails extends React.Component<Props> {
<AddRemoveButtons
onAdd={() => AddRoleBindingDialog.open(roleBinding)}
onRemove={selectedSubjects.length ? this.removeSelectedSubjects : null}
addTooltip="Add bindings to {name}"
removeTooltip="Remove selected bindings from ${name}"
addTooltip={`Add bindings to ${roleRef.name}`}
removeTooltip={`Remove selected bindings from ${roleRef.name}`}
/>
</div>
);

View File

@ -50,8 +50,8 @@ export class RoleBindings extends React.Component<Props> {
renderTableContents={(binding: RoleBinding) => [
binding.getName(),
<KubeObjectStatusIcon key="icon" object={binding} />,
binding.getSubjectNames(),
binding.getNs() || "-",
binding.getSubjectNames(),
binding.getAge(),
]}
addRemoveButtons={{

View File

@ -71,7 +71,7 @@ export class AddRoleDialog extends React.Component<Props> {
<SubTitle title="Role Name" />
<Input
required autoFocus
placeholder={`Name`}
placeholder="Name"
iconLeft="supervisor_account"
value={this.roleName}
onChange={v => this.roleName = v}

View File

@ -66,7 +66,7 @@ export class CreateServiceAccountDialog extends React.Component<Props> {
<SubTitle title="Account Name" />
<Input
autoFocus required
placeholder={`Enter a name`}
placeholder="Enter a name"
validators={systemName}
value={name} onChange={v => this.name = v.toLowerCase()}
/>

View File

@ -87,7 +87,7 @@ export function CronJobMenu(props: KubeObjectMenuProps<CronJob>) {
return (
<>
<MenuItem onClick={() => CronJobTriggerDialog.open(object)}>
<Icon material="play_circle_filled" title={`Trigger`} interactive={toolbar}/>
<Icon material="play_circle_filled" title="Trigger" interactive={toolbar}/>
<span className="title">Trigger</span>
</MenuItem>
@ -106,7 +106,7 @@ export function CronJobMenu(props: KubeObjectMenuProps<CronJob>) {
Resume CronJob <b>{object.getName()}</b>?
</p>),
})}>
<Icon material="play_circle_outline" title={`Resume`} interactive={toolbar}/>
<Icon material="play_circle_outline" title="Resume" interactive={toolbar}/>
<span className="title">Resume</span>
</MenuItem>
@ -124,7 +124,7 @@ export function CronJobMenu(props: KubeObjectMenuProps<CronJob>) {
Suspend CronJob <b>{object.getName()}</b>?
</p>),
})}>
<Icon material="pause_circle_filled" title={`Suspend`} interactive={toolbar}/>
<Icon material="pause_circle_filled" title="Suspend" interactive={toolbar}/>
<span className="title">Suspend</span>
</MenuItem>
}

View File

@ -104,7 +104,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps<Deployment>) {
return (
<>
<MenuItem onClick={() => DeploymentScaleDialog.open(object)}>
<Icon material="open_with" title={`Scale`} interactive={toolbar}/>
<Icon material="open_with" title="Scale" interactive={toolbar}/>
<span className="title">Scale</span>
</MenuItem>
<MenuItem onClick={() => ConfirmDialog.open({
@ -126,7 +126,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps<Deployment>) {
</p>
),
})}>
<Icon material="autorenew" title={`Restart`} interactive={toolbar}/>
<Icon material="autorenew" title="Restart" interactive={toolbar}/>
<span className="title">Restart</span>
</MenuItem>
</>

View File

@ -30,7 +30,11 @@ export const ContainerEnvironment = observer((props: Props) => {
}
});
envFrom && envFrom.forEach(item => {
const { configMapRef } = item;
const { configMapRef, secretRef } = item;
if (secretRef && secretRef.name) {
secretsStore.load({ name: secretRef.name, namespace });
}
if (configMapRef && configMapRef.name) {
configMapsStore.load({ name: configMapRef.name, namespace });
@ -89,21 +93,54 @@ export const ContainerEnvironment = observer((props: Props) => {
const renderEnvFrom = () => {
const envVars = envFrom.map(vars => {
if (!vars.configMapRef || !vars.configMapRef.name) return;
const configMap = configMapsStore.getByName(vars.configMapRef.name, namespace);
if (!configMap) return;
return Object.entries(configMap.data).map(([name, value]) => (
<div className="variable" key={name}>
<span className="var-name">{name}</span>: {value}
</div>
));
if (vars.configMapRef?.name) {
return renderEnvFromConfigMap(vars.configMapRef.name);
} else if (vars.secretRef?.name ) {
return renderEnvFromSecret(vars.secretRef.name);
}
});
return _.flatten(envVars);
};
const renderEnvFromConfigMap = (configMapName: string) => {
const configMap = configMapsStore.getByName(configMapName, namespace);
if (!configMap) return;
return Object.entries(configMap.data).map(([name, value]) => (
<div className="variable" key={name}>
<span className="var-name">{name}</span>: {value}
</div>
));
};
const renderEnvFromSecret = (secretName: string) => {
const secret = secretsStore.getByName(secretName, namespace);
if (!secret) return;
return Object.keys(secret.data).map(key => {
const secretKeyRef = {
name: secret.getName(),
key
};
const value = (
<SecretKey
reference={secretKeyRef}
namespace={namespace}
/>
);
return (
<div className="variable" key={key}>
<span className="var-name">{key}</span>: {value}
</div>
);
});
};
return (
<DrawerItem name="Environment" className="ContainerEnvironment">
{env && renderEnv()}

View File

@ -43,7 +43,7 @@ export class PodContainerPort extends React.Component<Props> {
return (
<div className={cssNames("PodContainerPort", { waiting: this.waiting })}>
<span title={`Open in a browser`} onClick={() => this.portForward() }>
<span title="Open in a browser" onClick={() => this.portForward() }>
{text}
{this.waiting && (
<Spinner />

View File

@ -78,7 +78,7 @@ export function ReplicaSetMenu(props: KubeObjectMenuProps<ReplicaSet>) {
return (
<>
<MenuItem onClick={() => ReplicaSetScaleDialog.open(object)}>
<Icon material="open_with" title={`Scale`} interactive={toolbar}/>
<Icon material="open_with" title="Scale" interactive={toolbar}/>
<span className="title">Scale</span>
</MenuItem>
</>

View File

@ -83,7 +83,7 @@ export function StatefulSetMenu(props: KubeObjectMenuProps<StatefulSet>) {
return (
<>
<MenuItem onClick={() => StatefulSetScaleDialog.open(object)}>
<Icon material="open_with" title={`Scale`} interactive={toolbar}/>
<Icon material="open_with" title="Scale" interactive={toolbar}/>
<span className="title">Scale</span>
</MenuItem>
</>

View File

@ -77,7 +77,7 @@ export class CreateResource extends React.Component<Props> {
tabId={tabId}
error={error}
submit={create}
submitLabel={`Create`}
submitLabel="Create"
showNotifications={false}
/>
<EditorPanel

View File

@ -68,7 +68,7 @@ export class DockTab extends React.Component<DockTabProps> {
{!pinned && (
<Icon
small material="close"
title={`Close (Ctrl+W)`}
title="Close (Ctrl+W)"
onClick={prevDefault(this.close)}
/>
)}

View File

@ -98,8 +98,8 @@ export class EditResource extends React.Component<Props> {
tabId={tabId}
error={error}
submit={save}
submitLabel={`Save`}
submittingMessage={`Applying..`}
submitLabel="Save"
submittingMessage="Applying.."
controls={(
<div className="resource-info flex gaps align-center">
<span>Kind:</span> <Badge label={kind}/>

View File

@ -125,17 +125,17 @@ export class InstallChart extends Component<Props> {
<div className="flex gaps align-center">
<Button
autoFocus primary
label={`View Helm Release`}
label="View Helm Release"
onClick={prevDefault(this.viewRelease)}
/>
<Button
plain active
label={`Show Notes`}
label="Show Notes"
onClick={() => this.showNotes = true}
/>
</div>
<LogsDialog
title={`Helm Chart Install`}
title="Helm Chart Install"
isOpen={this.showNotes}
close={() => this.showNotes = false}
logs={this.releaseDetails.log}
@ -148,7 +148,7 @@ export class InstallChart extends Component<Props> {
const panelControls = (
<div className="install-controls flex gaps align-center">
<span>Chart</span>
<Badge label={`${repo}/${name}`} title={`Repo/Name`} />
<Badge label={`${repo}/${name}`} title="Repo/Name" />
<span>Version</span>
<Select
className="chart-version"
@ -167,8 +167,8 @@ export class InstallChart extends Component<Props> {
onChange={this.onNamespaceChange}
/>
<Input
placeholder={`Name (optional)`}
title={`Release name`}
placeholder="Name (optional)"
title="Release name"
maxLength={50}
value={releaseName}
onChange={this.onReleaseNameChange}
@ -183,8 +183,8 @@ export class InstallChart extends Component<Props> {
controls={panelControls}
error={this.error}
submit={install}
submitLabel={`Install`}
submittingMessage={`Installing...`}
submitLabel="Install"
submittingMessage="Installing..."
showSubmitClose={false}
/>
<EditorPanel

View File

@ -68,13 +68,13 @@ export const LogSearch = observer((props: Props) => {
/>
<Icon
material="keyboard_arrow_up"
tooltip={`Previous`}
tooltip="Previous"
onClick={onPrevOverlay}
disabled={jumpDisabled}
/>
<Icon
material="keyboard_arrow_down"
tooltip={`Next`}
tooltip="Next"
onClick={onNextOverlay}
disabled={jumpDisabled}
/>

View File

@ -123,8 +123,8 @@ export class UpgradeChart extends React.Component<Props> {
tabId={tabId}
error={error}
submit={upgrade}
submitLabel={`Upgrade`}
submittingMessage={`Updating..`}
submitLabel="Upgrade"
submittingMessage="Updating.."
controls={controlsAndInfo}
/>
<EditorPanel

View File

@ -61,7 +61,7 @@ export class ErrorBoundary extends React.Component<Props, State> {
</div>
<Button
className="box self-flex-start"
primary label={`Back`}
primary label="Back"
onClick={this.back}
/>
</div>

View File

@ -44,6 +44,11 @@
> main {
display: contents;
> * {
grid-area: main;
overflow: auto;
}
}
footer {

View File

@ -106,13 +106,13 @@ export class MenuActions extends React.Component<MenuActionsProps> {
{children}
{updateAction && (
<MenuItem onClick={updateAction}>
<Icon material="edit" interactive={toolbar} title={`Edit`}/>
<Icon material="edit" interactive={toolbar} title="Edit"/>
<span className="title">Edit</span>
</MenuItem>
)}
{removeAction && (
<MenuItem onClick={this.remove}>
<Icon material="delete" interactive={toolbar} title={`Delete`}/>
<Icon material="delete" interactive={toolbar} title="Delete"/>
<span className="title">Remove</span>
</MenuItem>
)}

View File

@ -2,7 +2,17 @@
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
## 4.1.2 (current version)
## 4.1.3 (current version)
- Don't reset selected namespaces to defaults in case of "All namespaces" on page reload
- Fix loading all namespaces for users with limited cluster access
- Display environment variables coming from secret in pod details
- Fix deprecated helm chart filtering
- Fix RoleBindings Namespace and Bindings field not displaying the correct data
- Fix RoleBindingDetails not rendering the name of the role binding
- Fix auto update on quit with newer version
## 4.1.2
**Upgrade note:** Where have all my pods gone? Namespaced Kubernetes resources are now initially shown only for the "default" namespace. Use the namespaces selector to add more.