Merge branch 'master' into extensions-api

This commit is contained in:
Jari Kolehmainen committed 2020-09-30 19:47:13 +03:00
commit de3849d22c
53 files changed
+1273 -584

No files matched your search

@@ -223,6 +223,7 @@ export class Issuer extends KubeObject {
}
getConditions() {
if (!this.status?.conditions) return [];
const { conditions = [] } = this.status;
return conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition;
+6 -6
View File
@@ -9,12 +9,12 @@ type AdditionalPrinterColumnsCommon = {
description: string;
}
type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & {
jsonPath: string;
type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & {
jsonPath: string;
}
type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
JSONPath: string;
type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
JSONPath: string;
}
export class CustomResourceDefinition extends KubeObject {
@@ -50,7 +50,7 @@ export class CustomResourceDefinition extends KubeObject {
message: string;
reason: string;
status: string;
type: string;
type?: string;
}[];
acceptedNames: {
plural: string;
@@ -132,7 +132,7 @@ export class CustomResourceDefinition extends KubeObject {
}
getConditions() {
if (!this.status.conditions) return [];
if (!this.status?.conditions) return [];
return this.status.conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition;
return {
+4 -4
View File
@@ -115,12 +115,12 @@ export class KubeObject implements ItemObject {
return KubeObject.stringifyLabels(this.metadata.labels);
}
getAnnotations(): string[] {
getAnnotations(filter = false): string[] {
const labels = KubeObject.stringifyLabels(this.metadata.annotations);
return labels.filter(label => {
return filter ? labels.filter(label => {
const skip = resourceApplierApi.annotations.some(key => label.startsWith(key));
return !skip;
})
}) : labels;
}
getOwnerRefs() {
@@ -138,7 +138,7 @@ export class KubeObject implements ItemObject {
getNs(),
getId(),
...getLabels(),
...getAnnotations(),
...getAnnotations(true),
]
}
+17 -2
View File
@@ -1,6 +1,6 @@
import "./components/app.scss"
import React from "react";
import { render } from "react-dom";
import { render, unmountComponentAtNode } from "react-dom";
import { isMac } from "../common/vars";
import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store";
@@ -30,12 +30,27 @@ export async function bootstrap(App: AppComponent) {
themeStore.init(),
]);
// Register additional store listeners
clusterStore.registerIpcListener();
// init app's dependencies if any
if (App.init) {
await App.init();
extensionStore.autoEnableOnLoad(getLensRuntime);
}
render(<App/>, rootElem);
window.addEventListener("message", (ev: MessageEvent) => {
if (ev.data === "teardown") {
userStore.unregisterIpcListener()
workspaceStore.unregisterIpcListener()
clusterStore.unregisterIpcListener()
unmountComponentAtNode(rootElem)
window.location.href = "about:blank"
}
})
render(<>
{isMac && <div id="draggable-top" />}
<App />
</>, rootElem);
}
// run
@@ -99,10 +99,7 @@ export class AddCluster extends React.Component {
getContexts(config: KubeConfig): Map<string, KubeConfig> {
const contexts = new Map();
splitConfig(config).forEach(config => {
const isExists = clusterStore.hasContext(config.currentContext);
if (!isExists) {
contexts.set(config.currentContext, config);
}
contexts.set(config.currentContext, config);
})
return contexts
}
@@ -49,4 +49,4 @@
.chart-description {
margin-top: $margin * 2;
}
}
}
@@ -3,8 +3,8 @@ import "./helm-chart-details.scss";
import React, { Component } from "react";
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
import { t, Trans } from "@lingui/macro";
import { autorun, observable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { observable, toJS } from "mobx";
import { observer } from "mobx-react";
import { Drawer, DrawerItem } from "../drawer";
import { autobind, stopPropagation } from "../../utils";
import { MarkdownViewer } from "../markdown-viewer";
@@ -25,39 +25,41 @@ interface Props {
export class HelmChartDetails extends Component<Props> {
@observable chartVersions: HelmChart[];
@observable selectedChart: HelmChart;
@observable description: string = null;
@observable readme: string = null;
@observable error: string = null;
private chartPromise: CancelablePromise<{ readme: string; versions: HelmChart[] }>;
@disposeOnUnmount
chartSelector = autorun(async () => {
if (!this.props.chart) return;
this.chartVersions = null;
this.selectedChart = null;
this.description = null;
this.loadChartData();
this.chartPromise.then(data => {
this.description = data.readme;
this.chartVersions = data.versions;
this.selectedChart = data.versions[0];
});
});
async componentDidMount() {
const { chart: { name, repo, version } } = this.props
loadChartData(version?: string) {
const { chart: { name, repo } } = this.props;
if (this.chartPromise) this.chartPromise.cancel();
this.chartPromise = helmChartsApi.get(repo, name, version);
try {
const { readme, versions } = await (this.chartPromise = helmChartsApi.get(repo, name, version))
this.readme = readme
this.chartVersions = versions
this.selectedChart = versions[0]
} catch (error) {
this.error = error
}
}
componentWillUnmount() {
this.chartPromise?.cancel();
}
@autobind()
onVersionChange(opt: SelectOption) {
const version = opt.value;
async onVersionChange({ value: version }: SelectOption) {
this.selectedChart = this.chartVersions.find(chart => chart.version === version);
this.description = null;
this.loadChartData(version);
this.chartPromise.then(data => {
this.description = data.readme
});
this.readme = null;
try {
this.chartPromise?.cancel();
const { chart: { name, repo } } = this.props;
const { readme } = await (this.chartPromise = helmChartsApi.get(repo, name, version))
this.readme = readme;
} catch (error) {
this.error = error;
}
}
@autobind()
@@ -79,7 +81,7 @@ 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={_i18n._(t`Install`)} onClick={this.install}/>
<Button primary label={_i18n._(t`Install`)} onClick={this.install} />
</div>
<DrawerItem name={_i18n._(t`Version`)} className="version" onClick={stopPropagation}>
<Select
@@ -95,12 +97,12 @@ export class HelmChartDetails extends Component<Props> {
</DrawerItem>
<DrawerItem name={_i18n._(t`Maintainers`)} className="maintainers">
{selectedChart.getMaintainers().map(({ name, email, url }) =>
<a key={name} href={url ? url : `mailto:${email}`} target="_blank">{name}</a>
<a key={name} href={url || `mailto:${email}`} target="_blank">{name}</a>
)}
</DrawerItem>
{selectedChart.getKeywords().length > 0 && (
<DrawerItem name={_i18n._(t`Keywords`)} labelsOnly>
{selectedChart.getKeywords().map(key => <Badge key={key} label={key}/>)}
{selectedChart.getKeywords().map(key => <Badge key={key} label={key} />)}
</DrawerItem>
)}
</div>
@@ -108,14 +110,35 @@ export class HelmChartDetails extends Component<Props> {
);
}
renderReadme() {
if (this.readme === null) {
return <Spinner center />
}
return (
<div className="chart-description">
<MarkdownViewer markdown={this.readme} />
</div>
)
}
renderContent() {
if (this.selectedChart === null || this.description === null) return <Spinner center/>;
if (!this.selectedChart) {
return <Spinner center />;
}
if (this.error) {
return (
<div className="box grow">
<p className="error">{this.error}</p>
</div>
)
}
return (
<div className="box grow">
{this.renderIntroduction()}
<div className="chart-description">
<MarkdownViewer markdown={this.description}/>
</div>
{this.renderReadme()}
</div>
);
}
@@ -135,4 +158,4 @@ export class HelmChartDetails extends Component<Props> {
</Drawer>
);
}
}
}
@@ -72,7 +72,7 @@ export class HelmCharts extends Component<Props> {
(items: HelmChart[]) => items.filter(item => !item.deprecated)
]}
customizeHeader={() => (
<SearchInput placeholder={_i18n._(t`Search Helm Charts`)}/>
<SearchInput placeholder={_i18n._(t`Search Helm Charts`)} />
)}
renderTableHeader={[
{ className: "icon" },
@@ -99,11 +99,13 @@ export class HelmCharts extends Component<Props> {
detailsItem={this.selectedChart}
onDetails={this.showDetails}
/>
<HelmChartDetails
chart={this.selectedChart}
hideDetails={this.hideDetails}
/>
{this.selectedChart && (
<HelmChartDetails
chart={this.selectedChart}
hideDetails={this.hideDetails}
/>
)}
</>
);
}
}
}
@@ -13,8 +13,9 @@ import { apiManager } from "../../api/api-manager";
import { crdStore } from "./crd.store";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { Input } from "../input";
import { CustomResourceDefinition } from "../../api/endpoints/crd.api";
interface Props extends KubeObjectDetailsProps {
interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> {
}
function CrdColumnValue({ value }: { value: any[] | {} | string }) {
@@ -66,12 +67,14 @@ export class CrdResourceDetails extends React.Component<Props> {
})}
{showStatus && (
<DrawerItem name={<Trans>Status</Trans>} className="status" labelsOnly>
{object.status.conditions.map((condition: { type: string; message: string; status: string }) => {
const { type, message, status } = condition;
{object.status.conditions.map((condition, index) => {
const { type, reason, message, status } = condition;
const kind = type || reason;
if (!kind) return null;
return (
<Badge
key={type} label={type}
className={cssNames({ disabled: status === "False" }, type.toLowerCase())}
key={kind + index} label={kind}
className={cssNames({ disabled: status === "False" }, kind.toLowerCase())}
tooltip={message}
/>
);
@@ -58,7 +58,7 @@ export class NamespaceSelect extends React.Component<Props> {
const { value, label } = option;
return label || (
<>
{showIcons && <Icon small material="layers"/>}
{showIcons && <Icon small material="layers" />}
{value}
</>
);
@@ -91,14 +91,15 @@ export class NamespaceSelectFilter extends React.Component {
closeMenuOnSelect={false}
isOptionSelected={() => false}
controlShouldRenderValue={false}
onChange={({ value: namespace }: SelectOption) => toggleContext(namespace)}
isMulti
onChange={([{ value }]: SelectOption[]) => toggleContext(value)}
formatOptionLabel={({ value: namespace }: SelectOption) => {
const isSelected = hasContext(namespace);
return (
<div className="flex gaps align-center">
<FilterIcon type={FilterType.NAMESPACE}/>
<FilterIcon type={FilterType.NAMESPACE} />
<span>{namespace}</span>
{isSelected && <Icon small material="check" className="box right"/>}
{isSelected && <Icon small material="check" className="box right" />}
</div>
)
}}
@@ -18,44 +18,22 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
{ value: "china", label: "China (Azure)" },
]
const save = () => {
preferences.downloadBinariesPath = downloadPath;
preferences.kubectlBinariesPath = binariesPath;
}
const renderPath = () => {
if (preferences.downloadKubectlBinaries) {
return null;
}
return (
<>
<SubTitle title="Path to Kubectl binary"/>
<Input
theme="round-black"
value={binariesPath}
validators={isPath}
onChange={setBinariesPath}
onBlur={save}
/>
<small className="hint">
<Trans>Default:</Trans>{" "}{Kubectl.bundledKubectlPath}
</small>
</>
);
}
return (
<>
<h2><Trans>Kubectl Binary</Trans></h2>
<small className="hint">
<Trans>Download kubectl binaries matching to Kubernetes cluster verison.</Trans>
</small>
<Checkbox
label={<Trans>Download kubectl binaries</Trans>}
value={preferences.downloadKubectlBinaries}
onChange={downloadKubectlBinaries => preferences.downloadKubectlBinaries = downloadKubectlBinaries}
/>
<small className="hint">
<Trans>Download kubectl binaries matching to Kubernetes cluster version.</Trans>
</small>
<SubTitle title="Download mirror" />
<Select
placeholder={<Trans>Download mirror for kubectl</Trans>}
@@ -64,20 +42,32 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
onChange={({ value }: SelectOption) => preferences.downloadMirror = value}
disabled={!preferences.downloadKubectlBinaries}
/>
<SubTitle title="Directory for binaries"/>
<SubTitle title="Directory for binaries" />
<Input
theme="round-black"
value={downloadPath}
placeholder={`Directory to download binaries into`}
placeholder={userStore.getDefaultKubectlPath()}
validators={isPath}
onChange={setDownloadPath}
onBlur={save}
disabled={!preferences.downloadKubectlBinaries}
/>
<small>
Default: {userStore.getDefaultKubectlPath()}
<small className="hint">
The directory to download binaries into.
</small>
<SubTitle title="Path to Kubectl binary" />
<Input
theme="round-black"
placeholder={Kubectl.bundledKubectlPath}
value={binariesPath}
validators={isPath}
onChange={setBinariesPath}
onBlur={save}
disabled={preferences.downloadKubectlBinaries}
/>
<small className="hint">
<Trans>The path to the kubectl binary on the system.</Trans>
</small>
{renderPath()}
</>
);
});
});
@@ -11,11 +11,9 @@ export class DaemonSetStore extends KubeObjectStore<DaemonSet> {
@observable metrics: IPodMetrics = null;
loadMetrics(daemonSet: DaemonSet) {
async loadMetrics(daemonSet: DaemonSet) {
const pods = this.getChildPods(daemonSet);
return podsApi.getMetrics(pods, daemonSet.getNs(), "").then(metrics =>
this.metrics = metrics
);
this.metrics = await podsApi.getMetrics(pods, daemonSet.getNs(), "");
}
getChildPods(daemonSet: DaemonSet): Pod[] {
@@ -16,11 +16,9 @@ export class DeploymentStore extends KubeObjectStore<Deployment> {
], "desc");
}
loadMetrics(deployment: Deployment) {
async loadMetrics(deployment: Deployment) {
const pods = this.getChildPods(deployment);
return podsApi.getMetrics(pods, deployment.getNs(), "").then(metrics =>
this.metrics = metrics
);
this.metrics = await podsApi.getMetrics(pods, deployment.getNs(), "");
}
getStatuses(deployments?: Deployment[]) {
@@ -5,72 +5,54 @@ import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { OverviewWorkloadStatus } from "./overview-workload-status";
import { Link } from "react-router-dom";
import { cronJobsURL, daemonSetsURL, deploymentsURL, jobsURL, podsURL, statefulSetsURL } from "../+workloads";
import { podsStore } from "../+workloads-pods/pods.store";
import { deploymentStore } from "../+workloads-deployments/deployments.store";
import { daemonSetStore } from "../+workloads-daemonsets/daemonsets.store";
import { statefulSetStore } from "../+workloads-statefulsets/statefulset.store";
import { jobStore } from "../+workloads-jobs/job.store";
import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
import { workloadURL, workloadStores } from "../+workloads";
import { namespaceStore } from "../+namespaces/namespace.store";
import { PageFiltersList } from "../item-object-list/page-filters-list";
import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
import { isAllowedResource } from "../../../common/rbac";
import { isAllowedResource, KubeResource } from "../../../common/rbac";
import { ResourceNames } from "../../../renderer/utils/rbac";
import { autobind } from "../../utils";
import { _i18n } from "../../i18n";
const resources: KubeResource[] = [
"pods",
"deployments",
"statefulsets",
"daemonsets",
"jobs",
"cronjobs",
]
@observer
export class OverviewStatuses extends React.Component {
@autobind()
renderWorkload(resource: KubeResource): React.ReactElement {
const store = workloadStores[resource];
const items = store.getAllByNs(namespaceStore.contextNs)
return (
<div className="workload" key={resource}>
<div className="title">
<Link to={workloadURL[resource]()}>{ResourceNames[resource]} ({items.length})</Link>
</div>
<OverviewWorkloadStatus status={store.getStatuses(items)} />
</div>
)
}
render() {
const { contextNs } = namespaceStore;
const pods = isAllowedResource("pods") ? podsStore.getAllByNs(contextNs) : [];
const deployments = isAllowedResource("deployments") ? deploymentStore.getAllByNs(contextNs) : [];
const statefulSets = isAllowedResource("statefulsets") ? statefulSetStore.getAllByNs(contextNs) : [];
const daemonSets = isAllowedResource("daemonsets") ? daemonSetStore.getAllByNs(contextNs) : [];
const jobs = isAllowedResource("jobs") ? jobStore.getAllByNs(contextNs) : [];
const cronJobs = isAllowedResource("cronjobs") ? cronJobStore.getAllByNs(contextNs) : [];
const workloads = resources
.filter(isAllowedResource)
.map(this.renderWorkload);
return (
<div className="OverviewStatuses">
<div className="header flex gaps align-center">
<h5 className="box grow"><Trans>Overview</Trans></h5>
<NamespaceSelectFilter/>
<NamespaceSelectFilter />
</div>
<PageFiltersList/>
<PageFiltersList />
<div className="workloads">
{isAllowedResource("pods") &&
<div className="workload">
<div className="title"><Link to={podsURL()}><Trans>Pods</Trans> ({pods.length})</Link></div>
<OverviewWorkloadStatus status={podsStore.getStatuses(pods)}/>
</div>
}
{isAllowedResource("deployments") &&
<div className="workload">
<div className="title"><Link to={deploymentsURL()}><Trans>Deployments</Trans> ({deployments.length})</Link></div>
<OverviewWorkloadStatus status={deploymentStore.getStatuses(deployments)}/>
</div>
}
{isAllowedResource("statefulsets") &&
<div className="workload">
<div className="title"><Link to={statefulSetsURL()}><Trans>StatefulSets</Trans> ({statefulSets.length})</Link></div>
<OverviewWorkloadStatus status={statefulSetStore.getStatuses(statefulSets)}/>
</div>
}
{isAllowedResource("daemonsets") &&
<div className="workload">
<div className="title"><Link to={daemonSetsURL()}><Trans>DaemonSets</Trans> ({daemonSets.length})</Link></div>
<OverviewWorkloadStatus status={daemonSetStore.getStatuses(daemonSets)}/>
</div>
}
{isAllowedResource("jobs") &&
<div className="workload">
<div className="title"><Link to={jobsURL()}><Trans>Jobs</Trans> ({jobs.length})</Link></div>
<OverviewWorkloadStatus status={jobStore.getStatuses(jobs)}/>
</div>
}
{isAllowedResource("cronjobs") &&
<div className="workload">
<div className="title"><Link to={cronJobsURL()}><Trans>CronJobs</Trans> ({cronJobs.length})</Link></div>
<OverviewWorkloadStatus status={cronJobStore.getStatuses(cronJobs)}/>
</div>
}
{workloads}
</div>
</div>
)
@@ -113,7 +113,8 @@ const SecretKey = (props: SecretKeyProps) => {
const [loading, setLoading] = useState(false)
const [secret, setSecret] = useState<Secret>()
const showKey = async () => {
const showKey = async (evt: React.MouseEvent) => {
evt.preventDefault()
setLoading(true)
const secret = await secretsStore.load({ name, namespace });
setLoading(false)
@@ -126,7 +127,7 @@ const SecretKey = (props: SecretKeyProps) => {
return (
<>
secretKeyRef({name}.{key})&nbsp;
secretKeyRef({name}.{key})&nbsp;
<Icon
className={cssNames("secret-button", { loading })}
material="visibility"
@@ -10,11 +10,9 @@ export class ReplicaSetStore extends KubeObjectStore<ReplicaSet> {
api = replicaSetApi
@observable metrics: IPodMetrics = null;
loadMetrics(replicaSet: ReplicaSet) {
async loadMetrics(replicaSet: ReplicaSet) {
const pods = this.getChildPods(replicaSet);
return podsApi.getMetrics(pods, replicaSet.getNs(), "").then(metrics =>
this.metrics = metrics
);
this.metrics = await podsApi.getMetrics(pods, replicaSet.getNs(), "");
}
getChildPods(replicaSet: ReplicaSet) {
@@ -10,11 +10,9 @@ export class StatefulSetStore extends KubeObjectStore<StatefulSet> {
api = statefulSetApi
@observable metrics: IPodMetrics = null;
loadMetrics(statefulSet: StatefulSet) {
async loadMetrics(statefulSet: StatefulSet) {
const pods = this.getChildPods(statefulSet);
return podsApi.getMetrics(pods, statefulSet.getNs(), "").then(metrics =>
this.metrics = metrics
);
this.metrics = await podsApi.getMetrics(pods, statefulSet.getNs(), "");
}
getChildPods(statefulSet: StatefulSet) {
+1 -1
View File
@@ -1,3 +1,3 @@
export * from "./workloads.route"
export * from "./workloads"
export * from "./workloads.stores"
@@ -1,6 +1,7 @@
import { RouteProps } from "react-router"
import { Workloads } from "./workloads";
import { buildURL, IURLParams } from "../../navigation";
import { KubeResource } from "../../../common/rbac";
export const workloadsRoute: RouteProps = {
get path() {
@@ -62,3 +63,12 @@ export const daemonSetsURL = buildURL<IDaemonSetsRouteParams>(daemonSetsRoute.pa
export const statefulSetsURL = buildURL<IStatefulSetsRouteParams>(statefulSetsRoute.path)
export const jobsURL = buildURL<IJobsRouteParams>(jobsRoute.path)
export const cronJobsURL = buildURL<ICronJobsRouteParams>(cronJobsRoute.path)
export const workloadURL: Partial<Record<KubeResource, ReturnType<typeof buildURL>>> = {
"pods": podsURL,
"deployments": deploymentsURL,
"daemonsets": daemonSetsURL,
"statefulsets": statefulSetsURL,
"jobs": jobsURL,
"cronjobs": cronJobsURL,
}
@@ -0,0 +1,17 @@
import { KubeObjectStore } from "../../kube-object.store";
import { podsStore } from "../+workloads-pods/pods.store";
import { deploymentStore } from "../+workloads-deployments/deployments.store";
import { daemonSetStore } from "../+workloads-daemonsets/daemonsets.store";
import { statefulSetStore } from "../+workloads-statefulsets/statefulset.store";
import { jobStore } from "../+workloads-jobs/job.store";
import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
import { KubeResource } from "../../../common/rbac";
export const workloadStores: Partial<Record<KubeResource, KubeObjectStore>> = {
"pods": podsStore,
"deployments": deploymentStore,
"daemonsets": daemonSetStore,
"statefulsets": statefulSetStore,
"jobs": jobStore,
"cronjobs": cronJobStore,
}
+2 -1
View File
@@ -13,6 +13,7 @@
:root {
--mainBackground: #1e2124;
--main-layout-header: 40px;
--drag-region-height: 22px
}
::selection {
@@ -47,7 +48,7 @@ html, body {
left: 0;
top: 0;
width: 100%;
height: var(--main-layout-header);
height: var(--drag-region-height);
z-index: 1000;
pointer-events: none;
}
+1 -1
View File
@@ -165,7 +165,7 @@ export const memoryOptions: ChartOptions = {
}
return bytesToUnits(parseInt(value));
}
return `${value}`;
return bytesToUnits(value);
},
stepSize: 1
}
@@ -55,25 +55,24 @@ export class ClusterManager extends React.Component {
render() {
return (
<div className="ClusterManager">
<div id="draggable-top"/>
<main>
<div id="lens-views"/>
<div id="lens-views" />
<Switch>
<Route component={LandingPage} {...landingRoute}/>
<Route component={Preferences} {...preferencesRoute}/>
<Route component={Workspaces} {...workspacesRoute}/>
<Route component={AddCluster} {...addClusterRoute}/>
<Route component={ClusterView} {...clusterViewRoute}/>
<Route component={ClusterSettings} {...clusterSettingsRoute}/>
<Route component={LandingPage} {...landingRoute} />
<Route component={Preferences} {...preferencesRoute} />
<Route component={Workspaces} {...workspacesRoute} />
<Route component={AddCluster} {...addClusterRoute} />
<Route component={ClusterView} {...clusterViewRoute} />
<Route component={ClusterSettings} {...clusterSettingsRoute} />
<Route component={Extensions} {...extensionsRoute}/>
{dynamicPages.globalPages.map(({ path, components: { Page } }) => {
return <Route key={path} path={path} component={Page}/>
})}
<Redirect exact to={this.startUrl}/>
<Redirect exact to={this.startUrl} />
</Switch>
</main>
<ClustersMenu/>
<BottomBar/>
<ClustersMenu />
<BottomBar />
</div>
)
}
@@ -19,8 +19,11 @@ export async function initView(clusterId: ClusterId) {
if (!clusterId || lensViews.has(clusterId)) {
return;
}
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`)
const cluster = clusterStore.getById(clusterId);
if (!cluster) {
return;
}
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`)
const parentElem = document.getElementById("lens-views");
const iframe = document.createElement("iframe");
iframe.name = cluster.contextName;
@@ -42,9 +45,9 @@ export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrame
// Keep frame in DOM to avoid possible bugs when same cluster re-created after being removed.
// In that case for some reasons `webFrame.routingId` returns some previous frameId (usage in app.tsx)
// Issue: https://github.com/lensapp/lens/issues/811
iframe.dataset.meta = `${iframe.name} was removed at ${new Date().toLocaleString()}`;
iframe.removeAttribute("src")
iframe.dataset.meta = `${iframe.name} was removed at ${new Date().toLocaleString()}`
iframe.removeAttribute("name")
iframe.contentWindow.postMessage("teardown", "*")
}
export function refreshViews() {
@@ -5,6 +5,7 @@ import { t } from "@lingui/macro";
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
import { IReleaseUpdateDetails } from "../../api/endpoints/helm-releases.api";
import { _i18n } from "../../i18n";
import { Notifications } from "../notifications";
export interface IChartInstallData {
name: string;
@@ -27,45 +28,50 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
});
autorun(() => {
const { selectedTab, isOpen } = dockStore;
if (!isInstallChartTab(selectedTab)) return;
if (isOpen) {
this.loadData();
if (isInstallChartTab(selectedTab) && isOpen) {
this.loadData()
.catch(err => Notifications.error(String(err)))
}
}, { delay: 250 })
}
@action
async loadData(tabId = dockStore.selectedTabId) {
const { values } = this.getData(tabId);
const versions = this.versions.getData(tabId);
return Promise.all([
!versions && this.loadVersions(tabId),
!values && this.loadValues(tabId),
])
const promises = []
if (!this.getData(tabId).values) {
promises.push(this.loadValues(tabId))
}
if (!this.versions.getData(tabId)) {
promises.push(this.loadVersions(tabId))
}
await Promise.all(promises)
}
@action
async loadVersions(tabId: TabId) {
const { repo, name } = this.getData(tabId);
const { repo, name, version } = this.getData(tabId);
this.versions.clearData(tabId); // reset
const charts = await helmChartsApi.get(repo, name);
const charts = await helmChartsApi.get(repo, name, version);
const versions = charts.versions.map(chartVersion => chartVersion.version);
this.versions.setData(tabId, versions);
}
@action
async loadValues(tabId: TabId) {
const data = this.getData(tabId);
const { repo, name, version } = data;
let values = "";
const fetchValues = async (retry = 1, maxRetries = 3) => {
values = await helmChartsApi.getValues(repo, name, version);
if (values || retry == maxRetries) return;
await fetchValues(retry + 1);
};
this.setData(tabId, { ...data, values: undefined }); // reset
await fetchValues();
this.setData(tabId, { ...data, values });
const data = this.getData(tabId)
const { repo, name, version } = data
// This loop is for "retrying" the "getValues" call
for (const _ of Array(3)) {
const values = await helmChartsApi.getValues(repo, name, version)
if (values) {
this.setData(tabId, { ...data, values })
return
}
}
}
}
@@ -107,15 +107,15 @@ export class InstallChart extends Component<Props> {
render() {
const { tabId, chartData, values, versions, install } = this;
if (!chartData || chartData.values === undefined || !versions) {
return <Spinner center/>;
if (chartData?.values === undefined || !versions) {
return <Spinner center />;
}
if (this.releaseDetails) {
return (
<div className="InstallChartDone flex column gaps align-center justify-center">
<p>
<Icon material="check" big sticker/>
<Icon material="check" big sticker />
</p>
<p><Trans>Installation complete!</Trans></p>
<div className="flex gaps align-center">
@@ -144,7 +144,7 @@ export class InstallChart extends Component<Props> {
const panelControls = (
<div className="install-controls flex gaps align-center">
<span><Trans>Chart</Trans></span>
<Badge label={`${repo}/${name}`} title={_i18n._(t`Repo/Name`)}/>
<Badge label={`${repo}/${name}`} title={_i18n._(t`Repo/Name`)} />
<span><Trans>Version</Trans></span>
<Select
className="chart-version"
@@ -191,4 +191,4 @@ export class InstallChart extends Component<Props> {
</div>
);
}
}
}
-1
View File
@@ -282,7 +282,6 @@ export class Input extends React.Component<InputProps, State> {
onKeyDown: this.onKeyDown,
rows: multiLine ? (rows || 1) : null,
ref: this.bindRef,
type: "text",
spellCheck: "false",
});
@@ -116,6 +116,6 @@ export function openServiceAccountKubeConfig(account: ServiceAccount) {
const namespace = account.getNs()
KubeConfigDialog.open({
title: <Trans>{accountName} kubeconfig</Trans>,
loader: () => apiBase.get(`/kubeconfig/service-account/${namespace}/${account}`)
loader: () => apiBase.get(`/kubeconfig/service-account/${namespace}/${accountName}`)
})
}
@@ -8,10 +8,16 @@ import { JsonApiErrorParsed } from "../../api/json-api";
export type IMessageId = string | number;
export type IMessage = React.ReactNode | React.ReactNode[] | JsonApiErrorParsed;
export enum NotificationStatus {
OK = "ok",
ERROR = "error",
INFO = "info",
}
export interface INotification {
id?: IMessageId;
message: IMessage;
status?: "ok" | "error" | "info";
status?: NotificationStatus;
timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide
}
@@ -5,7 +5,7 @@ import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"
import { JsonApiErrorParsed } from "../../api/json-api";
import { cssNames, prevDefault } from "../../utils";
import { IMessage, INotification, notificationsStore } from "./notifications.store";
import { IMessage, INotification, notificationsStore, NotificationStatus } from "./notifications.store";
import { Animate } from "../animate";
import { Icon } from "../icon"
@@ -17,7 +17,7 @@ export class Notifications extends React.Component {
notificationsStore.add({
message: message,
timeout: 2500,
status: "ok"
status: NotificationStatus.OK
})
}
@@ -25,13 +25,13 @@ export class Notifications extends React.Component {
notificationsStore.add({
message: message,
timeout: 10000,
status: "error"
status: NotificationStatus.ERROR
});
}
static info(message: IMessage, customOpts: Partial<INotification> = {}) {
return notificationsStore.add({
status: "info",
status: NotificationStatus.INFO,
timeout: 0,
message: message,
...customOpts,
@@ -78,7 +78,7 @@ export class Notifications extends React.Component {
onMouseLeave={() => addAutoHideTimer(notification)}
onMouseEnter={() => removeAutoHideTimer(notification)}>
<div className="box center">
<Icon material="info_outline"/>
<Icon material="info_outline" />
</div>
<div className="message box grow">{msgText}</div>
<div className="box center">
@@ -32,6 +32,7 @@ html {
background: transparent;
min-height: 0;
box-shadow: 0 0 0 1px $halfGray;
cursor: pointer;
&--is-focused {
box-shadow: 0 0 0 2px $primary;
@@ -88,6 +89,7 @@ html {
&__option {
white-space: nowrap;
cursor: pointer;
&:active {
background: $primary;
+2
View File
@@ -19,6 +19,8 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
kubeWatchApi.addListener(this, this.onWatchApiEvent);
}
getStatuses?(items: T[]): Record<string, number>;
getAllByNs(namespace: string | string[], strict = false): T[] {
const namespaces: string[] = [].concat(namespace);
if (namespaces.length) {
+28
View File
@@ -0,0 +1,28 @@
import { KubeResource } from "../../common/rbac";
import { _i18n } from "../i18n";
export const ResourceNames: Record<KubeResource, string> = {
"namespaces": _i18n._("Namespaces"),
"nodes": _i18n._("Nodes"),
"events": _i18n._("Events"),
"resourcequotas": _i18n._("Resource Quotas"),
"services": _i18n._("Services"),
"secrets": _i18n._("Secrets"),
"configmaps": _i18n._("Config Maps"),
"ingresses": _i18n._("Ingresses"),
"networkpolicies": _i18n._("Network Policies"),
"persistentvolumes": _i18n._("Persistent Volumes"),
"storageclasses": _i18n._("Storage Classes"),
"pods": _i18n._("Pods"),
"daemonsets": _i18n._("Daemon Sets"),
"deployments": _i18n._("Deployments"),
"statefulsets": _i18n._("Stateful Sets"),
"replicasets": _i18n._("Replica Sets"),
"jobs": _i18n._("Jobs"),
"cronjobs": _i18n._("Cron Jobs"),
"endpoints": _i18n._("Endpoints"),
"customresourcedefinitions": _i18n._("Custom Resource Definitions"),
"horizontalpodautoscalers": _i18n._("Horizontal Pod Autoscalers"),
"podsecuritypolicies": _i18n._("Pod Security Policies"),
"poddisruptionbudgets": _i18n._("Pod Disruption Budgets"),
}