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

Add notification to user to add accessible namespaces (#2173)

This commit is contained in:
Sebastian Malton 2021-03-23 13:21:47 -04:00 committed by GitHub
parent 0b99377feb
commit ee4d434d35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 85 additions and 18 deletions

View File

@ -0,0 +1,11 @@
/**
* This channel is broadcast on whenever the cluster fails to list namespaces
* during a refresh and no `accessibleNamespaces` have been set.
*/
export const ClusterListNamespaceForbiddenChannel = "cluster:list-namespace-forbidden";
export type ListNamespaceForbiddenArgs = [clusterId: string];
export function isListNamespaceForbiddenArgs(args: unknown[]): args is ListNamespaceForbiddenArgs {
return args.length === 1 && typeof args[0] === "string";
}

View File

@ -1,4 +1,5 @@
export * from "./ipc"; export * from "./ipc";
export * from "./invalid-kubeconfig"; export * from "./invalid-kubeconfig";
export * from "./update-available"; export * from "./update-available.ipc";
export * from "./cluster.ipc";
export * from "./type-enforced-ipc"; export * from "./type-enforced-ipc";

View File

@ -3,10 +3,7 @@ import { UpdateInfo } from "electron-updater";
export const UpdateAvailableChannel = "update-available"; export const UpdateAvailableChannel = "update-available";
export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]"; export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]";
/** export type UpdateAvailableFromMain = [backChannel: string, updateInfo: UpdateInfo];
* [<back-channel>, <update-info>]
*/
export type UpdateAvailableFromMain = [string, UpdateInfo];
export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain { export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain {
if (args.length !== 2) { if (args.length !== 2) {
@ -32,7 +29,7 @@ export type BackchannelArg = {
now: boolean; now: boolean;
}; };
export type UpdateAvailableToBackchannel = [BackchannelArg]; export type UpdateAvailableToBackchannel = [updateDecision: BackchannelArg];
export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel { export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel {
if (args.length !== 1) { if (args.length !== 1) {

View File

@ -4,9 +4,9 @@ import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
import type { WorkspaceId } from "../common/workspace-store"; import type { WorkspaceId } from "../common/workspace-store";
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx"; import { action, comparer, computed, observable, reaction, toJS, when } from "mobx";
import { apiKubePrefix } from "../common/vars"; import { apiKubePrefix } from "../common/vars";
import { broadcastMessage, InvalidKubeconfigChannel } from "../common/ipc"; import { broadcastMessage, InvalidKubeconfigChannel, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
import { ContextHandler } from "./context-handler"; import { ContextHandler } from "./context-handler";
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
import { Kubectl } from "./kubectl"; import { Kubectl } from "./kubectl";
import { KubeconfigManager } from "./kubeconfig-manager"; import { KubeconfigManager } from "./kubeconfig-manager";
import { loadConfig, validateKubeConfig } from "../common/kube-helpers"; import { loadConfig, validateKubeConfig } from "../common/kube-helpers";
@ -690,10 +690,14 @@ export class Cluster implements ClusterModel, ClusterState {
return namespaceList.body.items.map(ns => ns.metadata.name); return namespaceList.body.items.map(ns => ns.metadata.name);
} catch (error) { } catch (error) {
const ctx = (await this.getProxyKubeconfig()).getContextObject(this.contextName); const ctx = (await this.getProxyKubeconfig()).getContextObject(this.contextName);
const namespaceList = [ctx.namespace].filter(Boolean);
if (ctx.namespace) return [ctx.namespace]; if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id });
broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id);
}
return []; return namespaceList;
} }
} }

View File

@ -15,6 +15,7 @@ import { clusterStore } from "../../../common/cluster-store";
import { PageLayout } from "../layout/page-layout"; import { PageLayout } from "../layout/page-layout";
import { requestMain } from "../../../common/ipc"; import { requestMain } from "../../../common/ipc";
import { clusterActivateHandler, clusterRefreshHandler } from "../../../common/cluster-ipc"; import { clusterActivateHandler, clusterRefreshHandler } from "../../../common/cluster-ipc";
import { navigation } from "../../navigation";
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> { interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
} }
@ -30,6 +31,10 @@ export class ClusterSettings extends React.Component<Props> {
} }
componentDidMount() { componentDidMount() {
const { hash } = navigation.location;
document.getElementById(hash.slice(1))?.scrollIntoView();
disposeOnUnmount(this, [ disposeOnUnmount(this, [
reaction(() => this.cluster, this.refreshCluster, { reaction(() => this.cluster, this.refreshCluster, {
fireImmediately: true, fireImmediately: true,

View File

@ -16,7 +16,7 @@ export class ClusterAccessibleNamespaces extends React.Component<Props> {
render() { render() {
return ( return (
<> <>
<SubTitle title="Accessible Namespaces" /> <SubTitle title="Accessible Namespaces" id="accessible-namespaces" />
<p>This setting is useful for manually specifying which namespaces you have access to. This is useful when you do not have permissions to list namespaces.</p> <p>This setting is useful for manually specifying which namespaces you have access to. This is useful when you do not have permissions to list namespaces.</p>
<EditableList <EditableList
placeholder="Add new namespace..." placeholder="Add new namespace..."

View File

@ -50,6 +50,10 @@
color: white; color: white;
} }
*:target {
color: $textColorAccent;
}
html { html {
font-size: 62.5%; // 1 rem == 10px font-size: 62.5%; // 1 rem == 10px
color: $textColorPrimary; color: $textColorPrimary;

View File

@ -6,19 +6,18 @@ interface Props {
className?: string; className?: string;
title: React.ReactNode; title: React.ReactNode;
compact?: boolean; // no bottom padding compact?: boolean; // no bottom padding
id?: string;
} }
export class SubTitle extends React.Component<Props> { export class SubTitle extends React.Component<Props> {
render() { render() {
const { compact, title, children } = this.props; const { className, compact, title, children, id } = this.props;
let { className } = this.props; const classNames = cssNames("SubTitle", className, {
className = cssNames("SubTitle", className, {
compact, compact,
}); });
return ( return (
<div className={className}> <div className={classNames} id={id}>
{title} {children} {title} {children}
</div> </div>
); );

View File

@ -1,10 +1,13 @@
import React from "react"; import React from "react";
import { ipcRenderer, IpcRendererEvent } from "electron"; import { ipcRenderer, IpcRendererEvent } from "electron";
import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg } from "../../common/ipc"; import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg, ClusterListNamespaceForbiddenChannel, isListNamespaceForbiddenArgs, ListNamespaceForbiddenArgs } from "../../common/ipc";
import { Notifications, notificationsStore } from "../components/notifications"; import { Notifications, notificationsStore } from "../components/notifications";
import { Button } from "../components/button"; import { Button } from "../components/button";
import { isMac } from "../../common/vars"; import { isMac } from "../../common/vars";
import { invalidKubeconfigHandler } from "./invalid-kubeconfig-handler"; import { invalidKubeconfigHandler } from "./invalid-kubeconfig-handler";
import { clusterStore } from "../../common/cluster-store";
import { navigate } from "../navigation";
import { clusterSettingsURL } from "../components/+cluster-settings";
function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void { function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void {
notificationsStore.remove(notificationId); notificationsStore.remove(notificationId);
@ -42,7 +45,8 @@ function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, update
<Button active outlined label="No" onClick={() => sendToBackchannel(backchannel, notificationId, { doUpdate: false })} /> <Button active outlined label="No" onClick={() => sendToBackchannel(backchannel, notificationId, { doUpdate: false })} />
</div> </div>
</div> </div>
), { ),
{
id: notificationId, id: notificationId,
onClose() { onClose() {
sendToBackchannel(backchannel, notificationId, { doUpdate: false }); sendToBackchannel(backchannel, notificationId, { doUpdate: false });
@ -51,6 +55,42 @@ function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, update
); );
} }
const listNamespacesForbiddenHandlerDisplayedAt = new Map<string, number>();
const intervalBetweenNotifications = 1000 * 60; // 60s
function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]: ListNamespaceForbiddenArgs): void {
const lastDisplayedAt = listNamespacesForbiddenHandlerDisplayedAt.get(clusterId);
const wasDisplayed = Boolean(lastDisplayedAt);
const now = Date.now();
if (!wasDisplayed || (now - lastDisplayedAt) > intervalBetweenNotifications) {
listNamespacesForbiddenHandlerDisplayedAt.set(clusterId, now);
} else {
// don't bother the user too often
return;
}
const notificationId = `list-namespaces-forbidden:${clusterId}`;
Notifications.info(
(
<div className="flex column gaps">
<b>Add Accessible Namespaces</b>
<p>Cluster <b>{clusterStore.active.name}</b> does not have permissions to list namespaces. Please add the namespaces you have access to.</p>
<div className="flex gaps row align-left box grow">
<Button active outlined label="Go to Accessible Namespaces Settings" onClick={()=> {
navigate(clusterSettingsURL({ params: { clusterId }, fragment: "accessible-namespaces" }));
notificationsStore.remove(notificationId);
}} />
</div>
</div>
),
{
id: notificationId,
}
);
}
export function registerIpcHandlers() { export function registerIpcHandlers() {
onCorrect({ onCorrect({
source: ipcRenderer, source: ipcRenderer,
@ -59,4 +99,10 @@ export function registerIpcHandlers() {
verifier: areArgsUpdateAvailableFromMain, verifier: areArgsUpdateAvailableFromMain,
}); });
onCorrect(invalidKubeconfigHandler); onCorrect(invalidKubeconfigHandler);
onCorrect({
source: ipcRenderer,
channel: ClusterListNamespaceForbiddenChannel,
listener: ListNamespacesForbiddenHandler,
verifier: isListNamespaceForbiddenArgs,
});
} }