Add eslint rule padding-line-between-statements (#1593)

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
This commit is contained in:
Panu Horsmalahti authored and GitHub committed 2020-12-02 09:55:52 +02:00
1 parent 7b77f18376
commit dcf253e7d5
401 files changed
+2018 -40

No files matched your search

+2
View File
@@ -25,6 +25,7 @@ export class ApiManager {
protected resolveApi(api: string | KubeApi): KubeApi {
if (typeof api === "string") return this.getApi(api);
return api;
}
@@ -33,6 +34,7 @@ export class ApiManager {
else {
const apis = Array.from(this.apis.entries());
const entry = apis.find(entry => entry[1] === api);
if (entry) this.unregisterApi(entry[0]);
}
}
@@ -90,6 +90,7 @@ export class Cluster extends KubeObject {
if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING;
if (!this.status || !this.status) return ClusterStatus.CREATING;
if (this.status.errorMessage) return ClusterStatus.ERROR;
return ClusterStatus.ACTIVE;
}
}
+5
View File
@@ -75,6 +75,7 @@ export class CustomResourceDefinition extends KubeObject {
getResourceApiBase() {
const { group } = this.spec;
return `/apis/${group}/${this.getVersion()}/${this.getPluralName()}`;
}
@@ -88,6 +89,7 @@ export class CustomResourceDefinition extends KubeObject {
getResourceTitle() {
const name = this.getPluralName();
return name[0].toUpperCase() + name.substr(1);
}
@@ -124,6 +126,7 @@ export class CustomResourceDefinition extends KubeObject {
const columns = this.spec.versions.find(a => this.getVersion() == a.name)?.additionalPrinterColumns
?? this.spec.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape
?? [];
return columns
.filter(column => column.name != "Age")
.filter(column => ignorePriority ? true : !column.priority);
@@ -135,8 +138,10 @@ export class CustomResourceDefinition extends KubeObject {
getConditions() {
if (!this.status?.conditions) return [];
return this.status.conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition;
return {
...condition,
isReady: status === "True",
@@ -71,6 +71,7 @@ export class CronJob extends KubeObject {
getLastScheduleTime() {
if (!this.status.lastScheduleTime) return "-";
const diff = moment().diff(this.status.lastScheduleTime);
return formatDuration(diff, true);
}
@@ -84,7 +85,9 @@ export class CronJob extends KubeObject {
const stamps = schedule.split(" ");
const day = Number(stamps[stamps.length - 3]); // 1-31
const month = Number(stamps[stamps.length - 2]); // 1-12
if (schedule.startsWith("@")) return false;
return day > daysInMonth[month - 1];
}
}
@@ -66,6 +66,7 @@ export class DaemonSet extends WorkloadKubeObject {
getImages() {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
const initContainers: IPodContainer[] = get(this, "spec.template.spec.initContainers", []);
return [...containers, ...initContainers].map(container => container.image);
}
}
@@ -171,10 +171,13 @@ export class Deployment extends WorkloadKubeObject {
getConditions(activeOnly = false) {
const { conditions } = this.status;
if (!conditions) return [];
if (activeOnly) {
return conditions.filter(c => c.status === "True");
}
return conditions;
}
@@ -73,11 +73,13 @@ export class EndpointSubset implements IEndpointSubset {
getAddresses(): EndpointAddress[] {
const addresses = this.addresses || [];
return addresses.map(a => new EndpointAddress(a));
}
getNotReadyAddresses(): EndpointAddress[] {
const notReadyAddresses = this.notReadyAddresses || [];
return notReadyAddresses.map(a => new EndpointAddress(a));
}
@@ -85,10 +87,12 @@ export class EndpointSubset implements IEndpointSubset {
if(!this.addresses) {
return "";
}
return this.addresses.map(address => {
if (!this.ports) {
return address.ip;
}
return this.ports.map(port => {
return `${address.ip}:${port.port}`;
}).join(", ");
@@ -106,6 +110,7 @@ export class Endpoint extends KubeObject {
getEndpointSubsets(): EndpointSubset[] {
const subsets = this.subsets || [];
return subsets.map(s => new EndpointSubset(s));
}
+3
View File
@@ -39,16 +39,19 @@ export class KubeEvent extends KubeObject {
getSource() {
const { component, host } = this.source;
return `${component} ${host || ""}`;
}
getFirstSeenTime() {
const diff = moment().diff(this.firstTimestamp);
return formatDuration(diff, true);
}
getLastSeenTime() {
const diff = moment().diff(this.lastTimestamp);
return formatDuration(diff, true);
}
}
@@ -33,11 +33,13 @@ export const helmChartsApi = {
get(repo: string, name: string, readmeVersion?: string) {
const path = endpoint({ repo, name });
return apiBase
.get<IHelmChartDetails>(`${path}?${stringify({ version: readmeVersion })}`)
.then(data => {
const versions = data.versions.map(HelmChart.create);
const readme = data.readme;
return {
readme,
versions,
@@ -76,9 +76,11 @@ export const helmReleasesApi = {
get(name: string, namespace: string) {
const path = endpoint({ name, namespace });
return apiBase.get<IReleaseRawDetails>(path).then(details => {
const items: KubeObject[] = JSON.parse(details.resources).items;
const resources = items.map(item => KubeObject.create(item));
return {
...details,
resources
@@ -88,35 +90,43 @@ export const helmReleasesApi = {
create(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
const { repo, ...data } = payload;
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
return apiBase.post(endpoint(), { data });
},
update(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
const { repo, ...data } = payload;
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
return apiBase.put(endpoint({ name, namespace }), { data });
},
async delete(name: string, namespace: string) {
const path = endpoint({ name, namespace });
return apiBase.del(path);
},
getValues(name: string, namespace: string) {
const path = `${endpoint({ name, namespace })}/values`;
return apiBase.get<string>(path);
},
getHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
const path = `${endpoint({ name, namespace })}/history`;
return apiBase.get(path);
},
rollback(name: string, namespace: string, revision: number) {
const path = `${endpoint({ name, namespace })}/rollback`;
return apiBase.put(path, {
data: {
revision
@@ -157,10 +167,13 @@ export class HelmRelease implements ItemObject {
getChart(withVersion = false) {
let chart = this.chart;
if(!withVersion && this.getVersion() != "" ) {
const search = new RegExp(`-${this.getVersion()}`);
chart = chart.replace(search, "");
}
return chart;
}
@@ -174,6 +187,7 @@ export class HelmRelease implements ItemObject {
getVersion() {
const versions = this.chart.match(/(v?\d+)[^-].*$/);
if (versions) {
return versions[0];
}
@@ -187,9 +201,11 @@ export class HelmRelease implements ItemObject {
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
const updatedDate = new Date(updated).getTime();
const diff = now - updatedDate;
if (humanize) {
return formatDuration(diff, compact);
}
return diff;
}
@@ -200,6 +216,7 @@ export class HelmRelease implements ItemObject {
const version = this.getVersion();
const versions = await helmChartStore.getVersions(chartName);
const chartVersion = versions.find(chartVersion => chartVersion.version === version);
return chartVersion ? chartVersion.repo : "";
}
}
+6
View File
@@ -80,8 +80,10 @@ export class HorizontalPodAutoscaler extends KubeObject {
getConditions() {
if (!this.status.conditions) return [];
return this.status.conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition;
return {
...condition,
isReady: status === "True",
@@ -100,6 +102,7 @@ export class HorizontalPodAutoscaler extends KubeObject {
protected getMetricName(metric: IHpaMetric): string {
const { type, resource, pods, object, external } = metric;
switch (type) {
case HpaMetricType.Resource:
return resource.name;
@@ -122,14 +125,17 @@ export class HorizontalPodAutoscaler extends KubeObject {
const target = metric[metricType];
let currentValue = "unknown";
let targetValue = "unknown";
if (current) {
currentValue = current.currentAverageUtilization || current.currentAverageValue || current.currentValue;
if (current.currentAverageUtilization) currentValue += "%";
}
if (target) {
targetValue = target.targetAverageUtilization || target.targetAverageValue || target.targetValue;
if (target.targetAverageUtilization) targetValue += "%";
}
return `${currentValue} / ${targetValue}`;
}
}
+7 -1
View File
@@ -6,6 +6,7 @@ import { KubeApi } from "../kube-api";
export class IngressApi extends KubeApi<Ingress> {
getMetrics(ingress: string, namespace: string): Promise<IIngressMetrics> {
const opts = { category: "ingress", ingress };
return metricsApi.getMetrics({
bytesSentSuccess: opts,
bytesSentFailure: opts,
@@ -98,15 +99,18 @@ export class Ingress extends KubeObject {
getRoutes() {
const { spec: { tls, rules } } = this;
if (!rules) return [];
let protocol = "http";
const routes: string[] = [];
if (tls && tls.length > 0) {
protocol += "s";
}
rules.map(rule => {
const host = rule.host ? rule.host : "*";
if (rule.http && rule.http.paths) {
rule.http.paths.forEach(path => {
const { serviceName, servicePort } = getBackendServiceNamePort(path.backend);
@@ -132,7 +136,9 @@ export class Ingress extends KubeObject {
getHosts() {
const { spec: { rules } } = this;
if (!rules) return [];
return rules.filter(rule => rule.host).map(rule => rule.host);
}
@@ -141,7 +147,6 @@ export class Ingress extends KubeObject {
const { spec: { tls, rules, backend, defaultBackend } } = this;
const httpPort = 80;
const tlsPort = 443;
// Note: not using the port name (string)
const servicePort = defaultBackend?.service.port.number ?? backend?.servicePort;
@@ -152,6 +157,7 @@ export class Ingress extends KubeObject {
} else if (servicePort !== undefined) {
ports.push(Number(servicePort));
}
if (tls && tls.length > 0) {
ports.push(tlsPort);
}
+4
View File
@@ -86,12 +86,15 @@ export class Job extends WorkloadKubeObject {
// Type of Job condition could be only Complete or Failed
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#jobcondition-v1-batch
const { conditions } = this.status;
if (!conditions) return;
return conditions.find(({ status }) => status === "True");
}
getImages() {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
return [...containers].map(container => container.image);
}
@@ -99,6 +102,7 @@ export class Job extends WorkloadKubeObject {
const params: JsonApiParams = {
query: { propagationPolicy: "Background" }
};
return super.delete(params);
}
}
@@ -41,6 +41,7 @@ export const metricsApi = {
if (!start && !end) {
const timeNow = Date.now() / 1000;
const now = moment.unix(timeNow).startOf("minute").unix(); // round date to minutes
start = now - range;
end = now;
}
@@ -76,8 +77,10 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
// fill the gaps
result.forEach(res => {
if (!res.values || !res.values.length) return;
while (res.values.length < frames) {
const timestamp = moment.unix(res.values[0][0]).subtract(1, "minute").unix();
res.values.unshift([timestamp, "0"]);
}
});
@@ -101,14 +104,17 @@ export function isMetricsEmpty(metrics: { [key: string]: IMetrics }) {
export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: string): { [key: string]: IMetrics } {
if (!metrics) return;
const itemMetrics = { ...metrics };
for (const metric in metrics) {
if (!metrics[metric]?.data?.result) {
continue;
}
const results = metrics[metric].data.result;
const result = results.find(res => Object.values(res.metric)[0] == itemName);
itemMetrics[metric].data.result = result ? [result] : [];
}
return itemMetrics;
}
@@ -118,11 +124,13 @@ export function getMetricLastPoints(metrics: { [key: string]: IMetrics }) {
Object.keys(metrics).forEach(metricName => {
try {
const metric = metrics[metricName];
if (metric.data.result.length) {
result[metricName] = +metric.data.result[0].values.slice(-1)[0][1];
}
} catch (e) {
}
return result;
}, {});
@@ -55,6 +55,7 @@ export class NetworkPolicy extends KubeObject {
getMatchLabels(): string[] {
if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) return [];
return Object
.entries(this.spec.podSelector.matchLabels)
.map(data => data.join(":"));
@@ -62,6 +63,7 @@ export class NetworkPolicy extends KubeObject {
getTypes(): string[] {
if (!this.spec.policyTypes) return [];
return this.spec.policyTypes;
}
}
+9
View File
@@ -87,9 +87,12 @@ export class Node extends KubeObject {
getNodeConditionText() {
const { conditions } = this.status;
if (!conditions) return "";
return conditions.reduce((types, current) => {
if (current.status !== "True") return "";
return types += ` ${current.type}`;
}, "");
}
@@ -112,19 +115,23 @@ export class Node extends KubeObject {
getCpuCapacity() {
if (!this.status.capacity || !this.status.capacity.cpu) return 0;
return cpuUnitsToNumber(this.status.capacity.cpu);
}
getMemoryCapacity() {
if (!this.status.capacity || !this.status.capacity.memory) return 0;
return unitsToBytes(this.status.capacity.memory);
}
getConditions() {
const conditions = this.status.conditions || [];
if (this.isUnschedulable()) {
return [{ type: "SchedulingDisabled", status: "True" }, ...conditions];
}
return conditions;
}
@@ -134,6 +141,7 @@ export class Node extends KubeObject {
getWarningConditions() {
const goodConditions = ["Ready", "HostUpgrades", "SchedulingDisabled"];
return this.getActiveConditions().filter(condition => {
return !goodConditions.includes(condition.type);
});
@@ -145,6 +153,7 @@ export class Node extends KubeObject {
getOperatingSystem(): string {
const label = this.getLabels().find(label => label.startsWith("kubernetes.io/os="));
if (label) {
return label.split("=", 2)[1];
}
@@ -52,6 +52,7 @@ export class PersistentVolumeClaim extends KubeObject {
getPods(allPods: Pod[]): Pod[] {
const pods = allPods.filter(pod => pod.getNs() === this.getNs());
return pods.filter(pod => {
return pod.getVolumes().filter(volume =>
volume.persistentVolumeClaim &&
@@ -62,22 +63,26 @@ export class PersistentVolumeClaim extends KubeObject {
getStorage(): string {
if (!this.spec.resources || !this.spec.resources.requests) return "-";
return this.spec.resources.requests.storage;
}
getMatchLabels(): string[] {
if (!this.spec.selector || !this.spec.selector.matchLabels) return [];
return Object.entries(this.spec.selector.matchLabels)
.map(([name, val]) => `${name}:${val}`);
}
getMatchExpressions() {
if (!this.spec.selector || !this.spec.selector.matchExpressions) return [];
return this.spec.selector.matchExpressions;
}
getStatus(): string {
if (this.status) return this.status.phase;
return "-";
}
}
@@ -47,20 +47,25 @@ export class PersistentVolume extends KubeObject {
getCapacity(inBytes = false) {
const capacity = this.spec.capacity;
if (capacity) {
if (inBytes) return unitsToBytes(capacity.storage);
return capacity.storage;
}
return 0;
}
getStatus() {
if (!this.status) return;
return this.status.phase || "-";
}
getClaimRefName() {
const { claimRef } = this.spec;
return claimRef ? claimRef.name : "";
}
}
@@ -22,6 +22,7 @@ export class PodDisruptionBudget extends KubeObject {
getSelectors() {
const selector = this.spec.selector;
return KubeObject.stringifyLabels(selector ? selector.matchLabels : null);
}
+27
View File
@@ -6,6 +6,7 @@ import { KubeApi } from "../kube-api";
export class PodsApi extends KubeApi<Pod> {
async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> {
const path = `${this.getUrl(params)}/log`;
return this.request.get(path, { query });
}
@@ -247,6 +248,7 @@ export class Pod extends WorkloadKubeObject {
getRunningContainers() {
const statuses = this.getContainerStatuses();
return this.getAllContainers().filter(container => {
return statuses.find(status => status.name === container.name && !!status.state["running"]);
}
@@ -256,18 +258,23 @@ export class Pod extends WorkloadKubeObject {
getContainerStatuses(includeInitContainers = true) {
const statuses: IPodContainerStatus[] = [];
const { containerStatuses, initContainerStatuses } = this.status;
if (containerStatuses) {
statuses.push(...containerStatuses);
}
if (includeInitContainers && initContainerStatuses) {
statuses.push(...initContainerStatuses);
}
return statuses;
}
getRestartsCount(): number {
const { containerStatuses } = this.status;
if (!containerStatuses) return 0;
return containerStatuses.reduce((count, item) => count + item.restartCount, 0);
}
@@ -290,18 +297,23 @@ export class Pod extends WorkloadKubeObject {
const goodConditions = ["Initialized", "Ready"].every(condition =>
!!this.getConditions().find(item => item.type === condition && item.status === "True")
);
if (reason === PodStatus.EVICTED) {
return PodStatus.EVICTED;
}
if (phase === PodStatus.FAILED) {
return PodStatus.FAILED;
}
if (phase === PodStatus.SUCCEEDED) {
return PodStatus.SUCCEEDED;
}
if (phase === PodStatus.RUNNING && goodConditions) {
return PodStatus.RUNNING;
}
return PodStatus.PENDING;
}
@@ -312,20 +324,26 @@ export class Pod extends WorkloadKubeObject {
let message = "";
const statuses = this.getContainerStatuses(false); // not including initContainers
if (statuses.length) {
statuses.forEach(status => {
const { state } = status;
if (state.waiting) {
const { reason } = state.waiting;
message = reason ? reason : "Waiting";
}
if (state.terminated) {
const { reason } = state.terminated;
message = reason ? reason : "Terminated";
}
});
}
if (message) return message;
return this.getStatusPhase();
}
@@ -349,7 +367,9 @@ export class Pod extends WorkloadKubeObject {
getNodeSelectors(): string[] {
const { nodeSelector } = this.spec;
if (!nodeSelector) return [];
return Object.entries(nodeSelector).map(values => values.join(": "));
}
@@ -367,8 +387,10 @@ export class Pod extends WorkloadKubeObject {
});
const crashLoop = !!this.getContainerStatuses().find(condition => {
const waiting = condition.state.waiting;
return (waiting && waiting.reason == "CrashLoopBackOff");
});
return (
notReady ||
crashLoop ||
@@ -391,18 +413,22 @@ export class Pod extends WorkloadKubeObject {
periodSeconds, successThreshold, failureThreshold
} = probeData;
const probe = [];
// HTTP Request
if (httpGet) {
const { path, port, host, scheme } = httpGet;
probe.push(
"http-get",
`${scheme.toLowerCase()}://${host || ""}:${port || ""}${path || ""}`,
);
}
// Command
if (exec && exec.command) {
probe.push(`exec [${exec.command.join(" ")}]`);
}
// TCP Probe
if (tcpSocket && tcpSocket.port) {
probe.push(`tcp-socket :${tcpSocket.port}`);
@@ -414,6 +440,7 @@ export class Pod extends WorkloadKubeObject {
`#success=${successThreshold || "0"}`,
`#failure=${failureThreshold || "0"}`,
);
return probe;
}
@@ -78,6 +78,7 @@ export class PodSecurityPolicy extends KubeObject {
getRules() {
const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec;
return {
fsGroup: fsGroup ? fsGroup.rule : "",
runAsGroup: runAsGroup ? runAsGroup.rule : "",
@@ -48,6 +48,7 @@ export class ReplicaSet extends WorkloadKubeObject {
getImages() {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
return [...containers].map(container => container.image);
}
}
@@ -13,17 +13,20 @@ export const resourceApplierApi = {
if (typeof resource === "string") {
resource = jsYaml.safeLoad(resource);
}
return apiBase
.post<KubeJsonApiData[]>("/stack", { data: resource })
.then(data => {
const items = data.map(obj => {
const api = apiManager.getApi(obj.metadata.selfLink);
if (api) {
return new api.objectConstructor(obj);
} else {
return new KubeObject(obj);
}
});
return items.length === 1 ? items[0] : items;
});
}
@@ -58,6 +58,7 @@ export class ResourceQuota extends KubeObject {
getScopeSelector() {
const { matchExpressions = [] } = this.spec.scopeSelector || {};
return matchExpressions;
}
}
@@ -38,16 +38,19 @@ export class SelfSubjectRulesReview extends KubeObject {
getResourceRules() {
const rules = this.status && this.status.resourceRules || [];
return rules.map(rule => this.normalize(rule));
}
getNonResourceRules() {
const rules = this.status && this.status.nonResourceRules || [];
return rules.map(rule => this.normalize(rule));
}
protected normalize(rule: ISelfSubjectReviewRule): ISelfSubjectReviewRule {
const { apiGroups = [], resourceNames = [], verbs = [], nonResourceURLs = [], resources = [] } = rule;
return {
apiGroups,
nonResourceURLs,
@@ -56,6 +59,7 @@ export class SelfSubjectRulesReview extends KubeObject {
resources: resources.map((resource, index) => {
const apiGroup = apiGroups.length >= index + 1 ? apiGroups[index] : apiGroups.slice(-1)[0];
const separator = apiGroup == "" ? "" : ".";
return resource + separator + apiGroup;
})
};
@@ -61,9 +61,11 @@ export class Service extends KubeObject {
getExternalIps() {
const lb = this.getLoadBalancer();
if (lb && lb.ingress) {
return lb.ingress.map(val => val.ip || val.hostname);
}
return this.spec.externalIPs || [];
}
@@ -73,11 +75,13 @@ export class Service extends KubeObject {
getSelector(): string[] {
if (!this.spec.selector) return [];
return Object.entries(this.spec.selector).map(val => val.join("="));
}
getPorts(): ServicePort[] {
const ports = this.spec.ports || [];
return ports.map(p => new ServicePort(p));
}
@@ -102,6 +102,7 @@ export class StatefulSet extends WorkloadKubeObject {
getImages() {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
return [...containers].map(container => container.image);
}
}
@@ -18,6 +18,7 @@ export class StorageClass extends KubeObject {
isDefault() {
const annotations = this.metadata.annotations || {};
return (
annotations["storageclass.kubernetes.io/is-default-class"] === "true" ||
annotations["storageclass.beta.kubernetes.io/is-default-class"] === "true"
+11
View File
@@ -75,11 +75,14 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
let reqUrl = this.config.apiBase + path;
const reqInit: RequestInit = { ...this.reqInit, ...init };
const { data, query } = params || {} as P;
if (data && !reqInit.body) {
reqInit.body = JSON.stringify(data);
}
if (query) {
const queryString = stringify(query);
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
}
const infoLog: JsonApiLog = {
@@ -87,6 +90,7 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
reqUrl,
reqInit,
};
return cancelableFetch(reqUrl, reqInit).then(res => {
return this.parseResponse<D>(res, infoLog);
});
@@ -94,21 +98,26 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
protected parseResponse<D>(res: Response, log: JsonApiLog): Promise<D> {
const { status } = res;
return res.text().then(text => {
let data;
try {
data = text ? JSON.parse(text) : ""; // DELETE-requests might not have response-body
} catch (e) {
data = text;
}
if (status >= 200 && status < 300) {
this.onData.emit(data, res);
this.writeLog({ ...log, data });
return data;
} else if (log.method === "GET" && res.status === 403) {
this.writeLog({ ...log, data });
} else {
const error = new JsonApiErrorParsed(data, this.parseError(data, res));
this.onError.emit(error, res);
this.writeLog({ ...log, error });
throw error;
@@ -126,6 +135,7 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
else if (error.message) {
return [error.message];
}
return [res.statusText || "Error!"];
}
@@ -133,6 +143,7 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
if (!this.config.debug) return;
const { method, reqUrl, ...params } = log;
let textStyle = "font-weight: bold;";
if (params.data) textStyle += "background: green; color: white;";
if (params.error) textStyle += "background: red; color: white;";
console.log(`%c${method} ${reqUrl}`, textStyle, params);
+6 -1
View File
@@ -29,7 +29,6 @@ export function parseKubeApi(path: string): IKubeApiParsed {
path = new URL(path, location.origin).pathname;
const [, prefix, ...parts] = path.split("/");
const apiPrefix = `/${prefix}`;
const [left, right, namespaced] = splitArray(parts, "namespaces");
let apiGroup, apiVersion, namespace, resource, name;
@@ -107,9 +106,11 @@ export function parseKubeApi(path: string): IKubeApiParsed {
export function createKubeApiURL(ref: IKubeApiLinkRef): string {
const { apiPrefix = "/apis", resource, apiVersion, name } = ref;
let { namespace } = ref;
if (namespace) {
namespace = `namespaces/${namespace}`;
}
return [apiPrefix, apiVersion, namespace, resource, name]
.filter(v => v)
.join("/");
@@ -125,6 +126,7 @@ export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): st
// search in registered apis by 'kind' & 'apiVersion'
const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == apiVersion);
if (api) {
return api.getUrl({ namespace, name });
}
@@ -132,8 +134,10 @@ export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): st
// lookup api by generated resource link
const apiPrefixes = ["/apis", "/api"];
const resource = kind.endsWith("s") ? `${kind.toLowerCase()}es` : `${kind.toLowerCase()}s`;
for (const apiPrefix of apiPrefixes) {
const apiLink = createKubeApiURL({ apiPrefix, apiVersion, name, namespace, resource });
if (apiManager.getApi(apiLink)) {
return apiLink;
}
@@ -141,6 +145,7 @@ export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): st
// resolve by kind only (hpa's might use refs to older versions of resources for example)
const apiByKind = apiManager.getApi(api => api.kind === kind);
if (apiByKind) {
return apiByKind.getUrl({ name, namespace });
}
+14
View File
@@ -72,6 +72,7 @@ export function forCluster<T extends KubeObject>(cluster: IKubeApiCluster, kubeC
"X-Cluster-ID": cluster.id
}
});
return new KubeApi({
objectConstructor: kubeClass,
request
@@ -83,6 +84,7 @@ export class KubeApi<T extends KubeObject = any> {
static watchAll(...apis: KubeApi[]) {
const disposers = apis.map(api => api.watch());
return () => disposers.forEach(unwatch => unwatch());
}
@@ -106,6 +108,7 @@ export class KubeApi<T extends KubeObject = any> {
kind = options.objectConstructor?.kind,
isNamespaced = options.objectConstructor?.namespaced
} = options || {};
if (!options.apiBase) {
options.apiBase = objectConstructor.apiBase;
}
@@ -205,6 +208,7 @@ export class KubeApi<T extends KubeObject = any> {
});
const res = await this.request.get<IKubePreferredVersion>(`${this.apiPrefix}/${this.apiGroup}`);
Object.defineProperty(this, "apiVersionPreferred", {
value: res?.preferredVersion?.version ?? null,
});
@@ -236,6 +240,7 @@ export class KubeApi<T extends KubeObject = any> {
namespace: this.isNamespaced ? namespace : undefined,
name,
});
return resourcePath + (query ? `?${stringify(this.normalizeQuery(query))}` : "");
}
@@ -243,14 +248,17 @@ export class KubeApi<T extends KubeObject = any> {
if (query.labelSelector) {
query.labelSelector = [query.labelSelector].flat().join(",");
}
if (query.fieldSelector) {
query.fieldSelector = [query.fieldSelector].flat().join(",");
}
return query;
}
protected parseResponse(data: KubeJsonApiData | KubeJsonApiData[] | KubeJsonApiDataList, namespace?: string): any {
const KubeObjectConstructor = this.objectConstructor;
if (KubeObject.isJsonApiData(data)) {
return new KubeObjectConstructor(data);
}
@@ -258,8 +266,10 @@ export class KubeApi<T extends KubeObject = any> {
// process items list response
if (KubeObject.isJsonApiDataList(data)) {
const { apiVersion, items, metadata } = data;
this.setResourceVersion(namespace, metadata.resourceVersion);
this.setResourceVersion("", metadata.resourceVersion);
return items.map(item => new KubeObjectConstructor({
kind: this.kind,
apiVersion,
@@ -277,6 +287,7 @@ export class KubeApi<T extends KubeObject = any> {
async list({ namespace = "" } = {}, query?: IKubeApiQueryParams): Promise<T[]> {
await this.checkPreferredVersion();
return this.request
.get(this.getUrl({ namespace }), { query })
.then(data => this.parseResponse(data, namespace));
@@ -284,6 +295,7 @@ export class KubeApi<T extends KubeObject = any> {
async get({ name = "", namespace = "default" } = {}, query?: IKubeApiQueryParams): Promise<T> {
await this.checkPreferredVersion();
return this.request
.get(this.getUrl({ namespace, name }), { query })
.then(this.parseResponse);
@@ -310,6 +322,7 @@ export class KubeApi<T extends KubeObject = any> {
async update({ name = "", namespace = "default" } = {}, data?: Partial<T>): Promise<T> {
await this.checkPreferredVersion();
const apiUrl = this.getUrl({ namespace, name });
return this.request
.put(apiUrl, { data })
.then(this.parseResponse);
@@ -318,6 +331,7 @@ export class KubeApi<T extends KubeObject = any> {
async delete({ name = "", namespace = "default" }) {
await this.checkPreferredVersion();
const apiUrl = this.getUrl({ namespace, name });
return this.request.del(apiUrl);
}
+2
View File
@@ -45,9 +45,11 @@ export interface KubeJsonApiError extends JsonApiError {
export class KubeJsonApi extends JsonApi<KubeJsonApiData> {
protected parseError(error: KubeJsonApiError | any, res: Response): string[] {
const { status, reason, message } = error;
if (status && reason) {
return [message || `${status}: ${reason}`];
}
return super.parseError(error, res);
}
}
+7
View File
@@ -65,6 +65,7 @@ export class KubeObject implements ItemObject {
static stringifyLabels(labels: { [name: string]: string }): string[] {
if (!labels) return [];
return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
}
@@ -104,9 +105,11 @@ export class KubeObject implements ItemObject {
return moment(this.metadata.creationTimestamp).fromNow();
}
const diff = new Date().getTime() - new Date(this.metadata.creationTimestamp).getTime();
if (humanize) {
return formatDuration(diff, compact);
}
return diff;
}
@@ -120,14 +123,17 @@ export class KubeObject implements ItemObject {
getAnnotations(filter = false): string[] {
const labels = KubeObject.stringifyLabels(this.metadata.annotations);
return filter ? labels.filter(label => {
const skip = resourceApplierApi.annotations.some(key => label.startsWith(key));
return !skip;
}) : labels;
}
getOwnerRefs() {
const refs = this.metadata.ownerReferences || [];
return refs.map(ownerRef => ({
...ownerRef,
namespace: this.getNs(),
@@ -136,6 +142,7 @@ export class KubeObject implements ItemObject {
getSearchFields() {
const { getName, getId, getNs, getAnnotations, getLabels } = this;
return [
getName(),
getNs(),
+14
View File
@@ -53,8 +53,10 @@ export class KubeWatchApi {
apis.forEach(api => {
this.subscribers.set(api, this.getSubscribersCount(api) + 1);
});
return () => apis.forEach(api => {
const count = this.getSubscribersCount(api) - 1;
if (count <= 0) this.subscribers.delete(api);
else this.subscribers.set(api, count);
});
@@ -62,9 +64,11 @@ export class KubeWatchApi {
protected getQuery(): Partial<IKubeWatchRouteQuery> {
const { isAdmin, allowedNamespaces } = getHostedCluster();
return {
api: this.activeApis.map(api => {
if (isAdmin) return api.getWatchUrl();
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace));
}).flat()
};
@@ -74,11 +78,13 @@ export class KubeWatchApi {
@autobind()
protected connect() {
if (this.evtSource) this.disconnect(); // close previous connection
if (!this.activeApis.length) {
return;
}
const query = this.getQuery();
const apiUrl = `${apiPrefix}/watch?${stringify(query)}`;
this.evtSource = new EventSource(apiUrl);
this.evtSource.onmessage = this.onMessage;
this.evtSource.onerror = this.onError;
@@ -102,6 +108,7 @@ export class KubeWatchApi {
protected onMessage(evt: MessageEvent) {
if (!evt.data) return;
const data = JSON.parse(evt.data);
if ((data as IKubeWatchEvent).object) {
this.onData.emit(data);
} else {
@@ -114,12 +121,14 @@ export class KubeWatchApi {
this.disconnect();
const { apiBase, namespace } = KubeApi.parseApi(event.url);
const api = apiManager.getApi(apiBase);
if (api) {
try {
await api.refreshResourceVersion({ namespace });
this.reconnect();
} catch (error) {
console.error("failed to refresh resource version", error);
if (this.subscribers.size > 0) {
setTimeout(() => {
this.onRouteEvent(event);
@@ -132,6 +141,7 @@ export class KubeWatchApi {
protected onError(evt: MessageEvent) {
const { reconnectAttempts: attemptsRemain, reconnectTimeoutMs } = this;
if (evt.eventPhase === EventSource.CLOSED) {
if (attemptsRemain > 0) {
this.reconnectAttempts--;
@@ -150,13 +160,17 @@ export class KubeWatchApi {
const listener = (evt: IKubeWatchEvent<KubeJsonApiData>) => {
const { selfLink, namespace, resourceVersion } = evt.object.metadata;
const api = apiManager.getApi(selfLink);
api.setResourceVersion(namespace, resourceVersion);
api.setResourceVersion("", resourceVersion);
if (store == apiManager.getStore(api)) {
callback(evt);
}
};
this.onData.addListener(listener);
return () => this.onData.removeListener(listener);
}
+11
View File
@@ -50,26 +50,32 @@ export class TerminalApi extends WebSocketApi {
const { id, node } = this.options;
const wss = `ws${protocol === "https:" ? "s" : ""}://`;
const query: TerminalApiQuery = { id };
if (port) {
port = `:${port}`;
}
if (node) {
query.node = node;
query.type = "node";
}
return `${wss}${hostname}${port}/api?${stringify(query)}`;
}
async connect() {
const apiUrl = await this.getUrl();
this.emitStatus("Connecting ...");
this.onData.addListener(this._onReady, { prepend: true });
return super.connect(apiUrl);
}
destroy() {
if (!this.socket) return;
const exitCode = String.fromCharCode(4); // ctrl+d
this.sendCommand(exitCode);
setTimeout(() => super.destroy(), 2000);
}
@@ -87,6 +93,7 @@ export class TerminalApi extends WebSocketApi {
this.onData.removeListener(this._onReady);
this.flush();
this.onData.emit(data); // re-emit data
return false; // prevent calling rest of listeners
}
@@ -100,6 +107,7 @@ export class TerminalApi extends WebSocketApi {
sendTerminalSize(cols: number, rows: number) {
const newSize = { Width: cols, Height: rows };
if (!isEqual(this.size, newSize)) {
this.sendCommand(JSON.stringify(newSize), TerminalChannels.TERMINAL_SIZE);
this.size = newSize;
@@ -108,6 +116,7 @@ export class TerminalApi extends WebSocketApi {
protected parseMessage(data: string) {
data = data.substr(1); // skip channel
return base64.decode(data);
}
@@ -125,10 +134,12 @@ export class TerminalApi extends WebSocketApi {
protected emitStatus(data: string, options: { color?: TerminalColor; showTime?: boolean } = {}) {
const { color, showTime } = options;
if (color) {
data = `${color}${data}${TerminalColor.NO_COLOR}`;
}
let time;
if (showTime) {
time = `${(new Date()).toLocaleString()} `;
}
+7
View File
@@ -47,9 +47,11 @@ export class WebSocketApi {
constructor(protected params: IParams) {
this.params = Object.assign({}, WebSocketApi.defaultParams, params);
const { autoConnect, pingIntervalSeconds } = this.params;
if (autoConnect) {
setTimeout(() => this.connect());
}
if (pingIntervalSeconds) {
this.pingTimer = setInterval(() => this.ping(), pingIntervalSeconds * 1000);
}
@@ -57,6 +59,7 @@ export class WebSocketApi {
get isConnected() {
const state = this.socket ? this.socket.readyState : -1;
return state === WebSocket.OPEN && this.isOnline;
}
@@ -87,6 +90,7 @@ export class WebSocketApi {
reconnect() {
const { reconnectDelaySeconds } = this.params;
if (!reconnectDelaySeconds) return;
this.writeLog("reconnect after", `${reconnectDelaySeconds}ms`);
this.reconnectTimer = setTimeout(() => this.connect(), reconnectDelaySeconds * 1000);
@@ -115,6 +119,7 @@ export class WebSocketApi {
id: (Math.random() * Date.now()).toString(16).replace(".", ""),
data: command,
};
if (this.isConnected) {
this.socket.send(msg.data);
}
@@ -141,6 +146,7 @@ export class WebSocketApi {
protected _onMessage(evt: MessageEvent) {
const data = this.parseMessage(evt.data);
this.onData.emit(data);
this.writeLog("%cMESSAGE", "color:black;font-weight:bold;", data);
}
@@ -151,6 +157,7 @@ export class WebSocketApi {
protected _onClose(evt: CloseEvent) {
const error = evt.code !== 1000 || !evt.wasClean;
if (error) {
this.reconnect();
}
+5
View File
@@ -51,16 +51,19 @@ export class WorkloadKubeObject extends KubeObject {
getSelectors(): string[] {
const selector = this.spec.selector;
return KubeObject.stringifyLabels(selector ? selector.matchLabels : null);
}
getNodeSelectors(): string[] {
const nodeSelector = get(this, "spec.template.spec.nodeSelector");
return KubeObject.stringifyLabels(nodeSelector);
}
getTemplateLabels(): string[] {
const labels = get(this, "spec.template.metadata.labels");
return KubeObject.stringifyLabels(labels);
}
@@ -74,7 +77,9 @@ export class WorkloadKubeObject extends KubeObject {
getAffinityNumber() {
const affinity = this.getAffinity();
if (!affinity) return 0;
return Object.keys(affinity).length;
}
}
+1
View File
@@ -30,6 +30,7 @@ export {
export async function bootstrap(App: AppComponent) {
const rootElem = document.getElementById("app");
rootElem.classList.toggle("is-mac", isMac);
extensionLoader.init();
@@ -68,6 +68,7 @@ export class AddCluster extends React.Component {
Notifications.error(
<div>Can&apos;t setup <code>{filePath}</code> as kubeconfig: {String(err)}</div>
);
if (throwError) {
throw err;
}
@@ -82,12 +83,14 @@ export class AddCluster extends React.Component {
switch (this.sourceTab) {
case KubeConfigSourceTab.FILE:
const contexts = this.getContexts(this.kubeConfigLocal);
this.kubeContexts.replace(contexts);
break;
case KubeConfigSourceTab.TEXT:
try {
this.error = "";
const contexts = this.getContexts(loadConfig(this.customConfig || "{}"));
this.kubeContexts.replace(contexts);
} catch (err) {
this.error = String(err);
@@ -102,9 +105,11 @@ export class AddCluster extends React.Component {
getContexts(config: KubeConfig): Map<string, KubeConfig> {
const contexts = new Map();
splitConfig(config).forEach(config => {
contexts.set(config.currentContext, config);
});
return contexts;
}
@@ -116,6 +121,7 @@ export class AddCluster extends React.Component {
message: _i18n._(t`Select custom kubeconfig file`),
buttonLabel: _i18n._(t`Use configuration`),
});
if (!canceled && filePaths.length) {
this.setKubeConfig(filePaths[0]);
}
@@ -129,9 +135,11 @@ export class AddCluster extends React.Component {
@action
addClusters = () => {
let newClusters: ClusterModel[] = [];
try {
if (!this.selectedContexts.length) {
this.error = <Trans>Please select at least one cluster context</Trans>;
return;
}
this.error = "";
@@ -140,12 +148,16 @@ export class AddCluster extends React.Component {
newClusters = this.selectedContexts.filter(context => {
try {
const kubeConfig = this.kubeContexts.get(context);
validateKubeConfig(kubeConfig);
return true;
} catch (err) {
this.error = String(err.message);
if (err instanceof ExecValidationNotFoundError) {
Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>);
return false;
} else {
throw new Error(err);
@@ -157,6 +169,7 @@ export class AddCluster extends React.Component {
const kubeConfigPath = this.sourceTab === KubeConfigSourceTab.FILE
? this.kubeConfigPath // save link to original kubeconfig in file-system
: ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder
return {
id: clusterId,
kubeConfigPath,
@@ -171,8 +184,10 @@ export class AddCluster extends React.Component {
runInAction(() => {
clusterStore.addClusters(...newClusters);
if (newClusters.length === 1) {
const clusterId = newClusters[0].id;
clusterStore.setActive(clusterId);
navigate(clusterViewURL({ params: { clusterId } }));
} else {
@@ -271,6 +286,7 @@ export class AddCluster extends React.Component {
const placeholder = this.selectedContexts.length > 0
? <Trans>Selected contexts: <b>{this.selectedContexts.length}</b></Trans>
: <Trans>Select contexts</Trans>;
return (
<div>
<Select
@@ -302,8 +318,10 @@ export class AddCluster extends React.Component {
onKubeConfigInputBlur = () => {
const isChanged = this.kubeConfigPath !== userStore.kubeConfigPath;
if (isChanged) {
this.kubeConfigPath = this.kubeConfigPath.replace("~", os.homedir());
try {
this.setKubeConfig(this.kubeConfigPath, { throwError: true });
} catch (err) {
@@ -321,6 +339,7 @@ export class AddCluster extends React.Component {
protected formatContextLabel = ({ value: context }: SelectOption<string>) => {
const isNew = userStore.newContexts.has(context);
const isSelected = this.selectedContexts.includes(context);
return (
<div className={cssNames("kube-context flex gaps align-center", context)}>
<span>{context}</span>
@@ -332,6 +351,7 @@ export class AddCluster extends React.Component {
render() {
const submitDisabled = this.selectedContexts.length === 0;
return (
<DropFileInput onDropFiles={this.onDropKubeConfig}>
<PageLayout className="AddClusters" header={<h2>Add Clusters</h2>}>
@@ -37,6 +37,7 @@ export class HelmChartDetails extends Component<Props> {
chartUpdater = autorun(() => {
this.selectedChart = null;
const { chart: { name, repo, version } } = this.props;
helmChartsApi.get(repo, name, version).then(result => {
this.readme = result.readme;
this.chartVersions = result.versions;
@@ -56,6 +57,7 @@ export class HelmChartDetails extends Component<Props> {
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;
@@ -71,6 +73,7 @@ export class HelmChartDetails extends Component<Props> {
renderIntroduction() {
const { selectedChart, chartVersions, onVersionChange } = this;
const placeholder = require("./helm-placeholder.svg");
return (
<div className="introduction flex align-flex-start">
<img
@@ -146,6 +149,7 @@ export class HelmChartDetails extends Component<Props> {
render() {
const { chart, hideDetails } = this.props;
const title = chart ? <Trans>Chart: {chart.getFullName()}</Trans> : "";
return (
<Drawer
className="HelmChartDetails"
@@ -30,9 +30,11 @@ export class HelmChartStore extends ItemStore<HelmChart> {
async getVersions(chartName: string, force?: boolean): Promise<IChartVersion[]> {
let versions = this.versions.get(chartName);
if (versions && !force) {
return versions;
}
const loadVersions = (repo: string) => {
return helmChartsApi.get(repo, chartName).then(({ versions }) => {
return versions.map(chart => ({
@@ -41,17 +43,20 @@ export class HelmChartStore extends ItemStore<HelmChart> {
}));
});
};
if (!this.isLoaded) {
await this.loadAll();
}
const repos = this.items
.filter(chart => chart.getName() === chartName)
.map(chart => chart.getRepository());
versions = await Promise.all(repos.map(loadVersions))
.then(flatten)
.then(this.sortVersions);
this.versions.set(chartName, versions);
return versions;
}
@@ -29,6 +29,7 @@ export class HelmCharts extends Component<Props> {
get selectedChart() {
const { match: { params: { chartName, repo } } } = this.props;
return helmChartStore.getByName(chartName, repo);
}
@@ -55,6 +55,7 @@ export class ReleaseDetails extends Component<Props> {
const { getReleaseSecret } = releaseStore;
const { release } = this.props;
const secret = getReleaseSecret(release);
if (this.releaseSecret) {
if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) return;
this.loadDetails();
@@ -64,12 +65,14 @@ export class ReleaseDetails extends Component<Props> {
async loadDetails() {
const { release } = this.props;
this.details = null;
this.details = await helmReleasesApi.get(release.getName(), release.getNs());
}
async loadValues() {
const { release } = this.props;
this.values = "";
this.values = await helmReleasesApi.getValues(release.getName(), release.getNs());
}
@@ -84,7 +87,9 @@ export class ReleaseDetails extends Component<Props> {
version: release.getVersion(),
values: this.values
};
this.saving = true;
try {
await releaseStore.update(name, namespace, data);
Notifications.ok(
@@ -98,12 +103,14 @@ export class ReleaseDetails extends Component<Props> {
upgradeVersion = () => {
const { release, hideDetails } = this.props;
createUpgradeChartTab(release);
hideDetails();
};
renderValues() {
const { values, saving } = this;
return (
<div className="values">
<DrawerTitle title={_i18n._(t`Values`)}/>
@@ -127,6 +134,7 @@ export class ReleaseDetails extends Component<Props> {
renderNotes() {
if (!this.details.info?.notes) return null;
const { notes } = this.details.info;
return (
<div className="notes">
{notes}
@@ -136,6 +144,7 @@ export class ReleaseDetails extends Component<Props> {
renderResources() {
const { resources } = this.details;
if (!resources) return null;
const groups = groupBy(resources, item => item.kind);
const tables = Object.entries(groups).map(([kind, items]) => {
@@ -156,6 +165,7 @@ export class ReleaseDetails extends Component<Props> {
name,
namespace,
})) : "";
return (
<TableRow key={item.getId()}>
<TableCell className="name">
@@ -170,6 +180,7 @@ export class ReleaseDetails extends Component<Props> {
</React.Fragment>
);
});
return (
<div className="resources">
{tables}
@@ -180,10 +191,13 @@ export class ReleaseDetails extends Component<Props> {
renderContent() {
const { release } = this.props;
const { details } = this;
if (!release) return null;
if (!details) {
return <Spinner center/>;
}
return (
<div>
<DrawerItem name={<Trans>Chart</Trans>} className="chart">
@@ -229,6 +243,7 @@ export class ReleaseDetails extends Component<Props> {
const { release, hideDetails } = this.props;
const title = release ? <Trans>Release: {release.getName()}</Trans> : "";
const toolbar = <HelmReleaseMenu release={release} toolbar hideDetails={hideDetails}/>;
return (
<Drawer
className={cssNames("ReleaseDetails", themeStore.activeTheme.type)}
@@ -24,6 +24,7 @@ export class HelmReleaseMenu extends React.Component<Props> {
@autobind()
upgrade() {
const { release, hideDetails } = this.props;
createUpgradeChartTab(release);
hideDetails && hideDetails();
}
@@ -35,8 +36,10 @@ export class HelmReleaseMenu extends React.Component<Props> {
renderContent() {
const { release, toolbar } = this.props;
if (!release) return;
const hasRollback = release && release.getRevision() > 1;
return (
<>
{hasRollback && (
@@ -51,6 +54,7 @@ export class HelmReleaseMenu extends React.Component<Props> {
render() {
const { className, release, ...menuProps } = this.props;
return (
<MenuActions
{...menuProps}
@@ -41,6 +41,7 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
this.isLoading = true;
const currentRevision = this.release.getRevision();
let releases = await helmReleasesApi.getHistory(this.release.getName(), this.release.getNs());
releases = releases.filter(item => item.revision !== currentRevision); // remove current
releases = orderBy(releases, "revision", "desc"); // sort
this.revisions.replace(releases);
@@ -50,6 +51,7 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
rollback = async () => {
const revisionNumber = this.revision.revision;
try {
await releaseStore.rollback(this.release.getName(), this.release.getNs(), revisionNumber);
this.close();
@@ -64,9 +66,11 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
renderContent() {
const { revision, revisions } = this;
if (!revision) {
return <p><Trans>No revisions to rollback.</Trans></p>;
}
return (
<div className="flex gaps align-center">
<b><Trans>Revision</Trans></b>
@@ -85,6 +89,7 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
const { ...dialogProps } = this.props;
const releaseName = this.release ? this.release.getName() : "";
const header = <h5><Trans>Rollback <b>{releaseName}</b></Trans></h5>;
return (
<Dialog
{...dialogProps}
@@ -26,9 +26,12 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
const amountChanged = secrets.length !== this.releaseSecrets.length;
const labelsChanged = this.releaseSecrets.some(item => {
const secret = secrets.find(secret => secret.getId() == item.getId());
if (!secret) return;
return !isEqual(item.getLabels(), secret.getLabels());
});
if (amountChanged || labelsChanged) {
this.loadAll();
}
@@ -49,6 +52,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
owner: "helm",
name: release.getName()
};
return secretsStore.getByLabel(labels)
.filter(secret => secret.getNs() == release.getNs())[0];
}
@@ -57,8 +61,10 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
async loadAll() {
this.isLoading = true;
let items;
try {
const { isAdmin, allowedNamespaces } = getHostedCluster();
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
} finally {
if (items) {
@@ -82,19 +88,25 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
async create(payload: IReleaseCreatePayload) {
const response = await helmReleasesApi.create(payload);
if (this.isLoaded) this.loadAll();
return response;
}
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
const response = await helmReleasesApi.update(name, namespace, payload);
if (this.isLoaded) this.loadAll();
return response;
}
async rollback(name: string, namespace: string, revision: number) {
const response = await helmReleasesApi.rollback(name, namespace, revision);
if (this.isLoaded) this.loadAll();
return response;
}
@@ -104,6 +116,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
async removeSelectedItems() {
if (!this.selectedItems.length) return;
return Promise.all(this.selectedItems.map(this.remove));
}
}
@@ -41,6 +41,7 @@ export class HelmReleases extends Component<Props> {
get selectedRelease() {
const { match: { params: { name, namespace } } } = this.props;
return releaseStore.items.find(release => {
return release.getName() == name && release.getNs() == namespace;
});
@@ -66,6 +67,7 @@ export class HelmReleases extends Component<Props> {
renderRemoveDialogMessage(selectedItems: HelmRelease[]) {
const releaseNames = selectedItems.map(item => item.getName()).join(", ");
return (
<div>
<Trans>Remove <b>{releaseNames}</b>?</Trans>
@@ -111,6 +113,7 @@ export class HelmReleases extends Component<Props> {
]}
renderTableContents={(release: HelmRelease) => {
const version = release.getVersion();
return [
release.getName(),
release.getNs(),
+1
View File
@@ -10,6 +10,7 @@ import { namespaceStore } from "../+namespaces/namespace.store";
export class Apps extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
const query = namespaceStore.getContextParams();
return [
{
title: <Trans>Charts</Trans>,
@@ -49,6 +49,7 @@ export class ClusterSettings extends React.Component<Props> {
render() {
const cluster = this.cluster;
if (!cluster) return null;
const header = (
<>
@@ -56,6 +57,7 @@ export class ClusterSettings extends React.Component<Props> {
<h2>{cluster.preferences.clusterName}</h2>
</>
);
return (
<PageLayout className="ClusterSettings" header={header}>
<Status cluster={cluster}></Status>
@@ -25,9 +25,11 @@ export class ClusterIconSetting extends React.Component<Props> {
@autobind()
async onIconPick([file]: File[]) {
const { cluster } = this.props;
try {
if (file) {
const buf = Buffer.from(await file.arrayBuffer());
cluster.preferences.icon = `data:${file.type};base64,${buf.toString("base64")}`;
} else {
// this has to be done as a seperate branch (and not always) because `cluster`
@@ -57,6 +59,7 @@ export class ClusterIconSetting extends React.Component<Props> {
{"Browse for new icon..."}
</>
);
return (
<>
<SubTitle title="Cluster Icon" />
@@ -23,6 +23,7 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
@computed get canEditPrometheusPath() {
if (this.provider === "" || this.provider === "lens") return false;
return true;
}
@@ -30,12 +31,15 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
disposeOnUnmount(this,
autorun(() => {
const { prometheus, prometheusProvider } = this.props.cluster.preferences;
if (prometheus) {
const prefix = prometheus.prefix || "";
this.path = `${prometheus.namespace}/${prometheus.service}:${prometheus.port}${prefix}`;
} else {
this.path = "";
}
if (prometheusProvider) {
this.provider = prometheusProvider.type;
} else {
@@ -51,9 +55,11 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
}
const parsed = this.path.split(/\/|:/, 3);
const apiPrefix = this.path.substring(parsed.join("/").length);
if (!parsed[0] || !parsed[1] || !parsed[2]) {
return null;
}
return {
namespace: parsed[0],
service: parsed[1],
@@ -24,6 +24,7 @@ export class InstallFeature extends React.Component<Props> {
const statusUpdate = interval(20, () => {
feature.updateStatus(cluster);
});
statusUpdate.start(true);
disposeOnUnmount(this, () => {
@@ -42,6 +43,7 @@ export class InstallFeature extends React.Component<Props> {
const { cluster, feature } = this.props;
const disabled = !cluster.isAdmin || this.loading;
const loadingIcon = this.loading ? <Spinner/> : null;
return (
<div className="flex gaps align-center">
{feature.status.canUpgrade &&
@@ -16,6 +16,7 @@ export class RemoveClusterButton extends React.Component<Props> {
@autobind()
confirmRemoveCluster() {
const { cluster } = this.props;
ConfirmDialog.open({
message: <p>Are you sure you want to remove <b>{cluster.preferences.clusterName}</b> from Lens?</p>,
labelOk: <Trans>Yes</Trans>,
@@ -28,6 +29,7 @@ export class RemoveClusterButton extends React.Component<Props> {
render() {
const { cluster } = this.props;
return (
<Button accent onClick={this.confirmRemoveCluster} className="button-area" disabled={cluster.isManaged}>
Remove Cluster
@@ -11,6 +11,7 @@ interface Props {
export class Features extends React.Component<Props> {
render() {
const { cluster } = this.props;
return (
<div>
<h2>Features</h2>
@@ -14,6 +14,7 @@ export class Status extends React.Component<Props> {
@autobind()
openKubeconfig() {
const { cluster } = this.props;
shell.showItemInFolder(cluster.kubeConfigPath);
}
@@ -26,6 +27,7 @@ export class Status extends React.Component<Props> {
["API Address", cluster.apiUrl || "N/A"],
["Nodes Count", cluster.metadata.nodes ? String(cluster.metadata.nodes) : "N/A"]
];
return (
<Table scrollable={false}>
{rows.map(([name, value]) => {
@@ -44,6 +44,7 @@ export class ClusterIssues extends React.Component<Props> {
// Node bad conditions
nodesStore.items.forEach(node => {
const { kind, selfLink, getId, getName } = node;
node.getWarningConditions().forEach(({ message }) => {
warnings.push({
kind,
@@ -57,9 +58,11 @@ export class ClusterIssues extends React.Component<Props> {
// Warning events for Workloads
const events = eventStore.getWarnings();
events.forEach(error => {
const { message, involvedObject } = error;
const { uid, name, kind } = involvedObject;
warnings.push({
getId: () => uid,
getName: () => name,
@@ -77,6 +80,7 @@ export class ClusterIssues extends React.Component<Props> {
const { warnings } = this;
const warning = warnings.find(warn => warn.getId() == uid);
const { getId, getName, message, kind, selfLink } = warning;
return (
<TableRow
key={getId()}
@@ -99,11 +103,13 @@ export class ClusterIssues extends React.Component<Props> {
renderContent() {
const { warnings } = this;
if (!eventStore.isLoaded) {
return (
<Spinner center/>
);
}
if (!warnings.length) {
return (
<div className="no-issues flex column box grow gaps align-center justify-center">
@@ -113,6 +119,7 @@ export class ClusterIssues extends React.Component<Props> {
</div>
);
}
return (
<>
<SubHeader>
@@ -14,6 +14,7 @@ export const ClusterMetricSwitchers = observer(() => {
const metricsValues = getMetricsValues(metrics);
const disableRoles = !masterNodes.length || !workerNodes.length;
const disableMetrics = !metricsValues.length;
return (
<div className="ClusterMetricSwitchers flex gaps">
<div className="box grow">
@@ -42,6 +42,7 @@ export const ClusterMetrics = observer(() => {
callbacks: {
label: ({ index }, data) => {
const value = data.datasets[0].data[index] as ChartPoint;
return value.y.toString();
}
}
@@ -60,6 +61,7 @@ export const ClusterMetrics = observer(() => {
callbacks: {
label: ({ index }, data) => {
const value = data.datasets[0].data[index] as ChartPoint;
return bytesToUnits(parseInt(value.y as string), 3);
}
}
@@ -71,9 +73,11 @@ export const ClusterMetrics = observer(() => {
if ((!metricValues.length || !liveMetricValues.length) && !metricsLoaded) {
return <Spinner center/>;
}
if (!memoryCapacity || !cpuCapacity) {
return <ClusterNoMetrics className="empty"/>;
}
return (
<BarChart
name={`${metricNodeRole}-${metricType}`}
@@ -141,6 +141,7 @@ export const ClusterPieCharts = observer(() => {
`${i18n._(t`Capacity`)}: ${podCapacity}`,
]
};
return (
<div className="NodeCharts flex justify-center box grow gaps">
<div className="chart flex column align-center box grow">
@@ -174,6 +175,7 @@ export const ClusterPieCharts = observer(() => {
const { masterNodes, workerNodes } = nodesStore;
const { metricNodeRole, metricsLoaded } = clusterStore;
const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes;
if (!nodes.length) {
return (
<div className="empty flex column box grow align-center justify-center">
@@ -182,6 +184,7 @@ export const ClusterPieCharts = observer(() => {
</div>
);
}
if (!metricsLoaded) {
return (
<div className="flex justify-center align-center box grow empty">
@@ -190,9 +193,11 @@ export const ClusterPieCharts = observer(() => {
);
}
const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterStore.metrics);
if (!memoryCapacity || !cpuCapacity || !podCapacity) {
return <ClusterNoMetrics className="empty"/>;
}
return renderCharts();
};
@@ -32,9 +32,11 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
// sync user setting with local storage
const storage = createStorage("cluster_metric_switchers", {});
Object.assign(this, storage.get());
reaction(() => {
const { metricType, metricNodeRole } = this;
return { metricType, metricNodeRole };
},
settings => storage.set(settings)
@@ -52,6 +54,7 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
// check which node type to select
reaction(() => nodesStore.items.length, () => {
const { masterNodes, workerNodes } = nodesStore;
if (!masterNodes.length) this.metricNodeRole = MetricNodeRole.WORKER;
if (!workerNodes.length) this.metricNodeRole = MetricNodeRole.MASTER;
});
@@ -61,6 +64,7 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
await when(() => nodesStore.isLoaded);
const { masterNodes, workerNodes } = nodesStore;
const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes;
return clusterApi.getMetrics(nodes.map(node => node.getName()), params);
}
@@ -79,6 +83,7 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
const range = 15;
const end = Date.now() / 1000;
const start = end - range;
this.liveMetrics = await this.loadMetrics({ start, end, step, range });
}
@@ -32,6 +32,7 @@ export class Cluster extends React.Component {
// todo: refactor
async componentDidMount() {
const { dependentStores } = this;
if (!isAllowedResource("nodes")) {
dependentStores.splice(dependentStores.indexOf(nodesStore), 1);
}
@@ -54,6 +55,7 @@ export class Cluster extends React.Component {
render() {
const { isLoaded } = this;
return (
<TabLayout>
<div className="Cluster">
@@ -28,6 +28,7 @@ export class HpaDetails extends React.Component<Props> {
switch (metric.type) {
case HpaMetricType.Resource:
const addition = metric.resource.targetAverageUtilization ? <Trans>(as a percentage of request)</Trans> : "";
return <Trans>Resource {metric.resource.name} on Pods {addition}</Trans>;
case HpaMetricType.Pods:
@@ -37,6 +38,7 @@ export class HpaDetails extends React.Component<Props> {
const { target } = metric.object;
const { kind, name } = target;
const objectUrl = getDetailsUrl(lookupApiLink(target, hpa));
return (
<Trans>
{metric.object.metricName} on{" "}
@@ -63,6 +65,7 @@ export class HpaDetails extends React.Component<Props> {
hpa.getMetrics().map((metric, index) => {
const name = renderName(metric);
const values = hpa.getMetricValues(metric);
return (
<TableRow key={index}>
<TableCell className="name">{name}</TableCell>
@@ -77,8 +80,10 @@ export class HpaDetails extends React.Component<Props> {
render() {
const { object: hpa } = this.props;
if (!hpa) return;
const { scaleTargetRef } = hpa.spec;
return (
<div className="HpaDetails">
<KubeObjectMeta object={hpa}/>
@@ -106,6 +111,7 @@ export class HpaDetails extends React.Component<Props> {
<DrawerItem name={<Trans>Status</Trans>} labelsOnly>
{hpa.getConditions().map(({ type, tooltip, isReady }) => {
if (!isReady) return null;
return (
<Badge
key={type}
@@ -31,6 +31,7 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
const metricsRemainCount = metrics.length - 1;
const metricsRemain = metrics.length > 1 ? <Trans>{metricsRemainCount} more...</Trans> : null;
const metricValues = hpa.getMetricValues(metrics[0]);
return <p>{metricValues} {metricsRemain && "+"}{metricsRemain}</p>;
}
@@ -71,6 +72,7 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
hpa.getAge(),
hpa.getConditions().map(({ type, tooltip, isReady }) => {
if (!isReady) return null;
return (
<Badge
key={type}
@@ -27,6 +27,7 @@ export class ConfigMapDetails extends React.Component<Props> {
disposeOnUnmount(this, [
autorun(() => {
const { object: configMap } = this.props;
if (configMap) {
this.data.replace(configMap.data); // refresh
}
@@ -36,6 +37,7 @@ export class ConfigMapDetails extends React.Component<Props> {
save = async () => {
const { object: configMap } = this.props;
try {
this.isSaving = true;
await configMapsStore.update(configMap, { ...configMap, data: this.data.toJSON() });
@@ -51,8 +53,10 @@ export class ConfigMapDetails extends React.Component<Props> {
render() {
const { object: configMap } = this.props;
if (!configMap) return null;
const data = Object.entries(this.data.toJSON());
return (
<div className="ConfigMapDetails">
<KubeObjectMeta object={configMap}/>
@@ -18,8 +18,10 @@ export class PodDisruptionBudgetDetails extends React.Component<Props> {
render() {
const { object: pdb } = this.props;
if (!pdb) return null;
const selectors = pdb.getSelectors();
return (
<div className="PdbDetails">
<KubeObjectMeta object={pdb}/>
@@ -72,6 +72,7 @@ export class AddQuotaDialog extends React.Component<Props> {
const isStorage = quota.endsWith(".storage") || quota === "persistentvolumeclaims";
const isCount = quota.startsWith("count/");
const icon = isCompute ? "memory" : isStorage ? "storage" : isCount ? "looks_one" : "";
return {
label: icon ? <span className="nobr"><Icon material={icon} /> {quota}</span> : quota,
value: quota,
@@ -102,8 +103,10 @@ export class AddQuotaDialog extends React.Component<Props> {
const { quotaName, namespace } = this;
const quotas = this.quotaEntries.reduce<IResourceQuotaValues>((quotas, [name, value]) => {
quotas[name] = value;
return quotas;
}, {});
await resourceQuotaApi.create({ namespace, name: quotaName }, {
spec: {
hard: quotas
@@ -127,6 +130,7 @@ export class AddQuotaDialog extends React.Component<Props> {
render() {
const { ...dialogProps } = this.props;
const header = <h5><Trans>Create ResourceQuota</Trans></h5>;
return (
<Dialog
{...dialogProps}
@@ -58,7 +58,9 @@ function renderQuotas(quota: ResourceQuota): JSX.Element[] {
export class ResourceQuotaDetails extends React.Component<Props> {
render() {
const { object: quota } = this.props;
if (!quota) return null;
return (
<div className="ResourceQuotaDetails">
<KubeObjectMeta object={quota}/>
@@ -79,6 +81,7 @@ export class ResourceQuotaDetails extends React.Component<Props> {
{
quota.getScopeSelector().map((selector, index) => {
const { operator, scopeName, values } = selector;
return (
<TableRow key={index}>
<TableCell>{operator}</TableCell>
@@ -81,9 +81,11 @@ export class AddSecretDialog extends React.Component<Props> {
private getDataFromFields = (fields: ISecretTemplateField[] = [], processValue?: (val: string) => string) => {
return fields.reduce<any>((data, field) => {
const { key, value } = field;
if (key) {
data[key] = processValue ? processValue(value) : value;
}
return data;
}, {});
};
@@ -101,8 +103,10 @@ export class AddSecretDialog extends React.Component<Props> {
labels: this.getDataFromFields(labels),
} as IKubeObjectMetadata
};
try {
const newSecret = await secretsApi.create({ namespace, name }, secret);
showDetails(newSecret.selfLink);
this.reset();
this.close();
@@ -113,17 +117,20 @@ export class AddSecretDialog extends React.Component<Props> {
addField = (field: ISecretField) => {
const fields = this.secret[this.type][field] || [];
fields.push({ key: "", value: "" });
this.secret[this.type][field] = fields;
};
removeField = (field: ISecretField, index: number) => {
const fields = this.secret[this.type][field] || [];
fields.splice(index, 1);
};
renderFields(field: ISecretField) {
const fields = this.secret[this.type][field] || [];
return (
<>
<SubTitle compact className="fields-title" title={upperFirst(field.toString())}>
@@ -137,6 +144,7 @@ export class AddSecretDialog extends React.Component<Props> {
<div className="secret-fields">
{fields.map((item, index) => {
const { key = "", value = "", required } = item;
return (
<div key={index} className="secret-field flex gaps auto align-center">
<Input
@@ -174,6 +182,7 @@ export class AddSecretDialog extends React.Component<Props> {
const { ...dialogProps } = this.props;
const { namespace, name, type } = this;
const header = <h5><Trans>Create Secret</Trans></h5>;
return (
<Dialog
{...dialogProps}
@@ -31,6 +31,7 @@ export class SecretDetails extends React.Component<Props> {
disposeOnUnmount(this, [
autorun(() => {
const { object: secret } = this.props;
if (secret) {
this.data = secret.data;
this.revealSecret = {};
@@ -41,7 +42,9 @@ export class SecretDetails extends React.Component<Props> {
saveSecret = async () => {
const { object: secret } = this.props;
this.isSaving = true;
try {
await secretsStore.update(secret, { ...secret, data: this.data });
Notifications.ok(<Trans>Secret successfully updated.</Trans>);
@@ -57,7 +60,9 @@ export class SecretDetails extends React.Component<Props> {
render() {
const { object: secret } = this.props;
if (!secret) return null;
return (
<div className="SecretDetails">
<KubeObjectMeta object={secret}/>
@@ -71,12 +76,14 @@ export class SecretDetails extends React.Component<Props> {
Object.entries(this.data).map(([name, value]) => {
const revealSecret = this.revealSecret[name];
let decodedVal = "";
try {
decodedVal = base64.decode(value);
} catch {
decodedVal = "";
}
value = revealSecret ? decodedVal : value;
return (
<div key={name} className="data">
<div className="name">{name}</div>
@@ -15,6 +15,7 @@ export class Config extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
const query = namespaceStore.getContextParams();
const routes: TabLayoutRoute[] = [];
if (isAllowedResource("configmaps")) {
routes.push({
title: <Trans>ConfigMaps</Trans>,
@@ -23,6 +24,7 @@ export class Config extends React.Component {
routePath: configMapsRoute.path.toString(),
});
}
if (isAllowedResource("secrets")) {
routes.push({
title: <Trans>Secrets</Trans>,
@@ -31,6 +33,7 @@ export class Config extends React.Component {
routePath: secretsRoute.path.toString(),
});
}
if (isAllowedResource("resourcequotas")) {
routes.push({
title: <Trans>Resource Quotas</Trans>,
@@ -39,6 +42,7 @@ export class Config extends React.Component {
routePath: resourceQuotaRoute.path.toString(),
});
}
if (isAllowedResource("horizontalpodautoscalers")) {
routes.push({
title: <Trans>HPA</Trans>,
@@ -47,6 +51,7 @@ export class Config extends React.Component {
routePath: hpaRoute.path.toString(),
});
}
if (isAllowedResource("poddisruptionbudgets")) {
routes.push({
title: <Trans>Pod Disruption Budgets</Trans>,
@@ -55,6 +60,7 @@ export class Config extends React.Component {
routePath: pdbRoute.path.toString(),
});
}
return routes;
}
@@ -22,10 +22,12 @@ interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> {
export class CRDDetails extends React.Component<Props> {
render() {
const { object: crd } = this.props;
if (!crd) return null;
const { plural, singular, kind, listKind } = crd.getNames();
const printerColumns = crd.getPrinterColumns();
const validation = crd.getValidation();
return (
<div className="CRDDetails">
<KubeObjectMeta object={crd}/>
@@ -60,6 +62,7 @@ export class CRDDetails extends React.Component<Props> {
{
crd.getConditions().map(condition => {
const { type, message, lastTransitionTime, status } = condition;
return (
<Badge
key={type}
@@ -103,6 +106,7 @@ export class CRDDetails extends React.Component<Props> {
{
printerColumns.map((column, index) => {
const { name, type, jsonPath } = column;
return (
<TableRow key={index}>
<TableCell className="name">{name}</TableCell>
@@ -30,6 +30,7 @@ export class CrdList extends React.Component {
onGroupChange(group: string) {
const groups = [...this.groups];
const index = groups.findIndex(item => item == group);
if (index !== -1) groups.splice(index, 1);
else groups.push(group);
setQueryParams({ groups });
@@ -43,6 +44,7 @@ export class CrdList extends React.Component {
[sortBy.version]: (crd: CustomResourceDefinition) => crd.getVersion(),
[sortBy.scope]: (crd: CustomResourceDefinition) => crd.getScope(),
};
return (
<KubeObjectListLayout
className="CrdList"
@@ -58,8 +60,10 @@ export class CrdList extends React.Component {
renderHeaderTitle={<Trans>Custom Resources</Trans>}
customizeHeader={() => {
let placeholder = <Trans>All groups</Trans>;
if (selectedGroups.length == 1) placeholder = <><Trans>Group</Trans>: {selectedGroups[0]}</>;
if (selectedGroups.length >= 2) placeholder = <><Trans>Groups</Trans>: {selectedGroups.join(", ")}</>;
return {
// todo: move to global filters
filters: (
@@ -71,6 +75,7 @@ export class CrdList extends React.Component {
controlShouldRenderValue={false}
formatOptionLabel={({ value: group }: SelectOption) => {
const isSelected = selectedGroups.includes(group);
return (
<div className="flex gaps align-center">
<Icon small material="folder"/>
@@ -53,6 +53,7 @@ export class CrdResourceDetails extends React.Component<Props> {
renderStatus(crd: CustomResourceDefinition, columns: AdditionalPrinterColumnsV1[]) {
const showStatus = !columns.find(column => column.name == "Status") && crd.status?.conditions;
if (!showStatus) {
return null;
}
@@ -77,6 +78,7 @@ export class CrdResourceDetails extends React.Component<Props> {
render() {
const { props: { object }, crd } = this;
if (!object || !crd) {
return null;
}
@@ -28,6 +28,7 @@ export class CrdResources extends React.Component<Props> {
disposeOnUnmount(this, [
autorun(() => {
const { store } = this;
if (store && !store.isLoading && !store.isLoaded) {
store.loadAll();
}
@@ -37,16 +38,19 @@ export class CrdResources extends React.Component<Props> {
@computed get crd() {
const { group, name } = this.props.match.params;
return crdStore.getByGroup(group, name);
}
@computed get store() {
if (!this.crd) return null;
return apiManager.getStore(this.crd.getResourceApiBase());
}
render() {
const { crd, store } = this;
if (!crd) return null;
const isNamespaced = crd.isNamespaced();
const extraColumns = crd.getPrinterColumns(false); // Cols with priority bigger than 0 are shown in details
@@ -55,6 +59,7 @@ export class CrdResources extends React.Component<Props> {
[sortBy.namespace]: (item: KubeObject) => item.getNs(),
[sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp,
};
extraColumns.forEach(column => {
sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.value(item, column.jsonPath.slice(1));
});
@@ -74,6 +79,7 @@ export class CrdResources extends React.Component<Props> {
isNamespaced && { title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
...extraColumns.map(column => {
const { name } = column;
return {
title: name,
className: name.toLowerCase(),
@@ -38,17 +38,22 @@ export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
@computed get groups() {
const groups: Record<string, CustomResourceDefinition[]> = {};
return this.items.reduce((groups, crd) => {
const group = crd.getGroup();
if (!groups[group]) groups[group] = [];
groups[group].push(crd);
return groups;
}, groups);
}
getByGroup(group: string, pluralName: string) {
const crdInGroup = this.groups[group];
if (!crdInGroup) return null;
return crdInGroup.find(crd => crd.getPluralName() === pluralName);
}
@@ -21,9 +21,11 @@ interface Props extends KubeObjectDetailsProps<KubeEvent> {
export class EventDetails extends React.Component<Props> {
render() {
const { object: event } = this.props;
if (!event) return;
const { message, reason, count, type, involvedObject } = event;
const { kind, name, namespace, fieldPath } = involvedObject;
return (
<div className="EventDetails">
<KubeObjectMeta object={event}/>
@@ -28,6 +28,7 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
if(obj.kind == "Node") {
return obj.getName() == evt.involvedObject.uid && evt.involvedObject.kind == "Node";
}
return obj.getId() == evt.involvedObject.uid;
});
}
@@ -38,12 +39,16 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
const eventsWithError = Object.values(groupsByInvolvedObject).map(events => {
const recent = events[0];
const { kind, uid } = recent.involvedObject;
if (kind == Pod.kind) { // Wipe out running pods
const pod = podsStore.items.find(pod => pod.getId() == uid);
if (!pod || (!pod.hasIssues() && pod.spec.priority < 500000)) return;
}
return recent;
});
return compact(eventsWithError);
}
}
@@ -88,6 +88,7 @@ export class Events extends React.Component<Props> {
const tooltipId = `message-${event.getId()}`;
const isWarning = type === "Warning";
const detailsUrl = getDetailsUrl(lookupApiLink(involvedObject, event));
return [
{
className: { warning: isWarning },
@@ -114,9 +115,11 @@ export class Events extends React.Component<Props> {
]}
/>
);
if (compact) {
return events;
}
return (
<TabLayout>
{events}
@@ -21,6 +21,7 @@ export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
render() {
const { object } = this.props;
const events = eventStore.getEventsByObject(object);
if (!events.length) {
return (
<DrawerTitle className="flex gaps align-center">
@@ -28,6 +29,7 @@ export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
</DrawerTitle>
);
}
return (
<div>
<DrawerTitle className="flex gaps align-center">
@@ -36,6 +38,7 @@ export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
<div className="KubeEventDetails">
{events.map(evt => {
const { message, count, lastTimestamp, involvedObject } = evt;
return (
<div className="event" key={evt.getId()}>
<div className={cssNames("title", { warning: evt.isWarning() })}>
@@ -24,11 +24,14 @@ export class KubeEventIcon extends React.Component<Props> {
const { object, showWarningsOnly, filterEvents } = this.props;
const events = eventStore.getEventsByObject(object);
let warnings = events.filter(evt => evt.isWarning());
if (filterEvents) warnings = filterEvents(warnings);
if (!events.length || (showWarningsOnly && !warnings.length)) {
return null;
}
const event = [...warnings, ...events][0]; // get latest event
return (
<Icon
material="warning"
@@ -106,6 +106,7 @@ export class Extensions extends React.Component {
return Array.from(extensionLoader.userExtensions.values()).filter(ext => {
const { name, description } = ext.manifest;
return [
name.toLowerCase().includes(searchText),
description?.toLowerCase().includes(searchText),
@@ -149,6 +150,7 @@ export class Extensions extends React.Component {
installFromUrlOrPath = async () => {
const { installPath } = this;
if (!installPath) return;
const fileName = path.basename(installPath);
@@ -158,6 +160,7 @@ export class Extensions extends React.Component {
if (InputValidators.isUrl.validate(installPath)) {
const { promise: filePromise } = downloadFile({ url: installPath, timeout: 60000 /*1m*/ });
const data = await filePromise;
this.requestInstall({ fileName, data });
}
// otherwise installing from system path
@@ -173,6 +176,7 @@ export class Extensions extends React.Component {
installOnDrop = (files: File[]) => {
logger.info("Install from D&D");
return this.requestInstall(
files.map(file => ({
fileName: path.basename(file.path),
@@ -190,8 +194,10 @@ export class Extensions extends React.Component {
.map(async request => {
try {
const data = await fse.readFile(request.filePath);
request.data = data;
preloadedRequests.push(request);
return request;
} catch(error) {
if (showError) {
@@ -209,6 +215,7 @@ export class Extensions extends React.Component {
// tarball from npm contains single root folder "package/*"
const firstFile = tarFiles[0];
if (!firstFile) {
throw new Error(`invalid extension bundle, ${manifestFilename} not found`);
}
@@ -230,6 +237,7 @@ export class Extensions extends React.Component {
if (!manifest.lens && !manifest.renderer) {
throw new Error(`${manifestFilename} must specify "main" and/or "renderer" fields`);
}
return manifest;
}
@@ -241,6 +249,7 @@ export class Extensions extends React.Component {
requests.forEach(req => {
const tempFile = this.getExtensionPackageTemp(req.fileName);
fse.writeFileSync(tempFile, req.data);
});
@@ -248,6 +257,7 @@ export class Extensions extends React.Component {
await Promise.all(
requests.map(async req => {
const tempFile = this.getExtensionPackageTemp(req.fileName);
try {
const manifest = await this.validatePackage(tempFile);
@@ -270,6 +280,7 @@ export class Extensions extends React.Component {
}
})
);
return validatedRequests;
}
@@ -342,6 +353,7 @@ export class Extensions extends React.Component {
Notifications.error(
<p>Installing extension <b>{displayName}</b> has failed: <em>{error}</em></p>
);
// Remove install state on install failure
if (this.extensionState.get(extensionId)?.state === "installing") {
this.extensionState.delete(extensionId);
@@ -378,6 +390,7 @@ export class Extensions extends React.Component {
Notifications.error(
<p>Uninstalling extension <b>{displayName}</b> has failed: <em>{error?.message ?? ""}</em></p>
);
// Remove uninstall state on uninstall failure
if (this.extensionState.get(extension.id)?.state === "uninstalling") {
this.extensionState.delete(extension.id);
@@ -14,6 +14,7 @@ export class LandingPage extends React.Component {
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
const noClustersInScope = !clusters.length;
const showStartupHint = this.showHint && noClustersInScope;
return (
<div className="LandingPage flex">
{showStartupHint && (
@@ -38,6 +38,7 @@ export class AddNamespaceDialog extends React.Component<Props> {
addNamespace = async () => {
const { namespace } = this;
const { onSuccess, onError } = this.props;
try {
await namespaceStore.create({ name: namespace }).then(onSuccess);
this.close();
@@ -51,6 +52,7 @@ export class AddNamespaceDialog extends React.Component<Props> {
const { ...dialogProps } = this.props;
const { namespace } = this;
const header = <h5><Trans>Create Namespace</Trans></h5>;
return (
<Dialog
{...dialogProps}
@@ -22,6 +22,7 @@ interface Props extends KubeObjectDetailsProps<Namespace> {
export class NamespaceDetails extends React.Component<Props> {
@computed get quotas() {
const namespace = this.props.object.getName();
return resourceQuotaStore.getAllByNs(namespace);
}
@@ -31,8 +32,10 @@ export class NamespaceDetails extends React.Component<Props> {
render() {
const { object: namespace } = this.props;
if (!namespace) return;
const status = namespace.getStatus();
return (
<div className="NamespaceDetails">
<KubeObjectMeta object={namespace}/>
@@ -46,16 +46,20 @@ export class NamespaceSelect extends React.Component<Props> {
@computed get options(): SelectOption[] {
const { customizeOptions, showClusterOption, clusterOptionLabel } = this.props;
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() }));
options = customizeOptions ? customizeOptions(options) : options;
if (showClusterOption) {
options.unshift({ value: null, label: clusterOptionLabel });
}
return options;
}
formatOptionLabel = (option: SelectOption) => {
const { showIcons } = this.props;
const { value, label } = option;
return label || (
<>
{showIcons && <Icon small material="layers" />}
@@ -66,6 +70,7 @@ export class NamespaceSelect extends React.Component<Props> {
render() {
const { className, showIcons, showClusterOption, clusterOptionLabel, customizeOptions, ...selectProps } = this.props;
return (
<Select
className={cssNames("NamespaceSelect", className)}
@@ -83,8 +88,10 @@ export class NamespaceSelectFilter extends React.Component {
render() {
const { contextNs, hasContext, toggleContext } = namespaceStore;
let placeholder = <Trans>All namespaces</Trans>;
if (contextNs.length == 1) placeholder = <Trans>Namespace: {contextNs[0]}</Trans>;
if (contextNs.length >= 2) placeholder = <Trans>Namespaces: {contextNs.join(", ")}</Trans>;
return (
<NamespaceSelect
placeholder={placeholder}
@@ -95,6 +102,7 @@ export class NamespaceSelectFilter extends React.Component {
onChange={([{ value }]: SelectOption[]) => toggleContext(value)}
formatOptionLabel={({ value: namespace }: SelectOption) => {
const isSelected = hasContext(namespace);
return (
<div className="flex gaps align-center">
<FilterIcon type={FilterType.NAMESPACE} />
@@ -15,6 +15,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
get initNamespaces() {
const fromUrl = navigation.searchParams.getAsArray("namespaces");
return fromUrl.length ? fromUrl : this.storage.get();
}
@@ -23,6 +24,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
// restore context namespaces
const { initNamespaces: namespaces } = this;
this.setContext(namespaces);
this.updateUrl(namespaces);
@@ -46,8 +48,10 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
protected async loadItems(namespaces?: string[]) {
if (!isAllowedResource("namespaces")) {
if (namespaces) return namespaces.map(this.getDummyNamespace);
return [];
}
if (namespaces) {
return Promise.all(namespaces.map(name => this.api.get({ name })));
} else {
@@ -74,6 +78,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
hasContext(namespace: string | string[]) {
const context = Array.isArray(namespace) ? namespace : [namespace];
return context.every(namespace => this.contextNs.includes(namespace));
}
@@ -18,7 +18,9 @@ interface Props extends KubeObjectDetailsProps<Endpoint> {
export class EndpointDetails extends React.Component<Props> {
render() {
const { object: endpoint } = this.props;
if (!endpoint) return;
return (
<div className="EndpointDetails">
<KubeObjectMeta object={endpoint}/>
@@ -21,6 +21,7 @@ export class EndpointSubsetList extends React.Component<Props> {
getAddressTableRow(ip: string) {
const { subset } = this.props;
const address = subset.getAddresses().find(address => address.getId() == ip);
return this.renderAddressTableRow(address);
}
@@ -28,6 +29,7 @@ export class EndpointSubsetList extends React.Component<Props> {
getNotReadyAddressTableRow(ip: string) {
const { subset} = this.props;
const address = subset.getNotReadyAddresses().find(address => address.getId() == ip);
return this.renderAddressTableRow(address);
}
@@ -60,6 +62,7 @@ export class EndpointSubsetList extends React.Component<Props> {
@autobind()
renderAddressTableRow(address: EndpointAddress) {
const { endpoint } = this.props;
return (
<TableRow
key={address.getId()}
@@ -51,6 +51,7 @@ export class Endpoints extends React.Component<Props> {
tableProps={{
customRowHeights: (item: Endpoint, lineHeight, paddings) => {
const lines = item.getEndpointSubsets().length || 1;
return lines * lineHeight + paddings;
}
}}
@@ -79,6 +79,7 @@ export const IngressCharts = observer(() => {
const { label, data } = datasets[datasetIndex];
const value = data[index] as ChartPoint;
const chartTooltipSec = _i18n._(t`sec`);
return `${label}: ${parseFloat(value.y as string).toFixed(3)} ${chartTooltipSec}`;
}
}
@@ -32,7 +32,9 @@ export class IngressDetails extends React.Component<Props> {
renderPaths(ingress: Ingress) {
const { spec: { rules } } = ingress;
if (!rules || !rules.length) return null;
return rules.map((rule, index) => {
return (
<div className="rules" key={index}>
@@ -71,6 +73,7 @@ export class IngressDetails extends React.Component<Props> {
renderIngressPoints(ingressPoints: ILoadBalancerIngress[]) {
if (!ingressPoints || ingressPoints.length === 0) return null;
return (
<div>
<Table className="ingress-points">
@@ -93,6 +96,7 @@ export class IngressDetails extends React.Component<Props> {
render() {
const { object: ingress } = this.props;
if (!ingress) {
return null;
}
@@ -54,6 +54,7 @@ export class Ingresses extends React.Component<Props> {
tableProps={{
customRowHeights: (item: Ingress, lineHeight, paddings) => {
const lines = item.getRoutes().length || 1;
return lines * lineHeight + paddings;
}
}}
@@ -21,16 +21,21 @@ interface Props extends KubeObjectDetailsProps<NetworkPolicy> {
export class NetworkPolicyDetails extends React.Component<Props> {
renderIngressFrom(ingress: IPolicyIngress) {
const { from } = ingress;
if (!from) return null;
return (
<>
<SubTitle title={<Trans>From</Trans>}/>
{from.map(item =>
Object.keys(item).map(key => {
const data = get(item, key);
if (key === "ipBlock") {
const { cidr, except } = data as IPolicyIpBlock;
if (!cidr) return;
return (
<DrawerItem name={key} key={key}>
cidr: {cidr}, {" "}
@@ -41,6 +46,7 @@ export class NetworkPolicyDetails extends React.Component<Props> {
);
}
const selector: IPolicySelector = data;
if (selector.matchLabels) {
return (
<DrawerItem name={key} key={key}>
@@ -64,15 +70,20 @@ export class NetworkPolicyDetails extends React.Component<Props> {
renderEgressTo(egress: IPolicyEgress) {
const { to } = egress;
if (!to) return null;
return (
<>
<SubTitle title={<Trans>To</Trans>}/>
{to.map(item => {
const { ipBlock } = item;
if (!ipBlock) return;
const { cidr, except } = ipBlock;
if (!cidr) return;
return (
<DrawerItem name="ipBlock" key={cidr}>
cidr: {cidr}, {" "}
@@ -88,11 +99,13 @@ export class NetworkPolicyDetails extends React.Component<Props> {
render() {
const { object: policy } = this.props;
if (!policy) {
return null;
}
const { ingress, egress } = policy.spec;
const selector = policy.getMatchLabels();
return (
<div className="NetworkPolicyDetails">
<KubeObjectMeta object={policy}/>
@@ -109,6 +122,7 @@ export class NetworkPolicyDetails extends React.Component<Props> {
<DrawerTitle title={_i18n._(t`Ingress`)}/>
{ingress.map((ingress, i) => {
const { ports } = ingress;
return (
<Fragment key={i}>
<DrawerItem name={<Trans>Ports</Trans>}>
@@ -126,6 +140,7 @@ export class NetworkPolicyDetails extends React.Component<Props> {
<DrawerTitle title={<Trans>Egress</Trans>}/>
{egress.map((egress, i) => {
const { ports } = egress;
return (
<Fragment key={i}>
<DrawerItem name={<Trans>Ports</Trans>}>
@@ -16,12 +16,15 @@ interface Props {
export class ServiceDetailsEndpoint extends React.Component<Props> {
render() {
const { endpoint } = this.props;
if (!endpoint && !endpointStore.isLoaded) return (
<div className="PodDetailsList flex justify-center"><Spinner/></div>
);
if (!endpoint) {
return null;
}
return (
<div className="EndpointList flex column">
<Table
@@ -29,9 +29,11 @@ export class ServiceDetails extends React.Component<Props> {
render() {
const { object: service } = this.props;
if (!service) return;
const { spec } = service;
const endpoint = endpointStore.getByName(service.getName(), service.getNs());
return (
<div className="ServicesDetails">
<KubeObjectMeta object={service}/>
@@ -22,7 +22,9 @@ export class ServicePortComponent extends React.Component<Props> {
async portForward() {
const { service, port } = this.props;
this.waiting = true;
try {
await apiBase.post(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}`, {});
} catch(error) {
@@ -34,6 +36,7 @@ export class ServicePortComponent extends React.Component<Props> {
render() {
const { port } = this.props;
return (
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
<span title={_i18n._(t`Open in a browser`)} onClick={() => this.portForward() }>
@@ -16,6 +16,7 @@ export class Network extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
const query = namespaceStore.getContextParams();
const routes: TabLayoutRoute[] = [];
if (isAllowedResource("services")) {
routes.push({
title: <Trans>Services</Trans>,
@@ -24,6 +25,7 @@ export class Network extends React.Component {
routePath: servicesRoute.path.toString(),
});
}
if (isAllowedResource("endpoints")) {
routes.push({
title: <Trans>Endpoints</Trans>,
@@ -32,6 +34,7 @@ export class Network extends React.Component {
routePath: endpointRoute.path.toString(),
});
}
if (isAllowedResource("ingresses")) {
routes.push({
title: <Trans>Ingresses</Trans>,
@@ -40,6 +43,7 @@ export class Network extends React.Component {
routePath: ingressRoute.path.toString(),
});
}
if (isAllowedResource("networkpolicies")) {
routes.push({
title: <Trans>Network Policies</Trans>,
@@ -48,6 +52,7 @@ export class Network extends React.Component {
routePath: networkPoliciesRoute.path.toString(),
});
}
return routes;
}
@@ -134,6 +134,7 @@ export const NodeCharts = observer(() => {
label: ({ datasetIndex, index }, { datasets }) => {
const { label, data } = datasets[datasetIndex];
const value = data[index] as ChartPoint;
return `${label}: ${value.y}`;
}
}
@@ -41,6 +41,7 @@ export class NodeDetails extends React.Component<Props> {
render() {
const { object: node } = this.props;
if (!node) return;
const { status } = node;
const { nodeInfo, addresses, capacity, allocatable } = status;
@@ -54,6 +55,7 @@ export class NodeDetails extends React.Component<Props> {
<Trans key="disk">Disk</Trans>,
<Trans key="pods">Pods</Trans>,
];
return (
<div className="NodeDetails">
{podsStore.isLoaded && (
@@ -121,6 +123,7 @@ export class NodeDetails extends React.Component<Props> {
{
conditions.map(condition => {
const { type } = condition;
return (
<Badge
key={type}
@@ -16,6 +16,7 @@ export class NodesStore extends KubeObjectStore<Node> {
@action
async loadUsageMetrics() {
this.metricsLoading = true;
try {
this.metrics = await nodesApi.getMetrics();
this.metricsLoaded = true;
@@ -42,6 +43,7 @@ export class NodesStore extends KubeObjectStore<Node> {
return;
}
const nodeName = node.getName();
return metricNames.map(metricName => {
try {
const metric = this.metrics[metricName];
@@ -52,6 +54,7 @@ export class NodesStore extends KubeObjectStore<Node> {
result.metric.kubernetes_node,
].includes(nodeName);
});
return result ? parseFloat(result.values.slice(-1)[0][1]) : 0;
} catch (e) {
return 0;
Loaded 100 of 401 files, more files were not shown because too many files have changed in this diff. Show more