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

cleanup Lens repo with tighter linting

Signed-off-by: Sebastian Malton <smalton@mirantis.com>
This commit is contained in:
Sebastian Malton 2020-07-09 16:58:39 -04:00
parent e468105143
commit b1ff34879a
470 changed files with 8447 additions and 7443 deletions

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
dashboard/node_modules

View File

@ -1,76 +0,0 @@
module.exports = {
overrides: [
{
files: [
"src/renderer/**/*.js",
"build/**/*.js",
"src/renderer/**/*.vue"
],
extends: [
'eslint:recommended',
'plugin:vue/recommended'
],
env: {
node: true
},
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
rules: {
"indent": ["error", 2],
"no-unused-vars": "off",
"vue/order-in-components": "off",
"vue/attributes-order": "off",
"vue/max-attributes-per-line": "off"
}
},
{
files: [
"build/*.ts",
"src/**/*.ts",
"spec/**/*.ts"
],
parser: "@typescript-eslint/parser",
extends: [
'plugin:@typescript-eslint/recommended',
],
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
rules: {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"indent": ["error", 2]
},
},
{
files: [
"dashboard/**/*.ts",
"dashboard/**/*.tsx",
],
parser: "@typescript-eslint/parser",
extends: [
'plugin:@typescript-eslint/recommended',
],
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
jsx: true,
},
rules: {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"indent": ["error", 2]
},
}
]
};

96
.eslintrc.json Normal file
View File

@ -0,0 +1,96 @@
{
"settings": {
"react": {
"version": "16.11"
}
},
"overrides": [
{
"files": [
"src/renderer/**/*.js",
"build/**/*.js",
"src/renderer/**/*.vue"
],
"extends": [
"eslint:recommended",
"plugin:vue/recommended"
],
"env": {
"node": true
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
"indent": ["error", 2],
"no-unused-vars": "off",
"vue/order-in-components": "off",
"vue/attributes-order": "off",
"vue/max-attributes-per-line": "off"
}
},
{
"files": [
"build/*.ts",
"src/**/*.ts",
"spec/**/*.ts",
"dashboard/**/*.ts",
"dashboard/**/*.tsx"
],
"excludedFiles": [
"**/node_modules/**/*"
],
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended"
],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"project": "./tsconfig.json",
"jsx": true
},
"rules": {
"@typescript-eslint/no-unused-vars": [
"warn",
{
"varsIgnorePattern": "^_",
"argsIgnorePattern": "^_"
}
],
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/array-type": [
"error",
{
"default": "array",
"readyonly": "array"
}
],
"@typescript-eslint/no-explicit-any": "off",
"indent": ["error", 2],
"@typescript-eslint/no-use-before-define": "error",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/consistent-type-assertions": [
"error",
{
"assertionStyle": "as",
"objectLiteralTypeAssertions": "never"
}
],
"@typescript-eslint/consistent-type-definitions": [
"error",
"interface"
],
"react/prop-types": [ 0 ],
"brace-style": ["error", "1tbs", { "allowSingleLine": false }],
"curly": "error",
"space-before-blocks": "error",
"semi": "error"
}
}
]
}

View File

@ -27,7 +27,10 @@ integration-win:
yarn integration yarn integration
lint: lint:
yarn lint yarn run eslint --ext tsx,ts,vue --max-warnings=0 src/ dashboard/ build/
lint-fix:
yarn run eslint --ext tsx,ts,vue --max-warnings=0 src/ dashboard/ build/ --fix
test-app: test-app:
yarn test yarn test

View File

@ -1,3 +1,3 @@
import { helmCli } from "../src/main/helm-cli" import { helmCli } from "../src/main/helm-cli";
helmCli.ensureBinary() helmCli.ensureBinary();

View File

@ -1,9 +1,9 @@
import * as request from "request" import * as request from "request";
import * as fs from "fs" import * as fs from "fs";
import { ensureDir, pathExists } from "fs-extra" import { ensureDir, pathExists } from "fs-extra";
import * as md5File from "md5-file" import * as md5File from "md5-file";
import * as requestPromise from "request-promise-native" import * as requestPromise from "request-promise-native";
import * as path from "path" import * as path from "path";
class KubectlDownloader { class KubectlDownloader {
public kubectlVersion: string public kubectlVersion: string
@ -13,92 +13,94 @@ class KubectlDownloader {
constructor(clusterVersion: string, platform: string, arch: string, target: string) { constructor(clusterVersion: string, platform: string, arch: string, target: string) {
this.kubectlVersion = clusterVersion; this.kubectlVersion = clusterVersion;
const binaryName = platform === "windows" ? "kubectl.exe" : "kubectl" const binaryName = platform === "windows" ? "kubectl.exe" : "kubectl";
this.url = `https://storage.googleapis.com/kubernetes-release/release/v${this.kubectlVersion}/bin/${platform}/${arch}/${binaryName}`; this.url = `https://storage.googleapis.com/kubernetes-release/release/v${this.kubectlVersion}/bin/${platform}/${arch}/${binaryName}`;
this.dirname = path.dirname(target); this.dirname = path.dirname(target);
this.path = target; this.path = target;
} }
protected async urlEtag() { protected async urlEtag(): Promise<string> {
const response = await requestPromise({ const response = await requestPromise({
method: "HEAD", method: "HEAD",
uri: this.url, uri: this.url,
resolveWithFullResponse: true resolveWithFullResponse: true
}).catch((error) => { console.log(error) }) }).catch((error) => {
console.log(error);
});
if (response.headers["etag"]) { if (response.headers["etag"]) {
return response.headers["etag"].replace(/"/g, "") return response.headers["etag"].replace(/"/g, "");
} }
return "" return "";
} }
public async checkBinary() { public async checkBinary(): Promise<boolean> {
const exists = await pathExists(this.path) const exists = await pathExists(this.path);
if (exists) { if (exists) {
const hash = md5File.sync(this.path) const hash = md5File.sync(this.path);
const etag = await this.urlEtag() const etag = await this.urlEtag();
if(hash == etag) { if(hash == etag) {
console.log("Kubectl md5sum matches the remote etag") console.log("Kubectl md5sum matches the remote etag");
return true return true;
} }
console.log("Kubectl md5sum " + hash + " does not match the remote etag " + etag + ", unlinking and downloading again") console.log("Kubectl md5sum " + hash + " does not match the remote etag " + etag + ", unlinking and downloading again");
await fs.promises.unlink(this.path) await fs.promises.unlink(this.path);
} }
return false return false;
} }
public async downloadKubectl() { public async downloadKubectl(): Promise<void> {
const exists = await this.checkBinary(); const exists = await this.checkBinary();
if(exists) { if(exists) {
console.log("Already exists and is valid") console.log("Already exists and is valid");
return return;
} }
await ensureDir(path.dirname(this.path), 0o755) await ensureDir(path.dirname(this.path), 0o755);
const file = fs.createWriteStream(this.path) const file = fs.createWriteStream(this.path);
console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`) console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
const requestOpts: request.UriOptions & request.CoreOptions = { const requestOpts: request.UriOptions & request.CoreOptions = {
uri: this.url, uri: this.url,
gzip: true gzip: true
} };
const stream = request(requestOpts) const stream = request(requestOpts);
stream.on("complete", () => { stream.on("complete", () => {
console.log("kubectl binary download finished") console.log("kubectl binary download finished");
file.end(() => {}) file.end(() => {});
}) });
stream.on("error", (error) => { stream.on("error", (error) => {
console.log(error) console.log(error);
fs.unlink(this.path, () => {}) fs.unlink(this.path, () => {});
throw(error) throw(error);
}) });
return new Promise((resolve, reject) => { return new Promise((resolve, _reject) => {
file.on("close", () => { file.on("close", () => {
console.log("kubectl binary download closed") console.log("kubectl binary download closed");
fs.chmod(this.path, 0o755, () => {}) fs.chmod(this.path, 0o755, () => {});
resolve() resolve();
}) });
stream.pipe(file) stream.pipe(file);
}) });
} }
} }
const downloadVersion: string = require("../package.json").config.bundledKubectlVersion const downloadVersion: string = require("../package.json").config.bundledKubectlVersion;
const baseDir = path.join(process.env.INIT_CWD, 'binaries', 'client') const baseDir = path.join(process.env.INIT_CWD, 'binaries', 'client');
const downloads = [ const downloads = [
{ platform: 'linux', arch: 'amd64', target: path.join(baseDir, 'linux', 'x64', 'kubectl') }, { platform: 'linux', arch: 'amd64', target: path.join(baseDir, 'linux', 'x64', 'kubectl') },
{ platform: 'darwin', arch: 'amd64', target: path.join(baseDir, 'darwin', 'x64', 'kubectl') }, { platform: 'darwin', arch: 'amd64', target: path.join(baseDir, 'darwin', 'x64', 'kubectl') },
{ platform: 'windows', arch: 'amd64', target: path.join(baseDir, 'windows', 'x64', 'kubectl.exe') }, { platform: 'windows', arch: 'amd64', target: path.join(baseDir, 'windows', 'x64', 'kubectl.exe') },
{ platform: 'windows', arch: '386', target: path.join(baseDir, 'windows', 'ia32', 'kubectl.exe') } { platform: 'windows', arch: '386', target: path.join(baseDir, 'windows', 'ia32', 'kubectl.exe') }
] ];
downloads.forEach((dlOpts) => { downloads.forEach((dlOpts) => {
console.log(dlOpts) console.log(dlOpts);
const downloader = new KubectlDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target); const downloader = new KubectlDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target);
console.log("Downloading: " + JSON.stringify(dlOpts)); console.log("Downloading: " + JSON.stringify(dlOpts));
downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete"))) downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete")));
}) });

View File

@ -1,8 +1,8 @@
import { KubeApi, IKubeApiLinkBase } from "../kube-api"; import { KubeApi, KubeApiLinkBase } from "../kube-api";
interface ParseAPITest { interface ParseAPITest {
url: string; url: string;
expected: Required<IKubeApiLinkBase>; expected: Required<KubeApiLinkBase>;
} }
const tests: ParseAPITest[] = [ const tests: ParseAPITest[] = [

View File

@ -17,7 +17,7 @@ export class ApiManager {
private stores = observable.map<KubeApi, KubeObjectStore>(); private stores = observable.map<KubeApi, KubeObjectStore>();
private views = observable.map<KubeApi, ApiComponents>(); private views = observable.map<KubeApi, ApiComponents>();
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) { getApi(pathOrCallback: string | ((api: KubeApi) => boolean)): KubeApi<any> {
if (typeof pathOrCallback === "string") { if (typeof pathOrCallback === "string") {
return this.apis.get(pathOrCallback) || this.apis.get(KubeApi.parseApi(pathOrCallback).apiBase); return this.apis.get(pathOrCallback) || this.apis.get(KubeApi.parseApi(pathOrCallback).apiBase);
} }
@ -25,27 +25,32 @@ export class ApiManager {
return Array.from(this.apis.values()).find(pathOrCallback); return Array.from(this.apis.values()).find(pathOrCallback);
} }
registerApi(apiBase: string, api: KubeApi) { registerApi(apiBase: string, api: KubeApi): void {
if (!this.apis.has(apiBase)) { if (!this.apis.has(apiBase)) {
this.apis.set(apiBase, api); this.apis.set(apiBase, api);
} }
} }
protected resolveApi(api: string | KubeApi): KubeApi { protected resolveApi(api: string | KubeApi): KubeApi {
if (typeof api === "string") return this.getApi(api) if (typeof api === "string") {
return this.getApi(api);
}
return api; return api;
} }
unregisterApi(api: string | KubeApi) { unregisterApi(api: string | KubeApi): void {
if (typeof api === "string") this.apis.delete(api); if (typeof api === "string") {
else { this.apis.delete(api);
} else {
const apis = Array.from(this.apis.entries()); const apis = Array.from(this.apis.entries());
const entry = apis.find(entry => entry[1] === api); const entry = apis.find(entry => entry[1] === api);
if (entry) this.unregisterApi(entry[0]); if (entry) {
this.unregisterApi(entry[0]);
}
} }
} }
registerStore(api: KubeApi, store: KubeObjectStore) { registerStore(api: KubeApi, store: KubeObjectStore): void {
this.stores.set(api, store); this.stores.set(api, store);
} }
@ -53,7 +58,7 @@ export class ApiManager {
return this.stores.get(this.resolveApi(api)); return this.stores.get(this.resolveApi(api));
} }
registerViews(api: KubeApi | KubeApi[], views: ApiComponents) { registerViews(api: KubeApi | KubeApi[], views: ApiComponents): void {
if (Array.isArray(api)) { if (Array.isArray(api)) {
api.forEach(api => this.registerViews(api, views)); api.forEach(api => this.registerViews(api, views));
return; return;
@ -66,7 +71,7 @@ export class ApiManager {
} }
getViews(api: string | KubeApi): ApiComponents { getViews(api: string | KubeApi): ApiComponents {
return this.views.get(this.resolveApi(api)) || {} return this.views.get(this.resolveApi(api)) || {};
} }
} }

View File

@ -18,7 +18,7 @@ const cronJob = new CronJob({
suspend: false, suspend: false,
}, },
status: {} status: {}
} as any) } as any);
describe("Check for CronJob schedule never run", () => { describe("Check for CronJob schedule never run", () => {
test("Should be false with normal schedule", () => { test("Should be false with normal schedule", () => {
@ -31,12 +31,12 @@ describe("Check for CronJob schedule never run", () => {
}); });
test("Should be true with date 31 of February", () => { test("Should be true with date 31 of February", () => {
cronJob.spec.schedule = "30 06 31 2 *" cronJob.spec.schedule = "30 06 31 2 *";
expect(cronJob.isNeverRun()).toBeTruthy(); expect(cronJob.isNeverRun()).toBeTruthy();
}); });
test("Should be true with date 32 of July", () => { test("Should be true with date 32 of July", () => {
cronJob.spec.schedule = "0 30 06 32 7 *" cronJob.spec.schedule = "0 30 06 32 7 *";
expect(cronJob.isNeverRun()).toBeTruthy(); expect(cronJob.isNeverRun()).toBeTruthy();
}); });

View File

@ -3,96 +3,23 @@
// API docs: https://docs.cert-manager.io/en/latest/reference/api-docs/index.html // API docs: https://docs.cert-manager.io/en/latest/reference/api-docs/index.html
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { ISecretRef, secretsApi } from "./secret.api"; import { SecretRef, secretsApi } from "./secret.api";
import { getDetailsUrl } from "../../navigation"; import { getDetailsUrl } from "../../navigation";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export class Certificate extends KubeObject { export type IssuerType = "ACME" | "CA" | "SelfSigned" | "Vault" | "Venafi";
static kind = "Certificate"
spec: { export interface IssuerConditionBase {
secretName: string; lastTransitionTime: string; // 2019-06-05T07:10:42Z,
commonName?: string; message: string; // The ACME account was registered with the ACME server,
dnsNames?: string[]; reason: string; // ACMEAccountRegistered,
organization?: string[]; status: string; // True,
ipAddresses?: string[]; type: string; // Ready
duration?: string; }
renewBefore?: string;
isCA?: boolean;
keySize?: number;
keyAlgorithm?: "rsa" | "ecdsa";
issuerRef: {
kind?: string;
name: string;
};
acme?: {
config: {
domains: string[];
http01: {
ingress?: string;
ingressClass?: string;
};
dns01?: {
provider: string;
};
}[];
};
}
status: {
conditions?: {
lastTransitionTime: string; // 2019-06-04T07:35:58Z,
message: string; // Certificate is up to date and has not expired,
reason: string; // Ready,
status: string; // True,
type: string; // Ready
}[];
notAfter: string; // 2019-11-01T05:36:27Z
lastFailureTime?: string;
}
getType(): string { export interface IssuerCondition extends IssuerConditionBase {
const { isCA, acme } = this.spec; tooltip: string;
if (isCA) return "CA" isReady: boolean;
if (acme) return "ACME"
}
getCommonName() {
return this.spec.commonName || ""
}
getIssuerName() {
return this.spec.issuerRef.name;
}
getSecretName() {
return this.spec.secretName;
}
getIssuerDetailsUrl() {
return getDetailsUrl(issuersApi.getUrl({
namespace: this.getNs(),
name: this.getIssuerName(),
}))
}
getSecretDetailsUrl() {
return getDetailsUrl(secretsApi.getUrl({
namespace: this.getNs(),
name: this.getSecretName(),
}))
}
getConditions() {
const { conditions = [] } = this.status;
return conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition;
return {
...condition,
isReady: status === "True",
tooltip: `${message || reason} (${lastTransitionTime})`
}
});
}
} }
export class Issuer extends KubeObject { export class Issuer extends KubeObject {
@ -103,23 +30,23 @@ export class Issuer extends KubeObject {
email: string; email: string;
server: string; server: string;
skipTLSVerify?: boolean; skipTLSVerify?: boolean;
privateKeySecretRef: ISecretRef; privateKeySecretRef: SecretRef;
solvers?: { solvers?: {
dns01?: { dns01?: {
cnameStrategy: string; cnameStrategy: string;
acmedns?: { acmedns?: {
host: string; host: string;
accountSecretRef: ISecretRef; accountSecretRef: SecretRef;
}; };
akamai?: { akamai?: {
accessTokenSecretRef: ISecretRef; accessTokenSecretRef: SecretRef;
clientSecretSecretRef: ISecretRef; clientSecretSecretRef: SecretRef;
clientTokenSecretRef: ISecretRef; clientTokenSecretRef: SecretRef;
serviceConsumerDomain: string; serviceConsumerDomain: string;
}; };
azuredns?: { azuredns?: {
clientID: string; clientID: string;
clientSecretSecretRef: ISecretRef; clientSecretSecretRef: SecretRef;
hostedZoneName: string; hostedZoneName: string;
resourceGroupName: string; resourceGroupName: string;
subscriptionID: string; subscriptionID: string;
@ -127,26 +54,26 @@ export class Issuer extends KubeObject {
}; };
clouddns?: { clouddns?: {
project: string; project: string;
serviceAccountSecretRef: ISecretRef; serviceAccountSecretRef: SecretRef;
}; };
cloudflare?: { cloudflare?: {
email: string; email: string;
apiKeySecretRef: ISecretRef; apiKeySecretRef: SecretRef;
}; };
digitalocean?: { digitalocean?: {
tokenSecretRef: ISecretRef; tokenSecretRef: SecretRef;
}; };
rfc2136?: { rfc2136?: {
nameserver: string; nameserver: string;
tsigAlgorithm: string; tsigAlgorithm: string;
tsigKeyName: string; tsigKeyName: string;
tsigSecretSecretRef: ISecretRef; tsigSecretSecretRef: SecretRef;
}; };
route53?: { route53?: {
accessKeyID: string; accessKeyID: string;
hostedZoneID: string; hostedZoneID: string;
region: string; region: string;
secretAccessKeySecretRef: ISecretRef; secretAccessKeySecretRef: SecretRef;
}; };
webhook?: { webhook?: {
config: object; // arbitrary json config: object; // arbitrary json
@ -180,7 +107,7 @@ export class Issuer extends KubeObject {
appRole: { appRole: {
path: string; path: string;
roleId: string; roleId: string;
secretRef: ISecretRef; secretRef: SecretRef;
}; };
}; };
}; };
@ -188,7 +115,7 @@ export class Issuer extends KubeObject {
venafi?: { venafi?: {
zone: string; zone: string;
cloud?: { cloud?: {
apiTokenSecretRef: ISecretRef; apiTokenSecretRef: SecretRef;
}; };
tpp?: { tpp?: {
url: string; url: string;
@ -204,25 +131,29 @@ export class Issuer extends KubeObject {
acme?: { acme?: {
uri: string; uri: string;
}; };
conditions?: { conditions?: IssuerConditionBase[];
lastTransitionTime: string; // 2019-06-05T07:10:42Z,
message: string; // The ACME account was registered with the ACME server,
reason: string; // ACMEAccountRegistered,
status: string; // True,
type: string; // Ready
}[];
} }
getType() { getType(): IssuerType {
const { acme, ca, selfSigned, vault, venafi } = this.spec; const { acme, ca, selfSigned, vault, venafi } = this.spec;
if (acme) return "ACME" if (acme) {
if (ca) return "CA" return "ACME";
if (selfSigned) return "SelfSigned" }
if (vault) return "Vault" if (ca) {
if (venafi) return "Venafi" return "CA";
}
if (selfSigned) {
return "SelfSigned";
}
if (vault) {
return "Vault";
}
if (venafi) {
return "Venafi";
}
} }
getConditions() { getConditions(): IssuerCondition[] {
const { conditions = [] } = this.status; const { conditions = [] } = this.status;
return conditions.map(condition => { return conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition; const { message, reason, lastTransitionTime, status } = condition;
@ -230,7 +161,113 @@ export class Issuer extends KubeObject {
...condition, ...condition,
isReady: status === "True", isReady: status === "True",
tooltip: `${message || reason} (${lastTransitionTime})`, tooltip: `${message || reason} (${lastTransitionTime})`,
} };
});
}
}
export const issuersApi = new KubeApi({
kind: Issuer.kind,
apiBase: "/apis/cert-manager.io/v1alpha2/issuers",
isNamespaced: true,
objectConstructor: Issuer,
});
export interface CertificateConditionBase {
lastTransitionTime: string; // 2019-06-04T07:35:58Z,
message: string; // Certificate is up to date and has not expired,
reason: string; // Ready,
status: string; // True,
type: string; // Ready
}
export interface CertificateCondition extends CertificateConditionBase {
isReady: boolean;
tooltip: string;
}
export class Certificate extends KubeObject {
static kind = "Certificate"
spec: {
secretName: string;
commonName?: string;
dnsNames?: string[];
organization?: string[];
ipAddresses?: string[];
duration?: string;
renewBefore?: string;
isCA?: boolean;
keySize?: number;
keyAlgorithm?: "rsa" | "ecdsa";
issuerRef: {
kind?: string;
name: string;
};
acme?: {
config: {
domains: string[];
http01: {
ingress?: string;
ingressClass?: string;
};
dns01?: {
provider: string;
};
}[];
};
}
status: {
conditions?: CertificateConditionBase[];
notAfter: string; // 2019-11-01T05:36:27Z
lastFailureTime?: string;
}
getType(): string {
const { isCA, acme } = this.spec;
if (isCA) {
return "CA";
}
if (acme) {
return "ACME";
}
}
getCommonName(): string {
return this.spec.commonName || "";
}
getIssuerName(): string {
return this.spec.issuerRef.name;
}
getSecretName(): string {
return this.spec.secretName;
}
getIssuerDetailsUrl(): string {
return getDetailsUrl(issuersApi.getUrl({
namespace: this.getNs(),
name: this.getIssuerName(),
}));
}
getSecretDetailsUrl(): string {
return getDetailsUrl(secretsApi.getUrl({
namespace: this.getNs(),
name: this.getSecretName(),
}));
}
getConditions(): CertificateCondition[] {
const { conditions = [] } = this.status;
return conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition;
return {
...condition,
isReady: status === "True",
tooltip: `${message || reason} (${lastTransitionTime})`
};
}); });
} }
} }
@ -246,13 +283,6 @@ export const certificatesApi = new KubeApi({
objectConstructor: Certificate, objectConstructor: Certificate,
}); });
export const issuersApi = new KubeApi({
kind: Issuer.kind,
apiBase: "/apis/cert-manager.io/v1alpha2/issuers",
isNamespaced: true,
objectConstructor: Issuer,
});
export const clusterIssuersApi = new KubeApi({ export const clusterIssuersApi = new KubeApi({
kind: ClusterIssuer.kind, kind: ClusterIssuer.kind,
apiBase: "/apis/cert-manager.io/v1alpha2/clusterissuers", apiBase: "/apis/cert-manager.io/v1alpha2/clusterissuers",

View File

@ -1,11 +1,11 @@
import { IMetrics, IMetricsReqParams, metricsApi } from "./metrics.api"; import { Metrics, MetricsReqParams, metricsApi } from "./metrics.api";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export class ClusterApi extends KubeApi<Cluster> { export class ClusterApi extends KubeApi<Cluster> {
async getMetrics(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> { async getMetrics(nodeNames: string[], params?: MetricsReqParams): Promise<ClusterMetrics> {
const nodes = nodeNames.join("|"); const nodes = nodeNames.join("|");
const opts = { category: "cluster", nodes: nodes } const opts = { category: "cluster", nodes: nodes };
return metricsApi.getMetrics({ return metricsApi.getMetrics({
memoryUsage: opts, memoryUsage: opts,
@ -31,7 +31,7 @@ export enum ClusterStatus {
ERROR = "Error" ERROR = "Error"
} }
export interface IClusterMetrics<T = IMetrics> { export interface ClusterMetrics<T = Metrics> {
[metric: string]: T; [metric: string]: T;
memoryUsage: T; memoryUsage: T;
memoryRequests: T; memoryRequests: T;
@ -82,10 +82,16 @@ export class Cluster extends KubeObject {
errorReason?: string; errorReason?: string;
} }
getStatus() { getStatus(): ClusterStatus {
if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING; if (this.metadata.deletionTimestamp) {
if (!this.status || !this.status) return ClusterStatus.CREATING; return ClusterStatus.REMOVING;
if (this.status.errorMessage) return ClusterStatus.ERROR; }
if (!this.status || !this.status) {
return ClusterStatus.CREATING;
}
if (this.status.errorMessage) {
return ClusterStatus.ERROR;
}
return ClusterStatus.ACTIVE; return ClusterStatus.ACTIVE;
} }
} }

View File

@ -1,7 +1,7 @@
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export interface IComponentStatusCondition { export interface ComponentStatusCondition {
type: string; type: string;
status: string; status: string;
message: string; message: string;
@ -10,9 +10,9 @@ export interface IComponentStatusCondition {
export class ComponentStatus extends KubeObject { export class ComponentStatus extends KubeObject {
static kind = "ComponentStatus" static kind = "ComponentStatus"
conditions: IComponentStatusCondition[] conditions: ComponentStatusCondition[]
getTruthyConditions() { getTruthyConditions(): ComponentStatusCondition[] {
return this.conditions.filter(c => c.status === "True"); return this.conditions.filter(c => c.status === "True");
} }
} }

View File

@ -1,9 +1,10 @@
// App configuration api // App configuration api
import { apiBase } from "../index"; import { apiBase } from "../index";
import { IConfig } from "../../../server/common/config"; import { IConfig } from "../../../server/common/config";
import { CancelablePromise } from "client/utils/cancelableFetch";
export const configApi = { export const configApi = {
getConfig() { getConfig(): CancelablePromise<IConfig> {
return apiBase.get<IConfig>("/config") return apiBase.get<IConfig>("/config");
}, },
}; };

View File

@ -51,81 +51,80 @@ export class CustomResourceDefinition extends KubeObject {
storedVersions: string[]; storedVersions: string[];
} }
getResourceUrl() { getResourceUrl(): string {
return crdResourcesURL({ return crdResourcesURL({
params: { params: {
group: this.getGroup(), group: this.getGroup(),
name: this.getPluralName(), name: this.getPluralName(),
} }
}) });
} }
getResourceApiBase() { getResourceApiBase(): string {
const { version, group } = this.spec; const { version, group } = this.spec;
return `/apis/${group}/${version}/${this.getPluralName()}` return `/apis/${group}/${version}/${this.getPluralName()}`;
} }
getPluralName() { getPluralName(): string {
return this.getNames().plural return this.getNames().plural;
} }
getResourceKind() { getResourceKind(): string {
return this.spec.names.kind return this.spec.names.kind;
} }
getResourceTitle() { getResourceTitle(): string {
const name = this.getPluralName(); const name = this.getPluralName();
return name[0].toUpperCase() + name.substr(1) return name[0].toUpperCase() + name.substr(1);
} }
getGroup() { getGroup(): string {
return this.spec.group; return this.spec.group;
} }
getScope() { getScope(): string {
return this.spec.scope; return this.spec.scope;
} }
getVersion() { getVersion(): string {
return this.spec.version; return this.spec.version;
} }
isNamespaced() { isNamespaced(): boolean {
return this.getScope() === "Namespaced"; return this.getScope() === "Namespaced";
} }
getStoredVersions() { getStoredVersions(): string {
return this.status.storedVersions.join(", "); return this.status.storedVersions.join(", ");
} }
getNames() { getNames(): CustomResourceDefinition["spec"]["names"] {
return this.spec.names; return this.spec.names;
} }
getConversion() { getConversion(): string {
return JSON.stringify(this.spec.conversion); return JSON.stringify(this.spec.conversion);
} }
getPrinterColumns(ignorePriority = true) { getPrinterColumns(ignorePriority = true): Required<CustomResourceDefinition["spec"]["additionalPrinterColumns"]> {
const columns = this.spec.additionalPrinterColumns || []; const columns = this.spec.additionalPrinterColumns || [];
return columns return columns
.filter(column => column.name != "Age") .filter(column => column.name != "Age")
.filter(column => ignorePriority ? true : !column.priority); .filter(column => ignorePriority ? true : !column.priority);
} }
getValidation() { getValidation(): string {
return JSON.stringify(this.spec.validation, null, 2); return JSON.stringify(this.spec.validation, null, 2);
} }
getConditions() { getConditions(): Required<CustomResourceDefinition["status"]["conditions"]> {
if (!this.status.conditions) return []; return (this.status.conditions || []).map(condition => {
return this.status.conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition; const { message, reason, lastTransitionTime, status } = condition;
return { return {
...condition, ...condition,
isReady: status === "True", isReady: status === "True",
tooltip: `${message || reason} (${lastTransitionTime})` tooltip: `${message || reason} (${lastTransitionTime})`
} };
}); });
} }
} }

View File

@ -1,6 +1,6 @@
import moment from "moment"; import moment from "moment";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { IPodContainer } from "./pods.api"; import { PodContainer } from "./pods.api";
import { formatDuration } from "../../utils/formatDuration"; import { formatDuration } from "../../utils/formatDuration";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
@ -39,7 +39,7 @@ export class CronJob extends KubeObject {
creationTimestamp?: string; creationTimestamp?: string;
}; };
spec: { spec: {
containers: IPodContainer[]; containers: PodContainer[];
restartPolicy: string; restartPolicy: string;
terminationGracePeriodSeconds: number; terminationGracePeriodSeconds: number;
dnsPolicy: string; dnsPolicy: string;
@ -56,26 +56,27 @@ export class CronJob extends KubeObject {
lastScheduleTime: string; lastScheduleTime: string;
} }
getSuspendFlag() { getSuspendFlag(): string {
return this.spec.suspend.toString() return this.spec.suspend.toString();
} }
getLastScheduleTime() { getLastScheduleTime(): string {
const diff = moment().diff(this.status.lastScheduleTime) return formatDuration(moment().diff(this.status.lastScheduleTime), true);
return formatDuration(diff, true)
} }
getSchedule() { getSchedule(): string {
return this.spec.schedule return this.spec.schedule;
} }
isNeverRun() { isNeverRun(): boolean {
const schedule = this.getSchedule(); const schedule = this.getSchedule();
const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const stamps = schedule.split(" "); const stamps = schedule.split(" ");
const day = Number(stamps[stamps.length - 3]); // 1-31 const day = Number(stamps[stamps.length - 3]); // 1-31
const month = Number(stamps[stamps.length - 2]); // 1-12 const month = Number(stamps[stamps.length - 2]); // 1-12
if (schedule.startsWith("@")) return false; if (schedule.startsWith("@")) {
return false;
}
return day > daysInMonth[month - 1]; return day > daysInMonth[month - 1];
} }
} }

View File

@ -1,6 +1,6 @@
import get from "lodash/get"; import get from "lodash/get";
import { IPodContainer } from "./pods.api"; import { PodContainer } from "./pods.api";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { Affinity, WorkloadKubeObject } from "../workload-kube-object";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
@ -22,13 +22,13 @@ export class DaemonSet extends WorkloadKubeObject {
}; };
}; };
spec: { spec: {
containers: IPodContainer[]; containers: PodContainer[];
initContainers?: IPodContainer[]; initContainers?: PodContainer[];
restartPolicy: string; restartPolicy: string;
terminationGracePeriodSeconds: number; terminationGracePeriodSeconds: number;
dnsPolicy: string; dnsPolicy: string;
hostPID: boolean; hostPID: boolean;
affinity?: IAffinity; affinity?: Affinity;
nodeSelector?: { nodeSelector?: {
[selector: string]: string; [selector: string]: string;
}; };
@ -61,10 +61,10 @@ export class DaemonSet extends WorkloadKubeObject {
numberUnavailable: number; numberUnavailable: number;
} }
getImages() { getImages(): string[] {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []) const containers: PodContainer[] = get(this, "spec.template.spec.containers", []);
const initContainers: IPodContainer[] = get(this, "spec.template.spec.initContainers", []) const initContainers: PodContainer[] = get(this, "spec.template.spec.initContainers", []);
return [...containers, ...initContainers].map(container => container.image) return [...containers, ...initContainers].map(container => container.image);
} }
} }

View File

@ -1,27 +1,24 @@
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { Affinity, WorkloadKubeObject } from "../workload-kube-object";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { CancelablePromise } from "client/utils/cancelableFetch";
import { KubeJsonApiData } from "../kube-json-api";
export class DeploymentApi extends KubeApi<Deployment> { export class DeploymentApi extends KubeApi<Deployment> {
protected getScaleApiUrl(params: { namespace: string; name: string }) { protected getScaleApiUrl(params: { namespace: string; name: string }): string {
return this.getUrl(params) + "/scale" return this.getUrl(params) + "/scale";
} }
getReplicas(params: { namespace: string; name: string }): Promise<number> { async getReplicas(params: { namespace: string; name: string }): Promise<number> {
return this.request const { status } = await this.request.get(this.getScaleApiUrl(params));
.get(this.getScaleApiUrl(params)) return status.replicas;
.then(({ status }: any) => status.replicas)
} }
scale(params: { namespace: string; name: string }, replicas: number) { scale(metadata: { namespace: string; name: string }, replicas: number): CancelablePromise<KubeJsonApiData> {
return this.request.put(this.getScaleApiUrl(params), { return this.request.put(
data: { this.getScaleApiUrl(metadata),
metadata: params, { data: { metadata, spec: { replicas } } }
spec: { );
replicas: replicas
}
}
})
} }
} }
@ -96,7 +93,7 @@ export class Deployment extends WorkloadKubeObject {
restartPolicy: string; restartPolicy: string;
terminationGracePeriodSeconds: number; terminationGracePeriodSeconds: number;
dnsPolicy: string; dnsPolicy: string;
affinity?: IAffinity; affinity?: Affinity;
nodeSelector?: { nodeSelector?: {
[selector: string]: string; [selector: string]: string;
}; };
@ -145,20 +142,15 @@ export class Deployment extends WorkloadKubeObject {
}[]; }[];
} }
getConditions(activeOnly = false) { getConditions(activeOnly = false): Deployment["status"]["conditions"] {
const { conditions } = this.status return this.status.conditions.filter(({ status }) => !activeOnly || status === "True");
if (!conditions) return []
if (activeOnly) {
return conditions.filter(c => c.status === "True")
}
return conditions
} }
getConditionsText(activeOnly = true) { getConditionsText(activeOnly = true): string {
return this.getConditions(activeOnly).map(({ type }) => type).join(" ") return this.getConditions(activeOnly).map(({ type }) => type).join(" ");
} }
getReplicas() { getReplicas(): number {
return this.spec.replicas || 0; return this.spec.replicas || 0;
} }
} }

View File

@ -2,25 +2,25 @@ import { autobind } from "../../utils";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export interface IEndpointPort { export interface EndpointPort {
name?: string; name?: string;
protocol: string; protocol: string;
port: number; port: number;
} }
export interface IEndpointAddress { export interface EndpointAddress {
hostname: string; hostname: string;
ip: string; ip: string;
nodeName: string; nodeName: string;
} }
export interface IEndpointSubset { export interface EndpointSubset {
addresses: IEndpointAddress[]; addresses: EndpointAddress[];
notReadyAddresses: IEndpointAddress[]; notReadyAddresses: EndpointAddress[];
ports: IEndpointPort[]; ports: EndpointPort[];
} }
interface ITargetRef { interface TargetRef {
kind: string; kind: string;
namespace: string; namespace: string;
name: string; name: string;
@ -29,7 +29,7 @@ interface ITargetRef {
apiVersion: string; apiVersion: string;
} }
export class EndpointAddress implements IEndpointAddress { export class EndpointAddress implements EndpointAddress {
hostname: string; hostname: string;
ip: string; ip: string;
nodeName: string; nodeName: string;
@ -41,34 +41,34 @@ export class EndpointAddress implements IEndpointAddress {
resourceVersion: string; resourceVersion: string;
}; };
constructor(data: IEndpointAddress) { constructor(data: EndpointAddress) {
Object.assign(this, data) Object.assign(this, data);
} }
getId() { getId(): string {
return this.ip return this.ip;
} }
getName() { getName(): string {
return this.hostname return this.hostname;
} }
getTargetRef(): ITargetRef { getTargetRef(): TargetRef {
if (this.targetRef) { if (this.targetRef) {
return Object.assign(this.targetRef, {apiVersion: "v1"}) return Object.assign(this.targetRef, {apiVersion: "v1"});
} else { } else {
return null return null;
} }
} }
} }
export class EndpointSubset implements IEndpointSubset { export class EndpointSubset implements EndpointSubset {
addresses: IEndpointAddress[]; addresses: EndpointAddress[];
notReadyAddresses: IEndpointAddress[]; notReadyAddresses: EndpointAddress[];
ports: IEndpointPort[]; ports: EndpointPort[];
constructor(data: IEndpointSubset) { constructor(data: EndpointSubset) {
Object.assign(this, data) Object.assign(this, data);
} }
getAddresses(): EndpointAddress[] { getAddresses(): EndpointAddress[] {
@ -83,16 +83,16 @@ export class EndpointSubset implements IEndpointSubset {
toString(): string { toString(): string {
if(!this.addresses) { if(!this.addresses) {
return "" return "";
} }
return this.addresses.map(address => { return this.addresses.map(address => {
if (!this.ports) { if (!this.ports) {
return address.ip return address.ip;
} }
return this.ports.map(port => { return this.ports.map(port => {
return `${address.ip}:${port.port}` return `${address.ip}:${port.port}`;
}).join(", ") }).join(", ");
}).join(", ") }).join(", ");
} }
} }
@ -100,7 +100,7 @@ export class EndpointSubset implements IEndpointSubset {
export class Endpoint extends KubeObject { export class Endpoint extends KubeObject {
static kind = "Endpoint" static kind = "Endpoint"
subsets: IEndpointSubset[] subsets: EndpointSubset[]
getEndpointSubsets(): EndpointSubset[] { getEndpointSubsets(): EndpointSubset[] {
const subsets = this.subsets || []; const subsets = this.subsets || [];
@ -109,9 +109,9 @@ export class Endpoint extends KubeObject {
toString(): string { toString(): string {
if(this.subsets) { if(this.subsets) {
return this.getEndpointSubsets().map(es => es.toString()).join(", ") return this.getEndpointSubsets().map(es => es.toString()).join(", ");
} else { } else {
return "<none>" return "<none>";
} }
} }

View File

@ -31,23 +31,23 @@ export class KubeEvent extends KubeObject {
reportingComponent: string reportingComponent: string
reportingInstance: string reportingInstance: string
isWarning() { isWarning(): boolean {
return this.type === "Warning"; return this.type === "Warning";
} }
getSource() { getSource(): string {
const { component, host } = this.source const { component, host } = this.source;
return `${component} ${host || ""}` return `${component} ${host || ""}`;
} }
getFirstSeenTime() { getFirstSeenTime(): string {
const diff = moment().diff(this.firstTimestamp) const diff = moment().diff(this.firstTimestamp);
return formatDuration(diff, true) return formatDuration(diff, true);
} }
getLastSeenTime() { getLastSeenTime(): string {
const diff = moment().diff(this.lastTimestamp) const diff = moment().diff(this.lastTimestamp);
return formatDuration(diff, true) return formatDuration(diff, true);
} }
} }
@ -56,4 +56,4 @@ export const eventApi = new KubeApi({
apiBase: "/api/v1/events", apiBase: "/api/v1/events",
isNamespaced: true, isNamespaced: true,
objectConstructor: KubeEvent, objectConstructor: KubeEvent,
}) });

View File

@ -2,54 +2,7 @@ import pathToRegExp from "path-to-regexp";
import { apiKubeHelm } from "../index"; import { apiKubeHelm } from "../index";
import { stringify } from "querystring"; import { stringify } from "querystring";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { CancelablePromise } from "client/utils/cancelableFetch";
interface IHelmChartList {
[repo: string]: {
[name: string]: HelmChart;
};
}
export interface IHelmChartDetails {
readme: string;
versions: HelmChart[];
}
const endpoint = pathToRegExp.compile(`/v2/charts/:repo?/:name?`) as (params?: {
repo?: string;
name?: string;
}) => string;
export const helmChartsApi = {
list() {
return apiKubeHelm
.get<IHelmChartList>(endpoint())
.then(data => {
return Object
.values(data)
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), [])
.map(HelmChart.create);
});
},
get(repo: string, name: string, readmeVersion?: string) {
const path = endpoint({ repo, name });
return apiKubeHelm
.get<IHelmChartDetails>(path + "?" + stringify({ version: readmeVersion }))
.then(data => {
const versions = data.versions.map(HelmChart.create);
const readme = data.readme;
return {
readme,
versions,
}
});
},
getValues(repo: string, name: string, version: string) {
return apiKubeHelm
.get<string>(`/v2/charts/${repo}/${name}/values?` + stringify({ version }));
}
};
@autobind() @autobind()
export class HelmChart { export class HelmChart {
@ -57,10 +10,6 @@ export class HelmChart {
Object.assign(this, data); Object.assign(this, data);
} }
static create(data: any) {
return new HelmChart(data);
}
apiVersion: string apiVersion: string
name: string name: string
version: string version: string
@ -83,47 +32,74 @@ export class HelmChart {
deprecated?: boolean deprecated?: boolean
tillerVersion?: string tillerVersion?: string
getId() { getId(): string {
return this.digest; return this.digest;
} }
getName() { getName(): string {
return this.name; return this.name;
} }
getFullName(splitter = "/") { getFullName(splitter = "/"): string {
return [this.getRepository(), this.getName()].join(splitter); return [this.repo, this.name].join(splitter);
} }
getDescription() { getMaintainers(): Required<HelmChart["maintainers"]> {
return this.description;
}
getIcon() {
return this.icon;
}
getHome() {
return this.home;
}
getMaintainers() {
return this.maintainers || []; return this.maintainers || [];
} }
getVersion() { getAppVersion(): string {
return this.version;
}
getRepository() {
return this.repo;
}
getAppVersion() {
return this.appVersion || ""; return this.appVersion || "";
} }
getKeywords() { getKeywords(): Required<HelmChart["keywords"]> {
return this.keywords || []; return this.keywords || [];
} }
} }
interface HelmChartList {
[repo: string]: {
[name: string]: HelmChart;
};
}
export interface HelmChartDetails {
readme: string;
versions: HelmChart[];
}
const endpoint = pathToRegExp.compile(`/v2/charts/:repo?/:name?`) as (params?: {
repo?: string;
name?: string;
}) => string;
export interface HelmChartInfo {
readme: string;
versions: HelmChart[];
}
export const helmChartsApi = {
list(): CancelablePromise<HelmChart[]> {
return apiKubeHelm.get<HelmChartList>(endpoint())
.then(data => {
return Object.values(data)
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), [])
.map(data => new HelmChart(data));
});
},
get(repo: string, name: string, readmeVersion?: string): CancelablePromise<HelmChartInfo> {
const path = endpoint({ repo, name });
return apiKubeHelm.get<HelmChartDetails>(path + "?" + stringify({ version: readmeVersion }))
.then(({ readme, versions }) => ({
readme,
versions: versions.map(data => new HelmChart(data))
}));
},
getValues(repo: string, name: string, version: string): CancelablePromise<string> {
return apiKubeHelm
.get<string>(`/v2/charts/${repo}/${name}/values?` + stringify({ version }));
}
};

View File

@ -6,8 +6,10 @@ import { apiKubeHelm } from "../index";
import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.store"; import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.store";
import { ItemObject } from "../../item.store"; import { ItemObject } from "../../item.store";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { CancelablePromise } from "client/utils/cancelableFetch";
import { KubeJsonApiData } from "../kube-json-api";
interface IReleasePayload { interface ReleasePayload {
name: string; name: string;
namespace: string; namespace: string;
version: string; version: string;
@ -23,15 +25,15 @@ interface IReleasePayload {
}; };
} }
interface IReleaseRawDetails extends IReleasePayload { interface ReleaseRawDetails extends ReleasePayload {
resources: string; resources: string;
} }
export interface IReleaseDetails extends IReleasePayload { export interface ReleaseInfo extends ReleasePayload {
resources: KubeObject[]; resources: KubeObject[];
} }
export interface IReleaseCreatePayload { export interface ReleaseCreatePayload {
name?: string; name?: string;
repo: string; repo: string;
chart: string; chart: string;
@ -40,19 +42,19 @@ export interface IReleaseCreatePayload {
values: string; values: string;
} }
export interface IReleaseUpdatePayload { export interface ReleaseUpdatePayload {
repo: string; repo: string;
chart: string; chart: string;
version: string; version: string;
values: string; values: string;
} }
export interface IReleaseUpdateDetails { export interface ReleaseUpdateDetails {
log: string; log: string;
release: IReleaseDetails; release: ReleaseInfo;
} }
export interface IReleaseRevision { export interface ReleaseRevision {
revision: number; revision: number;
updated: string; updated: string;
status: string; status: string;
@ -67,74 +69,12 @@ const endpoint = pathToRegExp.compile(`/v2/releases/:namespace?/:name?`) as (
} }
) => string; ) => string;
export const helmReleasesApi = {
list(namespace?: string) {
return apiKubeHelm
.get<HelmRelease[]>(endpoint({ namespace }))
.then(releases => releases.map(HelmRelease.create));
},
get(name: string, namespace: string) {
const path = endpoint({ name, namespace });
return apiKubeHelm.get<IReleaseRawDetails>(path).then(details => {
const items: KubeObject[] = JSON.parse(details.resources).items;
const resources = items.map(item => KubeObject.create(item));
return {
...details,
resources
}
});
},
create(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
const { repo, ...data } = payload;
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
return apiKubeHelm.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 apiKubeHelm.put(endpoint({ name, namespace }), { data });
},
async delete(name: string, namespace: string) {
const path = endpoint({ name, namespace });
return apiKubeHelm.del(path);
},
getValues(name: string, namespace: string) {
const path = endpoint({ name, namespace }) + "/values";
return apiKubeHelm.get<string>(path);
},
getHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
const path = endpoint({ name, namespace }) + "/history";
return apiKubeHelm.get(path);
},
rollback(name: string, namespace: string, revision: number) {
const path = endpoint({ name, namespace }) + "/rollback";
return apiKubeHelm.put(path, {
data: {
revision: revision
}
});
}
};
@autobind() @autobind()
export class HelmRelease implements ItemObject { export class HelmRelease implements ItemObject {
constructor(data: any) { constructor(data: any) {
Object.assign(this, data); Object.assign(this, data);
} }
static create(data: any) {
return new HelmRelease(data);
}
appVersion: string appVersion: string
name: string name: string
namespace: string namespace: string
@ -143,45 +83,32 @@ export class HelmRelease implements ItemObject {
updated: string updated: string
revision: number revision: number
getId() { getId(): string {
return this.namespace + this.name; return this.namespace + this.name;
} }
getName() { getName(): string {
return this.name; return this.name;
} }
getNs() { getChart(withVersion = false): string {
return this.namespace; let chart = this.chart;
} if (!withVersion && this.getVersion() != "") {
const search = new RegExp(`-${this.getVersion()}`);
getChart(withVersion = false) {
let chart = this.chart
if(!withVersion && this.getVersion() != "" ) {
const search = new RegExp(`-${this.getVersion()}`)
chart = chart.replace(search, ""); chart = chart.replace(search, "");
} }
return chart return chart;
} }
getRevision() { getStatus(): string {
return this.revision;
}
getStatus() {
return capitalize(this.status); return capitalize(this.status);
} }
getVersion() { getVersion(): string {
const versions = this.chart.match(/(v?\d+)[^-].*$/) return this.chart.match(/(v?\d+)[^-].*$/)?.[0] || "";
if (versions) {
return versions[0]
} else {
return ""
}
} }
getUpdated(humanize = true, compact = true) { getUpdated(humanize = true, compact = true): number | string {
const now = new Date().getTime(); const now = new Date().getTime();
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 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 updatedDate = new Date(updated).getTime();
@ -194,11 +121,67 @@ export class HelmRelease implements ItemObject {
// Helm does not store from what repository the release is installed, // Helm does not store from what repository the release is installed,
// so we have to try to guess it by searching charts // so we have to try to guess it by searching charts
async getRepo() { async getRepo(): Promise<string> {
const chartName = this.getChart(); const chartName = this.getChart();
const version = this.getVersion(); const version = this.getVersion();
const versions = await helmChartStore.getVersions(chartName); const versions = await helmChartStore.getVersions(chartName);
const chartVersion = versions.find(chartVersion => chartVersion.version === version); const chartVersion = versions.find(chartVersion => chartVersion.version === version);
return chartVersion ? chartVersion.repo : ""; return chartVersion?.repo || "";
} }
} }
export const helmReleasesApi = {
async list(namespace?: string): Promise<HelmRelease[]> {
const releases = await apiKubeHelm.get<HelmRelease[]>(endpoint({ namespace }));
return releases.map(data => new HelmRelease(data));
},
async get(name: string, namespace: string): Promise<ReleaseInfo> {
const path = endpoint({ name, namespace });
const details = await apiKubeHelm.get<ReleaseRawDetails>(path);
const items: KubeObject[] = JSON.parse(details.resources).items;
const resources = items.map(item => new KubeObject(item));
return {
...details,
resources
};
},
create(payload: ReleaseCreatePayload): Promise<ReleaseUpdateDetails> {
const { repo, ...data } = payload;
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
return apiKubeHelm.post(endpoint(), { data });
},
update(name: string, namespace: string, payload: ReleaseUpdatePayload): Promise<ReleaseUpdateDetails> {
const { repo, ...data } = payload;
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
return apiKubeHelm.put(endpoint({ name, namespace }), { data });
},
delete(name: string, namespace: string): CancelablePromise<KubeJsonApiData> {
const path = endpoint({ name, namespace });
return apiKubeHelm.del(path);
},
getValues(name: string, namespace: string): CancelablePromise<string> {
const path = endpoint({ name, namespace }) + "/values";
return apiKubeHelm.get<string>(path);
},
getHistory(name: string, namespace: string): Promise<ReleaseRevision[]> {
const path = endpoint({ name, namespace }) + "/history";
return apiKubeHelm.get(path);
},
rollback(name: string, namespace: string, revision: number): CancelablePromise<KubeJsonApiData> {
const path = endpoint({ name, namespace }) + "/rollback";
return apiKubeHelm.put(path, {
data: {
revision: revision
}
});
}
};

View File

@ -22,7 +22,7 @@ export type IHpaMetricData<T = any> = T & {
targetAverageValue?: string; targetAverageValue?: string;
} }
export interface IHpaMetric { export interface HpaMetric {
[kind: string]: IHpaMetricData; [kind: string]: IHpaMetricData;
type: HpaMetricType; type: HpaMetricType;
@ -38,6 +38,19 @@ export interface IHpaMetric {
}>; }>;
} }
export interface PodStatusCondition {
lastTransitionTime: string;
message: string;
reason: string;
status: string;
type: string;
}
export interface PodCondition extends PodStatusCondition {
isReady: boolean;
tooltip: string;
}
export class HorizontalPodAutoscaler extends KubeObject { export class HorizontalPodAutoscaler extends KubeObject {
static kind = "HorizontalPodAutoscaler"; static kind = "HorizontalPodAutoscaler";
@ -49,58 +62,34 @@ export class HorizontalPodAutoscaler extends KubeObject {
}; };
minReplicas: number; minReplicas: number;
maxReplicas: number; maxReplicas: number;
metrics: IHpaMetric[]; metrics: HpaMetric[];
} }
status: { status: {
currentReplicas: number; currentReplicas: number;
desiredReplicas: number; desiredReplicas: number;
currentMetrics: IHpaMetric[]; currentMetrics: HpaMetric[];
conditions: { conditions: PodStatusCondition[];
lastTransitionTime: string;
message: string;
reason: string;
status: string;
type: string;
}[];
} }
getMaxPods() { getConditions(): PodCondition[] {
return this.spec.maxReplicas || 0; if (!this.status.conditions) {
} return [];
}
getMinPods() {
return this.spec.minReplicas || 0;
}
getReplicas() {
return this.status.currentReplicas;
}
getConditions() {
if (!this.status.conditions) return [];
return this.status.conditions.map(condition => { return this.status.conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition; const { message, reason, lastTransitionTime, status } = condition;
return { return {
...condition, ...condition,
isReady: status === "True", isReady: status === "True",
tooltip: `${message || reason} (${lastTransitionTime})` tooltip: `${message || reason} (${lastTransitionTime})`
} };
}); });
} }
getMetrics() { protected getMetricName(metric: HpaMetric): string {
return this.spec.metrics || [];
}
getCurrentMetrics() {
return this.status.currentMetrics || [];
}
protected getMetricName(metric: IHpaMetric): string {
const { type, resource, pods, object, external } = metric; const { type, resource, pods, object, external } = metric;
switch (type) { switch (type) {
case HpaMetricType.Resource: case HpaMetricType.Resource:
return resource.name return resource.name;
case HpaMetricType.Pods: case HpaMetricType.Pods:
return pods.metricName; return pods.metricName;
case HpaMetricType.Object: case HpaMetricType.Object:
@ -111,9 +100,9 @@ export class HorizontalPodAutoscaler extends KubeObject {
} }
// todo: refactor // todo: refactor
getMetricValues(metric: IHpaMetric): string { getMetricValues(metric: HpaMetric): string {
const metricType = metric.type.toLowerCase(); const metricType = metric.type.toLowerCase();
const currentMetric = this.getCurrentMetrics().find(current => const currentMetric = this.status.currentMetrics.find(current =>
metric.type == current.type && this.getMetricName(metric) == this.getMetricName(current) metric.type == current.type && this.getMetricName(metric) == this.getMetricName(current)
); );
const current = currentMetric ? currentMetric[metricType] : null; const current = currentMetric ? currentMetric[metricType] : null;
@ -122,11 +111,15 @@ export class HorizontalPodAutoscaler extends KubeObject {
let targetValue = "unknown"; let targetValue = "unknown";
if (current) { if (current) {
currentValue = current.currentAverageUtilization || current.currentAverageValue || current.currentValue; currentValue = current.currentAverageUtilization || current.currentAverageValue || current.currentValue;
if (current.currentAverageUtilization) currentValue += "%"; if (current.currentAverageUtilization) {
currentValue += "%";
}
} }
if (target) { if (target) {
targetValue = target.targetAverageUtilization || target.targetAverageValue || target.targetValue; targetValue = target.targetAverageUtilization || target.targetAverageValue || target.targetValue;
if (target.targetAverageUtilization) targetValue += "%" if (target.targetAverageUtilization) {
targetValue += "%";
}
} }
return `${currentValue} / ${targetValue}`; return `${currentValue} / ${targetValue}`;
} }

View File

@ -1,33 +1,33 @@
// Local express.js endpoints // Local express.js endpoints
export * from "./config.api" export * from "./config.api";
export * from "./cluster.api" export * from "./cluster.api";
export * from "./kubeconfig.api" export * from "./kubeconfig.api";
// Kubernetes endpoints // Kubernetes endpoints
// Docs: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/ // Docs: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/
export * from "./namespaces.api" export * from "./namespaces.api";
export * from "./cluster-role.api" export * from "./cluster-role.api";
export * from "./cluster-role-binding.api" export * from "./cluster-role-binding.api";
export * from "./role.api" export * from "./role.api";
export * from "./role-binding.api" export * from "./role-binding.api";
export * from "./secret.api" export * from "./secret.api";
export * from "./service-accounts.api" export * from "./service-accounts.api";
export * from "./nodes.api" export * from "./nodes.api";
export * from "./pods.api" export * from "./pods.api";
export * from "./deployment.api" export * from "./deployment.api";
export * from "./daemon-set.api" export * from "./daemon-set.api";
export * from "./stateful-set.api" export * from "./stateful-set.api";
export * from "./replica-set.api" export * from "./replica-set.api";
export * from "./job.api" export * from "./job.api";
export * from "./cron-job.api" export * from "./cron-job.api";
export * from "./configmap.api" export * from "./configmap.api";
export * from "./ingress.api" export * from "./ingress.api";
export * from "./network-policy.api" export * from "./network-policy.api";
export * from "./persistent-volume-claims.api" export * from "./persistent-volume-claims.api";
export * from "./persistent-volume.api" export * from "./persistent-volume.api";
export * from "./service.api" export * from "./service.api";
export * from "./endpoint.api" export * from "./endpoint.api";
export * from "./storage-class.api" export * from "./storage-class.api";
export * from "./pod-metrics.api" export * from "./pod-metrics.api";
export * from "./podsecuritypolicy.api" export * from "./podsecuritypolicy.api";
export * from "./selfsubjectrulesreviews.api" export * from "./selfsubjectrulesreviews.api";

View File

@ -1,11 +1,11 @@
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api"; import { Metrics, metricsApi } from "./metrics.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export class IngressApi extends KubeApi<Ingress> { export class IngressApi extends KubeApi<Ingress> {
getMetrics(ingress: string, namespace: string): Promise<IIngressMetrics> { getMetrics(ingress: string, namespace: string): Promise<IngressMetrics> {
const opts = { category: "ingress", ingress } const opts = { category: "ingress", ingress };
return metricsApi.getMetrics({ return metricsApi.getMetrics({
bytesSentSuccess: opts, bytesSentSuccess: opts,
bytesSentFailure: opts, bytesSentFailure: opts,
@ -17,7 +17,7 @@ export class IngressApi extends KubeApi<Ingress> {
} }
} }
export interface IIngressMetrics<T = IMetrics> { export interface IngressMetrics<T = Metrics> {
[metric: string]: T; [metric: string]: T;
bytesSentSuccess: T; bytesSentSuccess: T;
bytesSentFailure: T; bytesSentFailure: T;
@ -56,52 +56,55 @@ export class Ingress extends KubeObject {
}; };
} }
getRoutes() { getRoutes(): string[] {
const { spec: { tls, rules } } = this const { spec: { tls, rules } } = this;
if (!rules) return [] if (!rules) {
return [];
}
let protocol = "http" let protocol = "http";
const routes: string[] = [] const routes: string[] = [];
if (tls && tls.length > 0) { if (tls && tls.length > 0) {
protocol += "s" protocol += "s";
} }
rules.map(rule => { rules.map(rule => {
const host = rule.host ? rule.host : "*" const host = rule.host ? rule.host : "*";
if (rule.http && rule.http.paths) { if (rule.http && rule.http.paths) {
rule.http.paths.forEach(path => { rule.http.paths.forEach(path => {
routes.push(protocol + "://" + host + (path.path || "/") + " ⇢ " + path.backend.serviceName + ":" + path.backend.servicePort) routes.push(protocol + "://" + host + (path.path || "/") + " ⇢ " + path.backend.serviceName + ":" + path.backend.servicePort);
}) });
} }
}) });
return routes; return routes;
} }
getHosts() { getHosts(): string[] {
const { spec: { rules } } = this const { spec: { rules } } = this;
if (!rules) return [] if (!rules) {
return rules.filter(rule => rule.host).map(rule => rule.host) return [];
}
return rules.filter(rule => rule.host).map(rule => rule.host);
} }
getPorts() { getPorts(): string {
const ports: number[] = [] const ports: number[] = [];
const { spec: { tls, rules, backend } } = this const { spec: { tls, rules, backend } } = this;
const httpPort = 80 const httpPort = 80;
const tlsPort = 443 const tlsPort = 443;
if (rules && rules.length > 0) { if (rules && rules.length > 0) {
if (rules.some(rule => rule.hasOwnProperty("http"))) { if (rules.some(rule => rule.hasOwnProperty("http"))) {
ports.push(httpPort) ports.push(httpPort);
} }
} } else {
else {
if (backend && backend.servicePort) { if (backend && backend.servicePort) {
ports.push(backend.servicePort) ports.push(backend.servicePort);
} }
} }
if (tls && tls.length > 0) { if (tls && tls.length > 0) {
ports.push(tlsPort) ports.push(tlsPort);
} }
return ports.join(", ") return ports.join(", ");
} }
} }

View File

@ -1,9 +1,19 @@
import get from "lodash/get"; import get from "lodash/get";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { Affinity, WorkloadKubeObject } from "../workload-kube-object";
import { IPodContainer } from "./pods.api"; import { PodContainer } from "./pods.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { JsonApiParams } from "../json-api"; import { JsonApiParams } from "../json-api";
import { CancelablePromise } from "client/utils/cancelableFetch";
import { KubeJsonApiData } from "../kube-json-api";
export interface JobCondition {
type: string;
status: string;
lastProbeTime: string;
lastTransitionTime: string;
message?: string;
}
@autobind() @autobind()
export class Job extends WorkloadKubeObject { export class Job extends WorkloadKubeObject {
@ -26,12 +36,12 @@ export class Job extends WorkloadKubeObject {
}; };
}; };
spec: { spec: {
containers: IPodContainer[]; containers: PodContainer[];
restartPolicy: string; restartPolicy: string;
terminationGracePeriodSeconds: number; terminationGracePeriodSeconds: number;
dnsPolicy: string; dnsPolicy: string;
hostPID: boolean; hostPID: boolean;
affinity?: IAffinity; affinity?: Affinity;
nodeSelector?: { nodeSelector?: {
[selector: string]: string; [selector: string]: string;
}; };
@ -44,7 +54,7 @@ export class Job extends WorkloadKubeObject {
schedulerName: string; schedulerName: string;
}; };
}; };
containers?: IPodContainer[]; containers?: PodContainer[];
restartPolicy?: string; restartPolicy?: string;
terminationGracePeriodSeconds?: number; terminationGracePeriodSeconds?: number;
dnsPolicy?: string; dnsPolicy?: string;
@ -53,48 +63,36 @@ export class Job extends WorkloadKubeObject {
schedulerName?: string; schedulerName?: string;
} }
status: { status: {
conditions: { conditions: JobCondition[];
type: string;
status: string;
lastProbeTime: string;
lastTransitionTime: string;
message?: string;
}[];
startTime: string; startTime: string;
completionTime: string; completionTime: string;
succeeded: number; succeeded: number;
} }
getDesiredCompletions() { getDesiredCompletions(): number {
return this.spec.completions || 0; return this.spec.completions || 0;
} }
getCompletions() { getCompletions(): number {
return this.status.succeeded || 0; return this.status.succeeded || 0;
} }
getParallelism() { getCondition(): JobCondition {
return this.spec.parallelism;
}
getCondition() {
// Type of Job condition could be only Complete or Failed // Type of Job condition could be only Complete or Failed
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#jobcondition-v1-batch // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#jobcondition-v1-batch
const { conditions } = this.status; return this.status.conditions.find(({ status }) => status === "True");
if (!conditions) return;
return conditions.find(({ status }) => status === "True");
} }
getImages() { getImages(): string[] {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []) const containers: PodContainer[] = get(this, "spec.template.spec.containers", []);
return [...containers].map(container => container.image) return containers.map(container => container.image);
} }
delete() { delete(): CancelablePromise<KubeJsonApiData> {
const params: JsonApiParams = { const params: JsonApiParams = {
query: { propagationPolicy: "Background" } query: { propagationPolicy: "Background" }
} };
return super.delete(params) return super.delete(params);
} }
} }

View File

@ -1,12 +1,14 @@
// Kubeconfig api // Kubeconfig api
import { apiBase } from "../index"; import { apiBase } from "../index";
import { CancelablePromise } from "client/utils/cancelableFetch";
import { JsonApiData } from "../json-api";
export const kubeConfigApi = { export const kubeConfigApi = {
getUserConfig() { getUserConfig(): CancelablePromise<JsonApiData> {
return apiBase.get("/kubeconfig/user"); return apiBase.get("/kubeconfig/user");
}, },
getServiceAccountConfig(account: string, namespace: string) { getServiceAccountConfig(account: string, namespace: string): CancelablePromise<JsonApiData> {
return apiBase.get(`/kubeconfig/service-account/${namespace}/${account}`); return apiBase.get(`/kubeconfig/service-account/${namespace}/${account}`);
}, },
}; };

View File

@ -4,15 +4,15 @@ import moment from "moment";
import { apiBase } from "../index"; import { apiBase } from "../index";
import { IMetricsQuery } from "../../../server/common/metrics"; import { IMetricsQuery } from "../../../server/common/metrics";
export interface IMetrics { export interface Metrics {
status: string; status: string;
data: { data: {
resultType: string; resultType: string;
result: IMetricsResult[]; result: MetricsResult[];
}; };
} }
export interface IMetricsResult { export interface MetricsResult {
metric: { metric: {
[name: string]: string; [name: string]: string;
instance: string; instance: string;
@ -25,7 +25,7 @@ export interface IMetricsResult {
values: [number, string][]; values: [number, string][];
} }
export interface IMetricsReqParams { export interface MetricsReqParams {
start?: number | string; // timestamp in seconds or valid date-string start?: number | string; // timestamp in seconds or valid date-string
end?: number | string; end?: number | string;
step?: number; // step in seconds (default: 60s = each point 1m) step?: number; // step in seconds (default: 60s = each point 1m)
@ -34,7 +34,7 @@ export interface IMetricsReqParams {
} }
export const metricsApi = { export const metricsApi = {
async getMetrics<T = IMetricsQuery>(query: T, reqParams: IMetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: IMetrics } : IMetrics> { async getMetrics<T = IMetricsQuery>(query: T, reqParams: MetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: Metrics } : Metrics> {
const { range = 3600, step = 60, namespace } = reqParams; const { range = 3600, step = 60, namespace } = reqParams;
let { start, end } = reqParams; let { start, end } = reqParams;
@ -55,18 +55,20 @@ export const metricsApi = {
}, },
}; };
export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { export function normalizeMetrics(metrics: Metrics, frames = 60): Metrics {
if (!metrics?.data?.result) { if (!metrics?.data?.result) {
return { return {
data: { data: {
resultType: "", resultType: "",
result: [{ result: [{
metric: {}, metric: {
instance: "",
},
values: [] values: []
} as IMetricsResult], }],
}, },
status: "", status: "",
} };
} }
const { result } = metrics.data; const { result } = metrics.data;
@ -75,34 +77,40 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
if (frames > 0) { if (frames > 0) {
// fill the gaps // fill the gaps
result.forEach(res => { result.forEach(res => {
if (!res.values || !res.values.length) return; if (!res.values || !res.values.length) {
return;
}
while (res.values.length < frames) { while (res.values.length < frames) {
const timestamp = moment.unix(res.values[0][0]).subtract(1, "minute").unix(); const timestamp = moment.unix(res.values[0][0]).subtract(1, "minute").unix();
res.values.unshift([timestamp, "0"]) res.values.unshift([timestamp, "0"]);
} }
}); });
} }
} else { } else {
// always return at least empty values array // always return at least empty values array
result.push({ result.push({
metric: {}, metric: {
instance: "",
},
values: [] values: []
} as IMetricsResult); });
} }
return metrics; return metrics;
} }
export function isMetricsEmpty(metrics: { [key: string]: IMetrics }) { export function isMetricsEmpty(metrics: { [key: string]: Metrics }): boolean {
return Object.values(metrics).every(metric => !metric?.data?.result?.length); return Object.values(metrics).every(metric => !metric?.data?.result?.length);
} }
export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: string): { [key: string]: IMetrics } { export function getItemMetrics(metrics: { [key: string]: Metrics }, itemName: string): { [key: string]: Metrics } {
if (!metrics) return; if (!metrics) {
return;
}
const itemMetrics = { ...metrics }; const itemMetrics = { ...metrics };
for (const metric in metrics) { for (const metric in metrics) {
if (!metrics[metric]?.data?.result) { if (!metrics[metric]?.data?.result) {
continue continue;
} }
const results = metrics[metric].data.result; const results = metrics[metric].data.result;
const result = results.find(res => Object.values(res.metric)[0] == itemName); const result = results.find(res => Object.values(res.metric)[0] == itemName);
@ -111,7 +119,7 @@ export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: s
return itemMetrics; return itemMetrics;
} }
export function getMetricLastPoints(metrics: { [key: string]: IMetrics }) { export function getMetricLastPoints(metrics: { [key: string]: Metrics }): { [metric: string]: number } {
const result: Partial<{[metric: string]: number}> = {}; const result: Partial<{[metric: string]: number}> = {};
Object.keys(metrics).forEach(metricName => { Object.keys(metrics).forEach(metricName => {

View File

@ -15,7 +15,7 @@ export class Namespace extends KubeObject {
phase: string; phase: string;
} }
getStatus() { getStatus(): string {
return this.status ? this.status.phase : "-"; return this.status ? this.status.phase : "-";
} }
} }

View File

@ -2,22 +2,22 @@ import { KubeObject } from "../kube-object";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export interface IPolicyIpBlock { export interface PolicyIpBlock {
cidr: string; cidr: string;
except?: string[]; except?: string[];
} }
export interface IPolicySelector { export interface PolicySelector {
matchLabels: { matchLabels: {
[label: string]: string; [label: string]: string;
}; };
} }
export interface IPolicyIngress { export interface PolicyIngress {
from: { from: {
ipBlock?: IPolicyIpBlock; ipBlock?: PolicyIpBlock;
namespaceSelector?: IPolicySelector; namespaceSelector?: PolicySelector;
podSelector?: IPolicySelector; podSelector?: PolicySelector;
}[]; }[];
ports: { ports: {
protocol: string; protocol: string;
@ -25,9 +25,9 @@ export interface IPolicyIngress {
}[]; }[];
} }
export interface IPolicyEgress { export interface PolicyEgress {
to: { to: {
ipBlock: IPolicyIpBlock; ipBlock: PolicyIpBlock;
}[]; }[];
ports: { ports: {
protocol: string; protocol: string;
@ -47,19 +47,23 @@ export class NetworkPolicy extends KubeObject {
}; };
}; };
policyTypes: string[]; policyTypes: string[];
ingress: IPolicyIngress[]; ingress: PolicyIngress[];
egress: IPolicyEgress[]; egress: PolicyEgress[];
} }
getMatchLabels(): string[] { getMatchLabels(): string[] {
if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) return []; if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) {
return [];
}
return Object return Object
.entries(this.spec.podSelector.matchLabels) .entries(this.spec.podSelector.matchLabels)
.map(data => data.join(":")) .map(data => data.join(":"));
} }
getTypes(): string[] { getTypes(): string[] {
if (!this.spec.policyTypes) return []; if (!this.spec.policyTypes) {
return [];
}
return this.spec.policyTypes; return this.spec.policyTypes;
} }
} }

View File

@ -1,11 +1,11 @@
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils"; import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api"; import { Metrics, metricsApi } from "./metrics.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export class NodesApi extends KubeApi<Node> { export class NodesApi extends KubeApi<Node> {
getMetrics(): Promise<INodeMetrics> { getMetrics(): Promise<NodeMetrics> {
const opts = { category: "nodes"} const opts = { category: "nodes"};
return metricsApi.getMetrics({ return metricsApi.getMetrics({
memoryUsage: opts, memoryUsage: opts,
@ -18,7 +18,7 @@ export class NodesApi extends KubeApi<Node> {
} }
} }
export interface INodeMetrics<T = IMetrics> { export interface NodeMetrics<T = Metrics> {
[metric: string]: T; [metric: string]: T;
memoryUsage: T; memoryUsage: T;
memoryCapacity: T; memoryCapacity: T;
@ -28,6 +28,21 @@ export interface INodeMetrics<T = IMetrics> {
fsSize: T; fsSize: T;
} }
export interface NodeTaint {
key: string;
value: string;
effect: string;
}
export interface NodeConditions {
type: string;
status?: string;
lastHeartbeatTime?: string;
lastTransitionTime?: string;
reason?: string;
message?: string;
}
@autobind() @autobind()
export class Node extends KubeObject { export class Node extends KubeObject {
static kind = "Node" static kind = "Node"
@ -35,11 +50,7 @@ export class Node extends KubeObject {
spec: { spec: {
podCIDR: string; podCIDR: string;
externalID: string; externalID: string;
taints?: { taints?: NodeTaint[];
key: string;
value: string;
effect: string;
}[];
unschedulable?: boolean; unschedulable?: boolean;
} }
status: { status: {
@ -53,14 +64,7 @@ export class Node extends KubeObject {
memory: string; memory: string;
pods: string; pods: string;
}; };
conditions: { conditions: NodeConditions[];
type: string;
status?: string;
lastHeartbeatTime?: string;
lastTransitionTime?: string;
reason?: string;
message?: string;
}[];
addresses: { addresses: {
type: string; type: string;
address: string; address: string;
@ -83,75 +87,75 @@ export class Node extends KubeObject {
}[]; }[];
} }
getNodeConditionText() { getNodeConditionText(): string {
const { conditions } = this.status return this.status
if (!conditions) return "" .conditions
return conditions.reduce((types, current) => { .filter(({status}) => status === "True")
if (current.status !== "True") return "" .map(({type}) => type)
return types += ` ${current.type}` .join(" ");
}, "")
} }
getTaints() { getTaints(): NodeTaint[] {
return this.spec.taints || []; return this.spec.taints || [];
} }
getRoleLabels() { getRoleLabels(): string {
const roleLabels = Object.keys(this.metadata.labels).filter(key => const roleLabels = Object.keys(this.metadata.labels).filter(key =>
key.includes("node-role.kubernetes.io") key.includes("node-role.kubernetes.io")
).map(key => key.match(/([^/]+$)/)[0]) // all after last slash ).map(key => key.match(/([^/]+$)/)[0]); // all after last slash
if (this.metadata.labels["kubernetes.io/role"] != undefined) { if (this.metadata.labels["kubernetes.io/role"] != undefined) {
roleLabels.push(this.metadata.labels["kubernetes.io/role"]) roleLabels.push(this.metadata.labels["kubernetes.io/role"]);
} }
return roleLabels.join(", ") return roleLabels.join(", ");
} }
getCpuCapacity() { getCpuCapacity(): number {
if (!this.status.capacity || !this.status.capacity.cpu) return 0 if (!this.status.capacity || !this.status.capacity.cpu) {
return cpuUnitsToNumber(this.status.capacity.cpu) return 0;
}
return cpuUnitsToNumber(this.status.capacity.cpu);
} }
getMemoryCapacity() { getMemoryCapacity(): number {
if (!this.status.capacity || !this.status.capacity.memory) return 0 if (!this.status.capacity || !this.status.capacity.memory) {
return unitsToBytes(this.status.capacity.memory) return 0;
}
return unitsToBytes(this.status.capacity.memory);
} }
getConditions() { getConditions(): NodeConditions[] {
const conditions = this.status.conditions || [];
if (this.isUnschedulable()) { if (this.isUnschedulable()) {
return [{ type: "SchedulingDisabled", status: "True" }, ...conditions]; return [{ type: "SchedulingDisabled", status: "True" }, ...this.status.conditions];
} }
return conditions; return this.status.conditions;
} }
getActiveConditions() { getActiveConditions(): NodeConditions[] {
return this.getConditions().filter(c => c.status === "True"); return this.getConditions().filter(c => c.status === "True");
} }
getWarningConditions() { getWarningConditions(): NodeConditions[] {
const goodConditions = ["Ready", "HostUpgrades", "SchedulingDisabled"]; const goodConditions = new Set(["Ready", "HostUpgrades", "SchedulingDisabled"]);
return this.getActiveConditions().filter(condition => { return this.getActiveConditions().filter(condition => !goodConditions.has(condition.type));
return !goodConditions.includes(condition.type);
});
} }
getKubeletVersion() { getKubeletVersion(): string {
return this.status.nodeInfo.kubeletVersion; return this.status.nodeInfo.kubeletVersion;
} }
getOperatingSystem(): string { getOperatingSystem(): string {
const label = this.getLabels().find(label => label.startsWith("kubernetes.io/os=")) const label = this.getLabels().find(label => label.startsWith("kubernetes.io/os="));
if (label) { if (label) {
return label.split("=", 2)[1] return label.split("=", 2)[1];
} }
return "linux" return "linux";
} }
isUnschedulable() { isUnschedulable(): boolean {
return this.spec.unschedulable return this.spec.unschedulable;
} }
} }

View File

@ -1,11 +1,12 @@
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api"; import { MatchExpression } from "../workload-kube-object";
import { Metrics, metricsApi } from "./metrics.api";
import { Pod } from "./pods.api"; import { Pod } from "./pods.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaim> { export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaim> {
getMetrics(pvcName: string, namespace: string): Promise<IPvcMetrics> { getMetrics(pvcName: string, namespace: string): Promise<PvcMetrics> {
return metricsApi.getMetrics({ return metricsApi.getMetrics({
diskUsage: { category: 'pvc', pvc: pvcName }, diskUsage: { category: 'pvc', pvc: pvcName },
diskCapacity: { category: 'pvc', pvc: pvcName } diskCapacity: { category: 'pvc', pvc: pvcName }
@ -15,7 +16,7 @@ export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaim> {
} }
} }
export interface IPvcMetrics<T = IMetrics> { export interface PvcMetrics<T = Metrics> {
[key: string]: T; [key: string]: T;
diskUsage: T; diskUsage: T;
diskCapacity: T; diskCapacity: T;
@ -32,11 +33,7 @@ export class PersistentVolumeClaim extends KubeObject {
matchLabels: { matchLabels: {
release: string; release: string;
}; };
matchExpressions: { matchExpressions: MatchExpression[];
key: string; // environment,
operator: string; // In,
values: string[]; // [dev]
}[];
}; };
resources: { resources: {
requests: { requests: {
@ -49,34 +46,39 @@ export class PersistentVolumeClaim extends KubeObject {
} }
getPods(allPods: Pod[]): Pod[] { getPods(allPods: Pod[]): Pod[] {
const pods = allPods.filter(pod => pod.getNs() === this.getNs()) const pods = allPods.filter(pod => pod.getNs() === this.getNs());
return pods.filter(pod => { return pods.filter(pod => {
return pod.getVolumes().filter(volume => return pod.getVolumes().filter(volume =>
volume.persistentVolumeClaim && volume.persistentVolumeClaim &&
volume.persistentVolumeClaim.claimName === this.getName() volume.persistentVolumeClaim.claimName === this.getName()
).length > 0 ).length > 0;
}) });
} }
getStorage(): string { getStorage(): string {
if (!this.spec.resources || !this.spec.resources.requests) return "-"; if (!this.spec.resources || !this.spec.resources.requests) {
return "-";
}
return this.spec.resources.requests.storage; return this.spec.resources.requests.storage;
} }
getMatchLabels(): string[] { getMatchLabels(): string[] {
if (!this.spec.selector || !this.spec.selector.matchLabels) return []; if (!this.spec.selector || !this.spec.selector.matchLabels) {
return [];
}
return Object.entries(this.spec.selector.matchLabels) return Object.entries(this.spec.selector.matchLabels)
.map(([name, val]) => `${name}:${val}`); .map(([name, val]) => `${name}:${val}`);
} }
getMatchExpressions() { getMatchExpressions(): MatchExpression[] {
if (!this.spec.selector || !this.spec.selector.matchExpressions) return []; return this.spec.selector?.matchExpressions;
return this.spec.selector.matchExpressions;
} }
getStatus(): string { getStatus(): string {
if (this.status) return this.status.phase; if (this.status) {
return "-" return this.status.phase;
}
return "-";
} }
} }

View File

@ -43,23 +43,23 @@ export class PersistentVolume extends KubeObject {
reason?: string; reason?: string;
} }
getCapacity(inBytes = false) { getCapacity(inBytes = false): number | string {
const capacity = this.spec.capacity; const capacity = this.spec.capacity;
if (capacity) { if (capacity) {
if (inBytes) return unitsToBytes(capacity.storage) if (inBytes) {
return unitsToBytes(capacity.storage);
}
return capacity.storage; return capacity.storage;
} }
return 0; return 0;
} }
getStatus() { getStatus(): string {
if (!this.status) return; return this?.status.phase || "-";
return this.status.phase || "-";
} }
getClaimRefName() { getClaimRefName(): string {
const { claimRef } = this.spec; return this.spec.claimRef.name;
return claimRef ? claimRef.name : "";
} }
} }

View File

@ -1,17 +1,17 @@
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { Affinity, WorkloadKubeObject } from "../workload-kube-object";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api"; import { Metrics, metricsApi } from "./metrics.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export class PodsApi extends KubeApi<Pod> { export class PodsApi extends KubeApi<Pod> {
async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> { async getLogs(params: { namespace: string; name: string }, query?: PodLogsQuery): Promise<string> {
const path = this.getUrl(params) + "/log"; const path = this.getUrl(params) + "/log";
return this.request.get(path, { query }); return this.request.get(path, { query });
} }
getMetrics(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise<IPodMetrics> { getMetrics(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise<PodMetricsData> {
const podSelector = pods.map(pod => pod.getName()).join("|"); const podSelector = pods.map(pod => pod.getName()).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector } const opts = { category: "pods", pods: podSelector, namespace, selector };
return metricsApi.getMetrics({ return metricsApi.getMetrics({
cpuUsage: opts, cpuUsage: opts,
@ -29,7 +29,7 @@ export class PodsApi extends KubeApi<Pod> {
} }
} }
export interface IPodMetrics<T = IMetrics> { export interface PodMetricsData<T = Metrics> {
[metric: string]: T; [metric: string]: T;
cpuUsage: T; cpuUsage: T;
cpuRequests: T; cpuRequests: T;
@ -42,7 +42,7 @@ export interface IPodMetrics<T = IMetrics> {
networkTransmit: T; networkTransmit: T;
} }
export interface IPodLogsQuery { export interface PodLogsQuery {
container?: string; container?: string;
tailLines?: number; tailLines?: number;
timestamps?: boolean; timestamps?: boolean;
@ -58,7 +58,7 @@ export enum PodStatus {
EVICTED = "Evicted" EVICTED = "Evicted"
} }
export interface IPodContainer { export interface PodContainer {
name: string; name: string;
image: string; image: string;
command?: string[]; command?: string[];
@ -106,12 +106,12 @@ export interface IPodContainer {
readOnly: boolean; readOnly: boolean;
mountPath: string; mountPath: string;
}[]; }[];
livenessProbe?: IContainerProbe; livenessProbe?: ContainerProbe;
readinessProbe?: IContainerProbe; readinessProbe?: ContainerProbe;
imagePullPolicy: string; imagePullPolicy: string;
} }
interface IContainerProbe { interface ContainerProbe {
httpGet?: { httpGet?: {
path?: string; path?: string;
port: number; port: number;
@ -131,7 +131,7 @@ interface IContainerProbe {
failureThreshold?: number; failureThreshold?: number;
} }
export interface IPodContainerStatus { export interface PodContainerStatus {
name: string; name: string;
state: { state: {
[index: string]: object; [index: string]: object;
@ -173,14 +173,14 @@ export class Pod extends WorkloadKubeObject {
}; };
configMap: { configMap: {
name: string; name: string;
} };
secret: { secret: {
secretName: string; secretName: string;
defaultMode: number; defaultMode: number;
}; };
}[]; }[];
initContainers: IPodContainer[]; initContainers: PodContainer[];
containers: IPodContainer[]; containers: PodContainer[];
restartPolicy: string; restartPolicy: string;
terminationGracePeriodSeconds: number; terminationGracePeriodSeconds: number;
dnsPolicy: string; dnsPolicy: string;
@ -200,7 +200,7 @@ export class Pod extends WorkloadKubeObject {
effect: string; effect: string;
tolerationSeconds: number; tolerationSeconds: number;
}[]; }[];
affinity: IAffinity; affinity: Affinity;
} }
status: { status: {
phase: string; phase: string;
@ -213,34 +213,29 @@ export class Pod extends WorkloadKubeObject {
hostIP: string; hostIP: string;
podIP: string; podIP: string;
startTime: string; startTime: string;
initContainerStatuses?: IPodContainerStatus[]; initContainerStatuses?: PodContainerStatus[];
containerStatuses?: IPodContainerStatus[]; containerStatuses?: PodContainerStatus[];
qosClass: string; qosClass: string;
reason?: string; reason?: string;
} }
getInitContainers() { getAllContainers(): PodContainer[] {
return this.spec.initContainers || []; return this.spec.containers.concat(this.spec.initContainers);
} }
getContainers() { getRunningContainers(): PodContainer[] {
return this.spec.containers || []; const activeContainers = new Set(
this.getContainerStatuses()
.filter(({ state }) => !!state.running)
.map(({ name }) => name)
);
return this.getAllContainers()
.filter(({ name }) => activeContainers.has(name));
} }
getAllContainers() { getContainerStatuses(includeInitContainers = true): PodContainerStatus[] {
return this.getContainers().concat(this.getInitContainers()); const statuses: PodContainerStatus[] = [];
}
getRunningContainers() {
const statuses = this.getContainerStatuses()
return this.getAllContainers().filter(container => {
return statuses.find(status => status.name === container.name && !!status.state["running"])
}
)
}
getContainerStatuses(includeInitContainers = true) {
const statuses: IPodContainerStatus[] = [];
const { containerStatuses, initContainerStatuses } = this.status; const { containerStatuses, initContainerStatuses } = this.status;
if (containerStatuses) { if (containerStatuses) {
statuses.push(...containerStatuses); statuses.push(...containerStatuses);
@ -253,28 +248,22 @@ export class Pod extends WorkloadKubeObject {
getRestartsCount(): number { getRestartsCount(): number {
const { containerStatuses } = this.status; const { containerStatuses } = this.status;
if (!containerStatuses) return 0; if (!containerStatuses) {
return 0;
}
return containerStatuses.reduce((count, item) => count + item.restartCount, 0); return containerStatuses.reduce((count, item) => count + item.restartCount, 0);
} }
getQosClass() { getReason(): string {
return this.status.qosClass || "";
}
getReason() {
return this.status.reason || ""; return this.status.reason || "";
} }
getPriorityClassName() {
return this.spec.priorityClassName || "";
}
// Returns one of 5 statuses: Running, Succeeded, Pending, Failed, Evicted // Returns one of 5 statuses: Running, Succeeded, Pending, Failed, Evicted
getStatus() { getStatus(): PodStatus {
const phase = this.getStatusPhase(); const phase = this.status.phase;
const reason = this.getReason(); const reason = this.getReason();
const goodConditions = ["Initialized", "Ready"].every(condition => const goodConditions = ["Initialized", "Ready"].every(condition =>
!!this.getConditions().find(item => item.type === condition && item.status === "True") !!this.status.conditions.find(item => item.type === condition && item.status === "True")
); );
if (reason === PodStatus.EVICTED) { if (reason === PodStatus.EVICTED) {
return PodStatus.EVICTED; return PodStatus.EVICTED;
@ -292,9 +281,13 @@ export class Pod extends WorkloadKubeObject {
} }
// Returns pod phase or container error if occured // Returns pod phase or container error if occured
getStatusMessage() { getStatusMessage(): string {
if (this.getReason() === PodStatus.EVICTED) return "Evicted"; if (this.getReason() === PodStatus.EVICTED) {
if (this.getStatus() === PodStatus.RUNNING && this.metadata.deletionTimestamp) return "Terminating"; return "Evicted";
}
if (this.getStatus() === PodStatus.RUNNING && this.metadata.deletionTimestamp) {
return "Terminating";
}
let message = ""; let message = "";
const statuses = this.getContainerStatuses(false); // not including initContainers const statuses = this.getContainerStatuses(false); // not including initContainers
@ -309,74 +302,61 @@ export class Pod extends WorkloadKubeObject {
const { reason } = state.terminated; const { reason } = state.terminated;
message = reason ? reason : "Terminated"; message = reason ? reason : "Terminated";
} }
}) });
} }
if (message) return message; if (message) {
return this.getStatusPhase(); return message;
} }
getStatusPhase() {
return this.status.phase; return this.status.phase;
} }
getConditions() {
return this.status.conditions || [];
}
getVolumes() {
return this.spec.volumes || [];
}
getSecrets(): string[] { getSecrets(): string[] {
return this.getVolumes() return this.spec.volumes
.filter(vol => vol.secret) .filter(vol => vol.secret)
.map(vol => vol.secret.secretName); .map(vol => vol.secret.secretName);
} }
getNodeSelectors(): string[] { getNodeSelectors(): string[] {
const { nodeSelector } = this.spec const { nodeSelector } = this.spec;
if (!nodeSelector) return [] if (!nodeSelector) {
return Object.entries(nodeSelector).map(values => values.join(": ")) return [];
}
return Object.entries(nodeSelector).map(values => values.join(": "));
} }
getTolerations() { hasIssues(): boolean {
return this.spec.tolerations || [] const notReady = !!this.status.conditions.find(condition => {
} return condition.type == "Ready" && condition.status !== "True";
getAffinity(): IAffinity {
return this.spec.affinity
}
hasIssues() {
const notReady = !!this.getConditions().find(condition => {
return condition.type == "Ready" && condition.status !== "True"
}); });
const crashLoop = !!this.getContainerStatuses().find(condition => { const crashLoop = !!this.getContainerStatuses().find(condition => {
const waiting = condition.state.waiting const waiting = condition.state.waiting;
return (waiting && waiting.reason == "CrashLoopBackOff") return (waiting && waiting.reason == "CrashLoopBackOff");
}) });
return ( return (
notReady || notReady ||
crashLoop || crashLoop ||
this.getStatusPhase() !== "Running" this.status.phase !== "Running"
) );
} }
getLivenessProbe(container: IPodContainer) { getLivenessProbe(container: PodContainer): string[] {
return this.getProbe(container.livenessProbe); return this.getProbe(container.livenessProbe);
} }
getReadinessProbe(container: IPodContainer) { getReadinessProbe(container: PodContainer): string[] {
return this.getProbe(container.readinessProbe); return this.getProbe(container.readinessProbe);
} }
getProbe(probeData: IContainerProbe) { getProbe(probeData: ContainerProbe): string[] {
if (!probeData) return []; if (!probeData) {
return [];
}
const { const {
httpGet, exec, tcpSocket, initialDelaySeconds, timeoutSeconds, httpGet, exec, tcpSocket, initialDelaySeconds, timeoutSeconds,
periodSeconds, successThreshold, failureThreshold periodSeconds, successThreshold, failureThreshold
} = probeData; } = probeData;
const probe = []; const probe: string[] = [];
// HTTP Request // HTTP Request
if (httpGet) { if (httpGet) {
const { path, port, host, scheme } = httpGet; const { path, port, host, scheme } = httpGet;
@ -403,15 +383,8 @@ export class Pod extends WorkloadKubeObject {
return probe; return probe;
} }
getNodeName() { getSelectedNodeOs(): string | null {
return this.spec?.nodeName return this.spec?.nodeSelector?.["kubernetes.io/os"] || this.spec?.nodeSelector?.["beta.kubernetes.io/os"] || null;
}
getSelectedNodeOs() {
if (!this.spec.nodeSelector) return
if (!this.spec.nodeSelector["kubernetes.io/os"] && !this.spec.nodeSelector["beta.kubernetes.io/os"]) return
return this.spec.nodeSelector["kubernetes.io/os"] || this.spec.nodeSelector["beta.kubernetes.io/os"]
} }
} }

View File

@ -2,6 +2,14 @@ import { autobind } from "../../utils";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export interface Rules {
fsGroup: string;
runAsGroup: string;
runAsUser: string;
supplementalGroups: string;
seLinux: string;
}
@autobind() @autobind()
export class PodSecurityPolicy extends KubeObject { export class PodSecurityPolicy extends KubeObject {
static kind = "PodSecurityPolicy" static kind = "PodSecurityPolicy"
@ -66,22 +74,22 @@ export class PodSecurityPolicy extends KubeObject {
volumes?: string[]; volumes?: string[];
} }
isPrivileged() { isPrivileged(): boolean {
return !!this.spec.privileged; return !!this.spec.privileged;
} }
getVolumes() { getVolumes(): string[] {
return this.spec.volumes || []; return this.spec.volumes || [];
} }
getRules() { getRules(): Rules {
const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec; const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec;
return { return {
fsGroup: fsGroup ? fsGroup.rule : "", fsGroup: fsGroup?.rule || "",
runAsGroup: runAsGroup ? runAsGroup.rule : "", runAsGroup: runAsGroup?.rule || "",
runAsUser: runAsUser ? runAsUser.rule : "", runAsUser: runAsUser?.rule || "",
supplementalGroups: supplementalGroups ? supplementalGroups.rule : "", supplementalGroups: supplementalGroups?.rule || "",
seLinux: seLinux ? seLinux.rule : "", seLinux: seLinux?.rule || "",
}; };
} }
} }

View File

@ -1,7 +1,7 @@
import get from "lodash/get"; import get from "lodash/get";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { Affinity, WorkloadKubeObject } from "../workload-kube-object";
import { IPodContainer } from "./pods.api"; import { PodContainer } from "./pods.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
@autobind() @autobind()
@ -15,10 +15,10 @@ export class ReplicaSet extends WorkloadKubeObject {
[key: string]: string; [key: string]: string;
}; };
}; };
containers?: IPodContainer[]; containers?: PodContainer[];
template?: { template?: {
spec?: { spec?: {
affinity?: IAffinity; affinity?: Affinity;
nodeSelector?: { nodeSelector?: {
[selector: string]: string; [selector: string]: string;
}; };
@ -28,7 +28,7 @@ export class ReplicaSet extends WorkloadKubeObject {
effect: string; effect: string;
tolerationSeconds: number; tolerationSeconds: number;
}[]; }[];
containers: IPodContainer[]; containers: PodContainer[];
}; };
}; };
restartPolicy?: string; restartPolicy?: string;
@ -44,9 +44,9 @@ export class ReplicaSet extends WorkloadKubeObject {
observedGeneration: number; observedGeneration: number;
} }
getImages() { getImages(): string[] {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []) const containers: PodContainer[] = get(this, "spec.template.spec.containers", []);
return [...containers].map(container => container.image) return containers.map(container => container.image);
} }
} }

View File

@ -1,4 +1,4 @@
import jsYaml from "js-yaml" import jsYaml from "js-yaml";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeJsonApiData } from "../kube-json-api"; import { KubeJsonApiData } from "../kube-json-api";
import { apiKubeResourceApplier } from "../index"; import { apiKubeResourceApplier } from "../index";

View File

@ -2,7 +2,7 @@ import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { KubeJsonApiData } from "../kube-json-api"; import { KubeJsonApiData } from "../kube-json-api";
export interface IResourceQuotaValues { export interface ResourceQuotaValues {
[quota: string]: string; [quota: string]: string;
// Compute Resource Quota // Compute Resource Quota
@ -30,33 +30,34 @@ export interface IResourceQuotaValues {
"count/deployments.extensions"?: string; "count/deployments.extensions"?: string;
} }
interface MatchExpression {
operator: string;
scopeName: string;
values: string[];
}
export class ResourceQuota extends KubeObject { export class ResourceQuota extends KubeObject {
static kind = "ResourceQuota" static kind = "ResourceQuota"
constructor(data: KubeJsonApiData) { constructor(data: KubeJsonApiData) {
super(data); super(data);
this.spec = this.spec || {} as any this.spec = this.spec || {} as any;
} }
spec: { spec: {
hard: IResourceQuotaValues; hard: ResourceQuotaValues;
scopeSelector?: { scopeSelector?: {
matchExpressions: { matchExpressions: MatchExpression[];
operator: string;
scopeName: string;
values: string[];
}[];
}; };
} }
status: { status: {
hard: IResourceQuotaValues; hard: ResourceQuotaValues;
used: IResourceQuotaValues; used: ResourceQuotaValues;
} }
getScopeSelector() { getScopeSelector(): MatchExpression[] {
const { matchExpressions = [] } = this.spec.scopeSelector || {}; return this.spec?.scopeSelector?.matchExpressions;
return matchExpressions;
} }
} }

View File

@ -2,7 +2,7 @@ import { autobind } from "../../utils";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export interface IRoleBindingSubject { export interface RoleBindingSubject {
kind: string; kind: string;
name: string; name: string;
namespace?: string; namespace?: string;
@ -13,19 +13,19 @@ export interface IRoleBindingSubject {
export class RoleBinding extends KubeObject { export class RoleBinding extends KubeObject {
static kind = "RoleBinding" static kind = "RoleBinding"
subjects?: IRoleBindingSubject[] subjects?: RoleBindingSubject[]
roleRef: { roleRef: {
kind: string; kind: string;
name: string; name: string;
apiGroup?: string; apiGroup?: string;
} }
getSubjects() { getSubjects(): RoleBindingSubject[] {
return this.subjects || []; return this.subjects || [];
} }
getSubjectNames(): string { getSubjectNames(): string {
return this.getSubjects().map(subject => subject.name).join(", ") return this.getSubjects().map(subject => subject.name).join(", ");
} }
} }

View File

@ -1,18 +1,20 @@
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export interface Rule {
verbs: string[];
apiGroups: string[];
resources: string[];
resourceNames?: string[];
}
export class Role extends KubeObject { export class Role extends KubeObject {
static kind = "Role" static kind = "Role"
rules: { rules: Rule[]
verbs: string[];
apiGroups: string[];
resources: string[];
resourceNames?: string[];
}[]
getRules() { getRules(): Rule[] {
return this.rules || []; return this.rules;
} }
} }

View File

@ -14,7 +14,7 @@ export enum SecretType {
BootstrapToken = "bootstrap.kubernetes.io/token", BootstrapToken = "bootstrap.kubernetes.io/token",
} }
export interface ISecretRef { export interface SecretRef {
key?: string; key?: string;
name: string; name: string;
} }
@ -37,10 +37,6 @@ export class Secret extends KubeObject {
getKeys(): string[] { getKeys(): string[] {
return Object.keys(this.data); return Object.keys(this.data);
} }
getToken() {
return this.data.token;
}
} }
export const secretsApi = new KubeApi({ export const secretsApi = new KubeApi({

View File

@ -12,7 +12,7 @@ export class SelfSubjectRulesReviewApi extends KubeApi<SelfSubjectRulesReview> {
} }
} }
export interface ISelfSubjectReviewRule { export interface SelfSubjectReviewRule {
verbs: string[]; verbs: string[];
apiGroups?: string[]; apiGroups?: string[];
resources?: string[]; resources?: string[];
@ -29,22 +29,22 @@ export class SelfSubjectRulesReview extends KubeObject {
} }
status: { status: {
resourceRules: ISelfSubjectReviewRule[]; resourceRules: SelfSubjectReviewRule[];
nonResourceRules: ISelfSubjectReviewRule[]; nonResourceRules: SelfSubjectReviewRule[];
incomplete: boolean; incomplete: boolean;
} }
getResourceRules() { getResourceRules(): SelfSubjectReviewRule[] {
const rules = this.status && this.status.resourceRules || []; const rules = this.status && this.status.resourceRules || [];
return rules.map(rule => this.normalize(rule)); return rules.map(rule => this.normalize(rule));
} }
getNonResourceRules() { getNonResourceRules(): SelfSubjectReviewRule[] {
const rules = this.status && this.status.nonResourceRules || []; const rules = this.status && this.status.nonResourceRules || [];
return rules.map(rule => this.normalize(rule)); return rules.map(rule => this.normalize(rule));
} }
protected normalize(rule: ISelfSubjectReviewRule): ISelfSubjectReviewRule { protected normalize(rule: SelfSubjectReviewRule): SelfSubjectReviewRule {
const { apiGroups = [], resourceNames = [], verbs = [], nonResourceURLs = [], resources = [] } = rule; const { apiGroups = [], resourceNames = [], verbs = [], nonResourceURLs = [], resources = [] } = rule;
return { return {
apiGroups, apiGroups,
@ -56,7 +56,7 @@ export class SelfSubjectRulesReview extends KubeObject {
const separator = apiGroup == "" ? "" : "."; const separator = apiGroup == "" ? "" : ".";
return resource + separator + apiGroup; return resource + separator + apiGroup;
}) })
} };
} }
} }

View File

@ -2,22 +2,26 @@ import { autobind } from "../../utils";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export interface ImagePullSecret {
name: string;
}
export interface Secret {
name: string;
}
@autobind() @autobind()
export class ServiceAccount extends KubeObject { export class ServiceAccount extends KubeObject {
static kind = "ServiceAccount"; static kind = "ServiceAccount";
secrets?: { secrets?: Secret[]
name: string; imagePullSecrets?: ImagePullSecret[]
}[]
imagePullSecrets?: {
name: string;
}[]
getSecrets() { getSecrets(): Secret[] {
return this.secrets || []; return this.secrets || [];
} }
getImagePullSecrets() { getImagePullSecrets(): ImagePullSecret[] {
return this.imagePullSecrets || []; return this.imagePullSecrets || [];
} }
} }

View File

@ -2,25 +2,25 @@ import { autobind } from "../../utils";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export interface IServicePort { export interface ServicePort {
name?: string; name?: string;
protocol: string; protocol: string;
port: number; port: number;
targetPort: number; targetPort: number;
} }
export class ServicePort implements IServicePort { export class ServicePort implements ServicePort {
name?: string; name?: string;
protocol: string; protocol: string;
port: number; port: number;
targetPort: number; targetPort: number;
nodePort?: number; nodePort?: number;
constructor(data: IServicePort) { constructor(data: ServicePort) {
Object.assign(this, data) Object.assign(this, data);
} }
toString() { toString(): string {
if (this.nodePort) { if (this.nodePort) {
return `${this.port}:${this.nodePort}/${this.protocol}`; return `${this.port}:${this.nodePort}/${this.protocol}`;
} else { } else {
@ -29,6 +29,13 @@ export class ServicePort implements IServicePort {
} }
} }
export interface LoadBalancer {
ingress?: {
ip?: string;
hostname?: string;
}[];
}
@autobind() @autobind()
export class Service extends KubeObject { export class Service extends KubeObject {
static kind = "Service" static kind = "Service"
@ -45,32 +52,23 @@ export class Service extends KubeObject {
} }
status: { status: {
loadBalancer?: { loadBalancer?: LoadBalancer;
ingress?: {
ip?: string;
hostname?: string;
}[];
};
} }
getClusterIp() { getExternalIps(): string[] {
return this.spec.clusterIP; return this.status.loadBalancer?.ingress?.map((val): string => val.ip || val.hostname || "")
|| this.spec.externalIPs
|| [];
} }
getExternalIps() { getType(): string {
const lb = this.getLoadBalancer();
if (lb && lb.ingress) {
return lb.ingress.map(val => val.ip || val.hostname)
}
return this.spec.externalIPs || [];
}
getType() {
return this.spec.type || "-"; return this.spec.type || "-";
} }
getSelector(): string[] { getSelector(): string[] {
if (!this.spec.selector) return []; if (!this.spec.selector) {
return [];
}
return Object.entries(this.spec.selector).map(val => val.join("=")); return Object.entries(this.spec.selector).map(val => val.join("="));
} }
@ -79,15 +77,11 @@ export class Service extends KubeObject {
return ports.map(p => new ServicePort(p)); return ports.map(p => new ServicePort(p));
} }
getLoadBalancer() { isActive(): boolean {
return this.status.loadBalancer;
}
isActive() {
return this.getType() !== "LoadBalancer" || this.getExternalIps().length > 0; return this.getType() !== "LoadBalancer" || this.getExternalIps().length > 0;
} }
getStatus() { getStatus(): "Active" | "Pending" {
return this.isActive() ? "Active" : "Pending"; return this.isActive() ? "Active" : "Pending";
} }
} }

View File

@ -1,6 +1,6 @@
import get from "lodash/get"; import get from "lodash/get";
import { IPodContainer } from "./pods.api"; import { PodContainer } from "./pods.api";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { Affinity, WorkloadKubeObject } from "../workload-kube-object";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
@ -35,7 +35,7 @@ export class StatefulSet extends WorkloadKubeObject {
mountPath: string; mountPath: string;
}[]; }[];
}[]; }[];
affinity?: IAffinity; affinity?: Affinity;
nodeSelector?: { nodeSelector?: {
[selector: string]: string; [selector: string]: string;
}; };
@ -70,9 +70,9 @@ export class StatefulSet extends WorkloadKubeObject {
collisionCount: number; collisionCount: number;
} }
getImages() { getImages(): string[] {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []) const containers: PodContainer[] = get(this, "spec.template.spec.containers", []);
return [...containers].map(container => container.image) return containers.map(container => container.image);
} }
} }

View File

@ -14,20 +14,20 @@ export class StorageClass extends KubeObject {
[param: string]: string; // every provisioner has own set of these parameters [param: string]: string; // every provisioner has own set of these parameters
} }
isDefault() { isDefault(): boolean {
const annotations = this.metadata.annotations || {}; const annotations = this.metadata.annotations || {};
return ( return (
annotations["storageclass.kubernetes.io/is-default-class"] === "true" || annotations["storageclass.kubernetes.io/is-default-class"] === "true" ||
annotations["storageclass.beta.kubernetes.io/is-default-class"] === "true" annotations["storageclass.beta.kubernetes.io/is-default-class"] === "true"
) );
} }
getVolumeBindingMode() { getVolumeBindingMode(): string {
return this.volumeBindingMode || "-" return this.volumeBindingMode || "-";
} }
getReclaimPolicy() { getReclaimPolicy(): string {
return this.reclaimPolicy || "-" return this.reclaimPolicy || "-";
} }
} }

View File

@ -27,7 +27,7 @@ export const apiKubeResourceApplier = new KubeJsonApi({
}); });
// Common handler for HTTP api errors // Common handler for HTTP api errors
function onApiError(error: JsonApiErrorParsed, res: Response) { function onApiError(error: JsonApiErrorParsed, res: Response): void {
switch (res.status) { switch (res.status) {
case 403: case 403:
error.isUsedForNotification = true; error.isUsedForNotification = true;

View File

@ -2,7 +2,7 @@
import { stringify } from "querystring"; import { stringify } from "querystring";
import { EventEmitter } from "../utils/eventEmitter"; import { EventEmitter } from "../utils/eventEmitter";
import { cancelableFetch } from "../utils/cancelableFetch"; import { cancelableFetch, CancelablePromise } from "../utils/cancelableFetch";
export interface JsonApiData { export interface JsonApiData {
} }
@ -31,6 +31,21 @@ export interface JsonApiConfig {
debug?: boolean; debug?: boolean;
} }
export class JsonApiErrorParsed {
isUsedForNotification = false;
constructor(private error: JsonApiError | DOMException, private messages: string[]) {
}
get isAborted(): boolean {
return this.error.code === DOMException.ABORT_ERR;
}
toString(): string {
return this.messages.join("\n");
}
}
export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> { export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
static reqInitDefault: RequestInit = { static reqInitDefault: RequestInit = {
headers: { headers: {
@ -51,30 +66,30 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
public onData = new EventEmitter<[D, Response]>(); public onData = new EventEmitter<[D, Response]>();
public onError = new EventEmitter<[JsonApiErrorParsed, Response]>(); public onError = new EventEmitter<[JsonApiErrorParsed, Response]>();
get<T = D>(path: string, params?: P, reqInit: RequestInit = {}) { get<T = D>(path: string, params?: P, reqInit: RequestInit = {}): CancelablePromise<T> {
return this.request<T>(path, params, { ...reqInit, method: "get" }); return this.request<T>(path, params, { ...reqInit, method: "get" });
} }
post<T = D>(path: string, params?: P, reqInit: RequestInit = {}) { post<T = D>(path: string, params?: P, reqInit: RequestInit = {}): CancelablePromise<T> {
return this.request<T>(path, params, { ...reqInit, method: "post" }); return this.request<T>(path, params, { ...reqInit, method: "post" });
} }
put<T = D>(path: string, params?: P, reqInit: RequestInit = {}) { put<T = D>(path: string, params?: P, reqInit: RequestInit = {}): CancelablePromise<T> {
return this.request<T>(path, params, { ...reqInit, method: "put" }); return this.request<T>(path, params, { ...reqInit, method: "put" });
} }
patch<T = D>(path: string, params?: P, reqInit: RequestInit = {}) { patch<T = D>(path: string, params?: P, reqInit: RequestInit = {}): CancelablePromise<T> {
return this.request<T>(path, params, { ...reqInit, method: "patch" }); return this.request<T>(path, params, { ...reqInit, method: "patch" });
} }
del<T = D>(path: string, params?: P, reqInit: RequestInit = {}) { del<T = D>(path: string, params?: P, reqInit: RequestInit = {}): CancelablePromise<T> {
return this.request<T>(path, params, { ...reqInit, method: "delete" }); return this.request<T>(path, params, { ...reqInit, method: "delete" });
} }
protected request<D>(path: string, params?: P, init: RequestInit = {}) { protected request<D>(path: string, params?: P, init: RequestInit = {}): CancelablePromise<D> {
let reqUrl = this.config.apiPrefix + path; let reqUrl = this.config.apiPrefix + path;
const reqInit: RequestInit = { ...this.reqInit, ...init }; const reqInit: RequestInit = { ...this.reqInit, ...init };
const { data, query } = params || {} as P; const { data, query } = params || {};
if (data && !reqInit.body) { if (data && !reqInit.body) {
reqInit.body = JSON.stringify(data); reqInit.body = JSON.stringify(data);
} }
@ -110,46 +125,35 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
} else { } else {
const error = new JsonApiErrorParsed(data, this.parseError(data, res)); const error = new JsonApiErrorParsed(data, this.parseError(data, res));
this.onError.emit(error, res); this.onError.emit(error, res);
this.writeLog({ ...log, error }) this.writeLog({ ...log, error });
throw error; throw error;
} }
}) });
} }
protected parseError(error: JsonApiError | string, res: Response): string[] { protected parseError(error: JsonApiError | string, res: Response): string[] {
if (typeof error === "string") { if (typeof error === "string") {
return [error] return [error];
} else if (Array.isArray(error.errors)) {
return error.errors.map(error => error.title);
} else if (error.message) {
return [error.message];
} }
else if (Array.isArray(error.errors)) { return [res.statusText || "Error!"];
return error.errors.map(error => error.title)
}
else if (error.message) {
return [error.message]
}
return [res.statusText || "Error!"]
} }
protected writeLog(log: JsonApiLog) { protected writeLog(log: JsonApiLog): void {
if (!this.config.debug) return; if (!this.config.debug) {
return;
}
const { method, reqUrl, ...params } = log; const { method, reqUrl, ...params } = log;
let textStyle = 'font-weight: bold;'; let textStyle = 'font-weight: bold;';
if (params.data) textStyle += 'background: green; color: white;'; if (params.data) {
if (params.error) textStyle += 'background: red; color: white;'; textStyle += 'background: green; color: white;';
}
if (params.error) {
textStyle += 'background: red; color: white;';
}
console.log(`%c${method} ${reqUrl}`, textStyle, params); console.log(`%c${method} ${reqUrl}`, textStyle, params);
} }
} }
export class JsonApiErrorParsed {
isUsedForNotification = false;
constructor(private error: JsonApiError | DOMException, private messages: string[]) {
}
get isAborted() {
return this.error.code === DOMException.ABORT_ERR;
}
toString() {
return this.messages.join("\n");
}
}

View File

@ -1,15 +1,16 @@
// Base class for building all kubernetes apis // Base class for building all kubernetes apis
import merge from "lodash/merge" import merge from "lodash/merge";
import { stringify } from "querystring"; import { stringify } from "querystring";
import { IKubeObjectConstructor, KubeObject } from "./kube-object"; import { IKubeObjectConstructor, KubeObject } from "./kube-object";
import { IKubeObjectRef, KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api"; import { KubeObjectRef, KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api";
import { apiKube } from "./index"; import { apiKube } from "./index";
import { kubeWatchApi } from "./kube-watch-api"; import { kubeWatchApi } from "./kube-watch-api";
import { apiManager } from "./api-manager"; import { apiManager } from "./api-manager";
import { split } from "../utils/arrays"; import { split } from "../utils/arrays";
import { CancelablePromise } from "client/utils/cancelableFetch";
export interface IKubeApiOptions<T extends KubeObject> { export interface KubeApiOptions<T extends KubeObject> {
kind: string; // resource type within api-group, e.g. "Namespace" kind: string; // resource type within api-group, e.g. "Namespace"
apiBase: string; // base api-path for listing all resources, e.g. "/api/v1/pods" apiBase: string; // base api-path for listing all resources, e.g. "/api/v1/pods"
isNamespaced: boolean; isNamespaced: boolean;
@ -17,7 +18,7 @@ export interface IKubeApiOptions<T extends KubeObject> {
request?: KubeJsonApi; request?: KubeJsonApi;
} }
export interface IKubeApiQueryParams { export interface KubeApiQueryParams {
watch?: boolean | number; watch?: boolean | number;
resourceVersion?: string; resourceVersion?: string;
timeoutSeconds?: number; timeoutSeconds?: number;
@ -25,7 +26,7 @@ export interface IKubeApiQueryParams {
continue?: string; // might be used with ?limit from second request continue?: string; // might be used with ?limit from second request
} }
export interface IKubeApiLinkRef { export interface KubeApiLinkRef {
apiPrefix?: string; apiPrefix?: string;
apiVersion: string; apiVersion: string;
resource: string; resource: string;
@ -33,14 +34,14 @@ export interface IKubeApiLinkRef {
namespace?: string; namespace?: string;
} }
export interface IKubeApiLinkBase extends IKubeApiLinkRef { export interface KubeApiLinkBase extends KubeApiLinkRef {
apiBase: string; apiBase: string;
apiGroup: string; apiGroup: string;
apiVersionWithGroup: string; apiVersionWithGroup: string;
} }
export class KubeApi<T extends KubeObject = any> { export class KubeApi<T extends KubeObject = any> {
static parseApi(apiPath = ""): IKubeApiLinkBase { static parseApi(apiPath = ""): KubeApiLinkBase {
apiPath = new URL(apiPath, location.origin).pathname; apiPath = new URL(apiPath, location.origin).pathname;
const [, prefix, ...parts] = apiPath.split("/"); const [, prefix, ...parts] = apiPath.split("/");
const apiPrefix = `/${prefix}`; const apiPrefix = `/${prefix}`;
@ -91,11 +92,11 @@ export class KubeApi<T extends KubeObject = any> {
*/ */
if (left[0].includes('.') || left[1].match(/^v[0-9]/)) { if (left[0].includes('.') || left[1].match(/^v[0-9]/)) {
[apiGroup, apiVersion] = left; [apiGroup, apiVersion] = left;
resource = left.slice(2).join("/") resource = left.slice(2).join("/");
} else { } else {
apiGroup = ""; apiGroup = "";
apiVersion = left[0]; apiVersion = left[0];
[resource, name] = left.slice(1) [resource, name] = left.slice(1);
} }
break; break;
} }
@ -105,7 +106,7 @@ export class KubeApi<T extends KubeObject = any> {
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/"); const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
if (!apiBase) { if (!apiBase) {
throw new Error(`invalid apiPath: ${apiPath}`) throw new Error(`invalid apiPath: ${apiPath}`);
} }
return { return {
@ -116,20 +117,20 @@ export class KubeApi<T extends KubeObject = any> {
}; };
} }
static createLink(ref: IKubeApiLinkRef): string { static createLink(ref: KubeApiLinkRef): string {
const { apiPrefix = "/apis", resource, apiVersion, name } = ref; const { apiPrefix = "/apis", resource, apiVersion, name } = ref;
let { namespace } = ref; let { namespace } = ref;
if (namespace) { if (namespace) {
namespace = `namespaces/${namespace}` namespace = `namespaces/${namespace}`;
} }
return [apiPrefix, apiVersion, namespace, resource, name] return [apiPrefix, apiVersion, namespace, resource, name]
.filter(v => v) .filter(v => v)
.join("/") .join("/");
} }
static watchAll(...apis: KubeApi[]) { static watchAll(...apis: KubeApi[]): () => void {
const disposers = apis.map(api => api.watch()); const disposers = apis.map(api => api.watch());
return () => disposers.forEach(unwatch => unwatch()); return (): void => disposers.forEach(unwatch => unwatch());
} }
readonly kind: string readonly kind: string
@ -145,7 +146,7 @@ export class KubeApi<T extends KubeObject = any> {
protected request: KubeJsonApi; protected request: KubeJsonApi;
protected resourceVersions = new Map<string, string>(); protected resourceVersions = new Map<string, string>();
constructor(protected options: IKubeApiOptions<T>) { constructor(protected options: KubeApiOptions<T>) {
const { const {
kind, kind,
isNamespaced = false, isNamespaced = false,
@ -169,19 +170,19 @@ export class KubeApi<T extends KubeObject = any> {
apiManager.registerApi(apiBase, this); apiManager.registerApi(apiBase, this);
} }
setResourceVersion(namespace = "", newVersion: string) { setResourceVersion(namespace = "", newVersion: string): void {
this.resourceVersions.set(namespace, newVersion); this.resourceVersions.set(namespace, newVersion);
} }
getResourceVersion(namespace = "") { getResourceVersion(namespace = ""): string {
return this.resourceVersions.get(namespace); return this.resourceVersions.get(namespace);
} }
async refreshResourceVersion(params?: { namespace: string }) { async refreshResourceVersion(params?: { namespace: string }): Promise<T[]> {
return this.list(params, { limit: 1 }); return this.list(params, { limit: 1 });
} }
getUrl({ name = "", namespace = "" } = {}, query?: Partial<IKubeApiQueryParams>) { getUrl({ name = "", namespace = "" } = {}, query?: Partial<KubeApiQueryParams>): string {
const { apiPrefix, apiVersionWithGroup, apiResource } = this; const { apiPrefix, apiVersionWithGroup, apiResource } = this;
const resourcePath = KubeApi.createLink({ const resourcePath = KubeApi.createLink({
apiPrefix: apiPrefix, apiPrefix: apiPrefix,
@ -208,7 +209,7 @@ export class KubeApi<T extends KubeObject = any> {
kind: this.kind, kind: this.kind,
apiVersion: apiVersion, apiVersion: apiVersion,
...item, ...item,
})) }));
} }
// custom apis might return array for list response, e.g. users, groups, etc. // custom apis might return array for list response, e.g. users, groups, etc.
@ -219,13 +220,13 @@ export class KubeApi<T extends KubeObject = any> {
return data; return data;
} }
async list({ namespace = "" } = {}, query?: IKubeApiQueryParams): Promise<T[]> { async list({ namespace = "" } = {}, query?: KubeApiQueryParams): Promise<T[]> {
return this.request return this.request
.get(this.getUrl({ namespace }), { query }) .get(this.getUrl({ namespace }), { query })
.then(data => this.parseResponse(data, namespace)); .then(data => this.parseResponse(data, namespace));
} }
async get({ name = "", namespace = "default" } = {}, query?: IKubeApiQueryParams): Promise<T> { async get({ name = "", namespace = "default" } = {}, query?: KubeApiQueryParams): Promise<T> {
return this.request return this.request
.get(this.getUrl({ namespace, name }), { query }) .get(this.getUrl({ namespace, name }), { query })
.then(this.parseResponse); .then(this.parseResponse);
@ -252,20 +253,20 @@ export class KubeApi<T extends KubeObject = any> {
const apiUrl = this.getUrl({ namespace, name }); const apiUrl = this.getUrl({ namespace, name });
return this.request return this.request
.put(apiUrl, { data }) .put(apiUrl, { data })
.then(this.parseResponse) .then(this.parseResponse);
} }
async delete({ name = "", namespace = "default" }) { delete({ name = "", namespace = "default" }): CancelablePromise<KubeJsonApiData> {
const apiUrl = this.getUrl({ namespace, name }); const apiUrl = this.getUrl({ namespace, name });
return this.request.del(apiUrl) return this.request.del(apiUrl);
} }
getWatchUrl(namespace = "", query: IKubeApiQueryParams = {}) { getWatchUrl(namespace = "", query: KubeApiQueryParams = {}): string {
return this.getUrl({ namespace }, { return this.getUrl({ namespace }, {
watch: 1, watch: 1,
resourceVersion: this.getResourceVersion(namespace), resourceVersion: this.getResourceVersion(namespace),
...query, ...query,
}) });
} }
watch(): () => void { watch(): () => void {
@ -273,16 +274,16 @@ export class KubeApi<T extends KubeObject = any> {
} }
} }
export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): string { export function lookupApiLink(ref: KubeObjectRef, parentObject: KubeObject): string {
const { const {
kind, apiVersion, name, kind, apiVersion, name,
namespace = parentObject.getNs() namespace = parentObject.getNs()
} = ref; } = ref;
// search in registered apis by 'kind' & 'apiVersion' // search in registered apis by 'kind' & 'apiVersion'
const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == apiVersion) const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == apiVersion);
if (api) { if (api) {
return api.getUrl({ namespace, name }) return api.getUrl({ namespace, name });
} }
// lookup api by generated resource link // lookup api by generated resource link
@ -298,10 +299,10 @@ export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): st
// resolve by kind only (hpa's might use refs to older versions of resources for example) // resolve by kind only (hpa's might use refs to older versions of resources for example)
const apiByKind = apiManager.getApi(api => api.kind === kind); const apiByKind = apiManager.getApi(api => api.kind === kind);
if (apiByKind) { if (apiByKind) {
return apiByKind.getUrl({ name, namespace }) return apiByKind.getUrl({ name, namespace });
} }
// otherwise generate link with default prefix // otherwise generate link with default prefix
// resource still might exists in k8s, but api is not registered in the app // resource still might exists in k8s, but api is not registered in the app
return KubeApi.createLink({ apiVersion, name, namespace, resource }) return KubeApi.createLink({ apiVersion, name, namespace, resource });
} }

View File

@ -31,7 +31,7 @@ export interface KubeJsonApiData extends JsonApiData {
}; };
} }
export interface IKubeObjectRef { export interface KubeObjectRef {
kind: string; kind: string;
apiVersion: string; apiVersion: string;
name: string; name: string;
@ -49,7 +49,7 @@ export interface KubeJsonApiError extends JsonApiError {
}; };
} }
export interface IKubeJsonApiQuery { export interface KubeJsonApiQuery {
watch?: any; watch?: any;
resourceVersion?: string; resourceVersion?: string;
timeoutSeconds?: number; timeoutSeconds?: number;

View File

@ -7,12 +7,13 @@ import { ItemObject } from "../item.store";
import { apiKube } from "./index"; import { apiKube } from "./index";
import { JsonApiParams } from "./json-api"; import { JsonApiParams } from "./json-api";
import { resourceApplierApi } from "./endpoints/resource-applier.api"; import { resourceApplierApi } from "./endpoints/resource-applier.api";
import { CancelablePromise } from "client/utils/cancelableFetch";
export type IKubeObjectConstructor<T extends KubeObject = any> = (new (data: KubeJsonApiData | any) => T) & { export type IKubeObjectConstructor<T extends KubeObject = any> = (new (data: KubeJsonApiData | any) => T) & {
kind?: string; kind?: string;
}; };
export interface IKubeObjectMetadata { export interface KubeObjectMetadata {
uid: string; uid: string;
name: string; name: string;
namespace?: string; namespace?: string;
@ -44,11 +45,7 @@ export type IKubeMetaField = keyof KubeObject["metadata"];
export class KubeObject implements ItemObject { export class KubeObject implements ItemObject {
static readonly kind: string; static readonly kind: string;
static create(data: any) { static isNonSystem(item: KubeJsonApiData | KubeObject): boolean {
return new KubeObject(data);
}
static isNonSystem(item: KubeJsonApiData | KubeObject) {
return !item.metadata.name.startsWith("system:"); return !item.metadata.name.startsWith("system:");
} }
@ -61,8 +58,10 @@ export class KubeObject implements ItemObject {
} }
static stringifyLabels(labels: { [name: string]: string }): string[] { static stringifyLabels(labels: { [name: string]: string }): string[] {
if (!labels) return []; if (!labels) {
return Object.entries(labels).map(([name, value]) => `${name}=${value}`) return [];
}
return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
} }
constructor(data: KubeJsonApiData) { constructor(data: KubeJsonApiData) {
@ -71,31 +70,27 @@ export class KubeObject implements ItemObject {
apiVersion: string apiVersion: string
kind: string kind: string
metadata: IKubeObjectMetadata; metadata: KubeObjectMetadata;
get selfLink() { get selfLink(): string {
return this.metadata.selfLink return this.metadata.selfLink;
} }
getId() { getId(): string {
return this.metadata.uid; return this.metadata.uid;
} }
getResourceVersion() { getName(): string {
return this.metadata.resourceVersion;
}
getName() {
return this.metadata.name; return this.metadata.name;
} }
getNs() { getNs(): string | undefined {
// avoid "null" serialization via JSON.stringify when post data // avoid "null" serialization via JSON.stringify when post data
return this.metadata.namespace || undefined; return this.metadata.namespace || undefined;
} }
// todo: refactor with named arguments // todo: refactor with named arguments
getAge(humanize = true, compact = true, fromNow = false) { getAge(humanize = true, compact = true, fromNow = false): number | string {
if (fromNow) { if (fromNow) {
return moment(this.metadata.creationTimestamp).fromNow(); return moment(this.metadata.creationTimestamp).fromNow();
} }
@ -119,26 +114,26 @@ export class KubeObject implements ItemObject {
return labels.filter(label => { return labels.filter(label => {
const skip = resourceApplierApi.annotations.some(key => label.startsWith(key)); const skip = resourceApplierApi.annotations.some(key => label.startsWith(key));
return !skip; return !skip;
}) });
} }
getOwnerRefs() { getOwnerRefs(): Required<KubeObjectMetadata["ownerReferences"]> {
const refs = this.metadata.ownerReferences || []; const refs = this.metadata.ownerReferences || [];
return refs.map(ownerRef => ({ return refs.map(ownerRef => ({
...ownerRef, ...ownerRef,
namespace: this.getNs(), namespace: this.getNs(),
})) }));
} }
getSearchFields() { getSearchFields(): string[] {
const { getName, getId, getNs, getAnnotations, getLabels } = this const { getName, getId, getNs, getAnnotations, getLabels } = this;
return [ return [
getName(), getName(),
getNs(), getNs(),
getId(), getId(),
...getLabels(), ...getLabels(),
...getAnnotations(), ...getAnnotations(),
] ];
} }
toPlainObject(): object { toPlainObject(): object {
@ -146,14 +141,14 @@ export class KubeObject implements ItemObject {
} }
// use unified resource-applier api for updating all k8s objects // use unified resource-applier api for updating all k8s objects
async update<T extends KubeObject>(data: Partial<T>) { async update<T extends KubeObject>(data: Partial<T>): Promise<T> {
return resourceApplierApi.update<T>({ return resourceApplierApi.update<T>({
...this.toPlainObject(), ...this.toPlainObject(),
...data, ...data,
}); });
} }
delete(params?: JsonApiParams) { delete(params?: JsonApiParams): CancelablePromise<any> {
return apiKube.del(this.selfLink, params); return apiKube.del(this.selfLink, params);
} }
} }

View File

@ -1,7 +1,7 @@
// Kubernetes watch-api consumer // Kubernetes watch-api consumer
import { computed, observable, reaction } from "mobx"; import { computed, observable, reaction } from "mobx";
import { stringify } from "querystring" import { stringify } from "querystring";
import { autobind, EventEmitter, interval } from "../utils"; import { autobind, EventEmitter, interval } from "../utils";
import { KubeJsonApiData } from "./kube-json-api"; import { KubeJsonApiData } from "./kube-json-api";
import { IKubeWatchEvent, IKubeWatchRouteEvent, IKubeWatchRouteQuery } from "../../server/common/kubewatch"; import { IKubeWatchEvent, IKubeWatchRouteEvent, IKubeWatchRouteQuery } from "../../server/common/kubewatch";
@ -12,7 +12,7 @@ import { apiManager } from "./api-manager";
export { export {
IKubeWatchEvent IKubeWatchEvent
} };
@autobind() @autobind()
export class KubeWatchApi { export class KubeWatchApi {
@ -32,22 +32,25 @@ export class KubeWatchApi {
}); });
} }
@computed get activeApis() { @computed get activeApis(): KubeApi<any>[] {
return Array.from(this.subscribers.keys()); return Array.from(this.subscribers.keys());
} }
getSubscribersCount(api: KubeApi) { getSubscribersCount(api: KubeApi): number {
return this.subscribers.get(api) || 0; return this.subscribers.get(api) || 0;
} }
subscribe(...apis: KubeApi[]) { subscribe(...apis: KubeApi[]): () => void {
apis.forEach(api => { apis.forEach(api => {
this.subscribers.set(api, this.getSubscribersCount(api) + 1); this.subscribers.set(api, this.getSubscribersCount(api) + 1);
}); });
return () => apis.forEach(api => { return (): void => apis.forEach(api => {
const count = this.getSubscribersCount(api) - 1; const count = this.getSubscribersCount(api) - 1;
if (count <= 0) this.subscribers.delete(api); if (count <= 0) {
else this.subscribers.set(api, count); this.subscribers.delete(api);
} else {
this.subscribers.set(api, count);
}
}); });
} }
@ -55,16 +58,20 @@ export class KubeWatchApi {
const { isClusterAdmin, allowedNamespaces } = configStore; const { isClusterAdmin, allowedNamespaces } = configStore;
return { return {
api: this.activeApis.map(api => { api: this.activeApis.map(api => {
if (isClusterAdmin) return api.getWatchUrl(); if (isClusterAdmin) {
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace)) return api.getWatchUrl();
}
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace));
}).flat() }).flat()
} };
} }
// todo: maybe switch to websocket to avoid often reconnects // todo: maybe switch to websocket to avoid often reconnects
@autobind() @autobind()
protected connect() { protected connect(): void {
if (this.evtSource) this.disconnect(); // close previous connection if (this.evtSource) {
this.disconnect();
} // close previous connection
if (!this.activeApis.length) { if (!this.activeApis.length) {
return; return;
} }
@ -76,32 +83,35 @@ export class KubeWatchApi {
this.writeLog("CONNECTING", query.api); this.writeLog("CONNECTING", query.api);
} }
reconnect() { reconnect(): void {
if (!this.evtSource || this.evtSource.readyState !== EventSource.OPEN) { if (!this.evtSource || this.evtSource.readyState !== EventSource.OPEN) {
this.reconnectAttempts = this.maxReconnectsOnError; this.reconnectAttempts = this.maxReconnectsOnError;
this.connect(); this.connect();
} }
} }
protected disconnect() { protected disconnect(): void {
if (!this.evtSource) return; if (!this.evtSource) {
return;
}
this.evtSource.close(); this.evtSource.close();
this.evtSource.onmessage = null; this.evtSource.onmessage = null;
this.evtSource = null; this.evtSource = null;
} }
protected onMessage(evt: MessageEvent) { protected onMessage(evt: MessageEvent): void {
if (!evt.data) return; if (!evt.data) {
return;
}
const data = JSON.parse(evt.data); const data = JSON.parse(evt.data);
if ((data as IKubeWatchEvent).object) { if ((data as IKubeWatchEvent).object) {
this.onData.emit(data); this.onData.emit(data);
} } else {
else {
this.onRouteEvent(data); this.onRouteEvent(data);
} }
} }
protected async onRouteEvent({ type, url }: IKubeWatchRouteEvent) { protected async onRouteEvent({ type, url }: IKubeWatchRouteEvent): Promise<void> {
if (type === "STREAM_END") { if (type === "STREAM_END") {
this.disconnect(); this.disconnect();
const { apiBase, namespace } = KubeApi.parseApi(url); const { apiBase, namespace } = KubeApi.parseApi(url);
@ -111,13 +121,13 @@ export class KubeWatchApi {
await api.refreshResourceVersion({ namespace }); await api.refreshResourceVersion({ namespace });
this.reconnect(); this.reconnect();
} catch(error) { } catch(error) {
console.debug("failed to refresh resource version", error) console.debug("failed to refresh resource version", error);
} }
} }
} }
} }
protected onError(evt: MessageEvent) { protected onError(evt: MessageEvent): void {
const { reconnectAttempts: attemptsRemain, reconnectTimeoutMs } = this; const { reconnectAttempts: attemptsRemain, reconnectTimeoutMs } = this;
if (evt.eventPhase === EventSource.CLOSED) { if (evt.eventPhase === EventSource.CLOSED) {
if (attemptsRemain > 0) { if (attemptsRemain > 0) {
@ -127,14 +137,14 @@ export class KubeWatchApi {
} }
} }
protected writeLog(...data: any[]) { protected writeLog(...data: any[]): void {
if (configStore.isDevelopment) { if (configStore.isDevelopment) {
console.log('%cKUBE-WATCH-API:', `font-weight: bold`, ...data); console.log('%cKUBE-WATCH-API:', `font-weight: bold`, ...data);
} }
} }
addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) { addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void): () => void {
const listener = (evt: IKubeWatchEvent<KubeJsonApiData>) => { const listener = (evt: IKubeWatchEvent<KubeJsonApiData>): void => {
const { selfLink, namespace, resourceVersion } = evt.object.metadata; const { selfLink, namespace, resourceVersion } = evt.object.metadata;
const api = apiManager.getApi(selfLink); const api = apiManager.getApi(selfLink);
api.setResourceVersion(namespace, resourceVersion); api.setResourceVersion(namespace, resourceVersion);
@ -144,10 +154,10 @@ export class KubeWatchApi {
} }
}; };
this.onData.addListener(listener); this.onData.addListener(listener);
return () => this.onData.removeListener(listener); return (): void => this.onData.removeListener(listener);
} }
reset() { reset(): void {
this.subscribers.clear(); this.subscribers.clear();
} }
} }

View File

@ -1,7 +1,7 @@
import { configStore } from "../config.store"; import { configStore } from "../config.store";
import { isArray } from "util"; import { isArray } from "util";
export function isAllowedResource(resources: string|string[]) { export function isAllowedResource(resources: string|string[]): boolean {
if (!isArray(resources)) { if (!isArray(resources)) {
resources = [resources]; resources = [resources];
} }

View File

@ -2,7 +2,7 @@ import { stringify } from "querystring";
import { autobind, base64, EventEmitter, interval } from "../utils"; import { autobind, base64, EventEmitter, interval } from "../utils";
import { WebSocketApi } from "./websocket-api"; import { WebSocketApi } from "./websocket-api";
import { configStore } from "../config.store"; import { configStore } from "../config.store";
import isEqual from "lodash/isEqual" import isEqual from "lodash/isEqual";
export enum TerminalChannels { export enum TerminalChannels {
STDIN = 0, STDIN = 0,
@ -24,7 +24,7 @@ enum TerminalColor {
NO_COLOR = "\u001b[0m", NO_COLOR = "\u001b[0m",
} }
export interface ITerminalApiOptions { export interface TerminalApiOptions {
id: string; id: string;
node?: string; node?: string;
colorTheme?: "light" | "dark"; colorTheme?: "light" | "dark";
@ -38,7 +38,7 @@ export class TerminalApi extends WebSocketApi {
public onReady = new EventEmitter<[]>(); public onReady = new EventEmitter<[]>();
public isReady = false; public isReady = false;
constructor(protected options: ITerminalApiOptions) { constructor(protected options: TerminalApiOptions) {
super({ super({
logging: configStore.isDevelopment, logging: configStore.isDevelopment,
flushOnOpen: false, flushOnOpen: false,
@ -46,7 +46,7 @@ export class TerminalApi extends WebSocketApi {
}); });
} }
async getUrl(token: string) { getUrl(token: string): string {
const { hostname, protocol } = location; const { hostname, protocol } = location;
const { id, node } = this.options; const { id, node } = this.options;
const apiPrefix = configStore.apiPrefix.TERMINAL; const apiPrefix = configStore.apiPrefix.TERMINAL;
@ -62,9 +62,9 @@ export class TerminalApi extends WebSocketApi {
return `${wss}${hostname}${configStore.serverPort}${apiPrefix}/api?${stringify(queryParams)}`; return `${wss}${hostname}${configStore.serverPort}${apiPrefix}/api?${stringify(queryParams)}`;
} }
async connect() { async connect(): Promise<void> {
const token = await configStore.getToken(); const token = await configStore.getToken();
const apiUrl = await this.getUrl(token); const apiUrl = this.getUrl(token);
const { colorTheme } = this.options; const { colorTheme } = this.options;
this.emitStatus("Connecting ...", { this.emitStatus("Connecting ...", {
color: colorTheme == "light" ? TerminalColor.GRAY : TerminalColor.LIGHT_GRAY color: colorTheme == "light" ? TerminalColor.GRAY : TerminalColor.LIGHT_GRAY
@ -76,29 +76,35 @@ export class TerminalApi extends WebSocketApi {
} }
@autobind() @autobind()
async sendNewToken() { async sendNewToken(): Promise<void> {
const token = await configStore.getToken(); const token = await configStore.getToken();
if (!this.isReady || token == this.currentToken) return; if (!this.isReady || token == this.currentToken) {
return;
}
this.sendCommand(token, TerminalChannels.TOKEN); this.sendCommand(token, TerminalChannels.TOKEN);
this.currentToken = token; this.currentToken = token;
} }
destroy() { destroy(): void {
if (!this.socket) return; if (!this.socket) {
return;
}
const exitCode = String.fromCharCode(4); // ctrl+d const exitCode = String.fromCharCode(4); // ctrl+d
this.sendCommand(exitCode); this.sendCommand(exitCode);
this.tokenInterval.stop(); this.tokenInterval.stop();
setTimeout(() => super.destroy(), 2000); setTimeout(() => super.destroy(), 2000);
} }
removeAllListeners() { removeAllListeners(): void {
super.removeAllListeners(); super.removeAllListeners();
this.onReady.removeAllListeners(); this.onReady.removeAllListeners();
} }
@autobind() @autobind()
protected _onReady(data: string) { protected _onReady(data: string): boolean | undefined {
if (!data) return; if (!data) {
return;
}
this.isReady = true; this.isReady = true;
this.onReady.emit(); this.onReady.emit();
this.onData.removeListener(this._onReady); this.onData.removeListener(this._onReady);
@ -107,16 +113,15 @@ export class TerminalApi extends WebSocketApi {
return false; // prevent calling rest of listeners return false; // prevent calling rest of listeners
} }
reconnect() { reconnect(): void {
const { reconnectDelaySeconds } = this.params;
super.reconnect(); super.reconnect();
} }
sendCommand(key: string, channel = TerminalChannels.STDIN) { sendCommand(key: string, channel = TerminalChannels.STDIN): any {
return this.send(channel + base64.encode(key)); return this.send(channel + base64.encode(key));
} }
sendTerminalSize(cols: number, rows: number) { sendTerminalSize(cols: number, rows: number): void {
const newSize = { Width: cols, Height: rows }; const newSize = { Width: cols, Height: rows };
if (!isEqual(this.size, newSize)) { if (!isEqual(this.size, newSize)) {
this.sendCommand(JSON.stringify(newSize), TerminalChannels.TERMINAL_SIZE); this.sendCommand(JSON.stringify(newSize), TerminalChannels.TERMINAL_SIZE);
@ -124,24 +129,24 @@ export class TerminalApi extends WebSocketApi {
} }
} }
protected parseMessage(data: string) { protected parseMessage(data: string): any {
data = data.substr(1); // skip channel data = data.substr(1); // skip channel
return base64.decode(data); return base64.decode(data);
} }
protected _onOpen(evt: Event) { protected _onOpen(evt: Event): void {
// Client should send terminal size in special channel 4, // Client should send terminal size in special channel 4,
// But this size will be changed by terminal.fit() // But this size will be changed by terminal.fit()
this.sendTerminalSize(120, 80); this.sendTerminalSize(120, 80);
super._onOpen(evt); super._onOpen(evt);
} }
protected _onClose(evt: CloseEvent) { protected _onClose(evt: CloseEvent): void {
super._onClose(evt); super._onClose(evt);
this.isReady = false; this.isReady = false;
} }
protected emitStatus(data: string, options: { color?: TerminalColor; showTime?: boolean } = {}) { protected emitStatus(data: string, options: { color?: TerminalColor; showTime?: boolean } = {}): void {
const { color, showTime } = options; const { color, showTime } = options;
if (color) { if (color) {
data = `${color}${data}${TerminalColor.NO_COLOR}`; data = `${color}${data}${TerminalColor.NO_COLOR}`;
@ -153,7 +158,7 @@ export class TerminalApi extends WebSocketApi {
this.onData.emit(`${showTime ? time : ""}${data}\r\n`); this.onData.emit(`${showTime ? time : ""}${data}\r\n`);
} }
protected emitError(error: string) { protected emitError(error: string): void {
this.emitStatus(error, { this.emitStatus(error, {
color: TerminalColor.RED color: TerminalColor.RED
}); });

View File

@ -1,7 +1,7 @@
import { observable } from "mobx"; import { observable } from "mobx";
import { EventEmitter } from "../utils/eventEmitter"; import { EventEmitter } from "../utils/eventEmitter";
interface IParams { interface Params {
url?: string; // connection url, starts with ws:// or wss:// url?: string; // connection url, starts with ws:// or wss://
autoConnect?: boolean; // auto-connect in constructor autoConnect?: boolean; // auto-connect in constructor
flushOnOpen?: boolean; // flush pending commands on open socket flushOnOpen?: boolean; // flush pending commands on open socket
@ -10,7 +10,7 @@ interface IParams {
logging?: boolean; // show logs in console logging?: boolean; // show logs in console
} }
interface IMessage { interface Message {
id: string; id: string;
data: string; data: string;
} }
@ -25,7 +25,7 @@ export enum WebSocketApiState {
export class WebSocketApi { export class WebSocketApi {
protected socket: WebSocket; protected socket: WebSocket;
protected pendingCommands: IMessage[] = []; protected pendingCommands: Message[] = [];
protected reconnectTimer: any; protected reconnectTimer: any;
protected pingTimer: any; protected pingTimer: any;
protected pingMessage = "PING"; protected pingMessage = "PING";
@ -36,7 +36,7 @@ export class WebSocketApi {
public onData = new EventEmitter<[string]>(); public onData = new EventEmitter<[string]>();
public onClose = new EventEmitter<[]>(); public onClose = new EventEmitter<[]>();
static defaultParams: Partial<IParams> = { static defaultParams: Partial<Params> = {
autoConnect: true, autoConnect: true,
logging: false, logging: false,
reconnectDelaySeconds: 10, reconnectDelaySeconds: 10,
@ -44,7 +44,7 @@ export class WebSocketApi {
flushOnOpen: true, flushOnOpen: true,
}; };
constructor(protected params: IParams) { constructor(protected params: Params) {
this.params = Object.assign({}, WebSocketApi.defaultParams, params); this.params = Object.assign({}, WebSocketApi.defaultParams, params);
const { autoConnect, pingIntervalSeconds } = this.params; const { autoConnect, pingIntervalSeconds } = this.params;
if (autoConnect) { if (autoConnect) {
@ -55,20 +55,20 @@ export class WebSocketApi {
} }
} }
get isConnected() { get isConnected(): boolean {
const state = this.socket ? this.socket.readyState : -1; const state = this.socket ? this.socket.readyState : -1;
return state === WebSocket.OPEN && this.isOnline; return state === WebSocket.OPEN && this.isOnline;
} }
get isOnline() { get isOnline(): boolean {
return navigator.onLine; return navigator.onLine;
} }
setParams(params: Partial<IParams>) { setParams(params: Partial<Params>): void {
Object.assign(this.params, params); Object.assign(this.params, params);
} }
connect(url = this.params.url) { connect(url = this.params.url): void {
if (this.socket) { if (this.socket) {
this.socket.close(); // close previous connection first this.socket.close(); // close previous connection first
} }
@ -80,21 +80,27 @@ export class WebSocketApi {
this.readyState = WebSocketApiState.CONNECTING; this.readyState = WebSocketApiState.CONNECTING;
} }
ping() { ping(): void {
if (!this.isConnected) return; if (!this.isConnected) {
return;
}
this.send(this.pingMessage); this.send(this.pingMessage);
} }
reconnect() { reconnect(): void {
const { reconnectDelaySeconds } = this.params; const { reconnectDelaySeconds } = this.params;
if (!reconnectDelaySeconds) return; if (!reconnectDelaySeconds) {
return;
}
this.writeLog('reconnect after', reconnectDelaySeconds + "ms"); this.writeLog('reconnect after', reconnectDelaySeconds + "ms");
this.reconnectTimer = setTimeout(() => this.connect(), reconnectDelaySeconds * 1000); this.reconnectTimer = setTimeout(() => this.connect(), reconnectDelaySeconds * 1000);
this.readyState = WebSocketApiState.RECONNECTING; this.readyState = WebSocketApiState.RECONNECTING;
} }
destroy() { destroy(): void {
if (!this.socket) return; if (!this.socket) {
return;
}
this.socket.close(); this.socket.close();
this.socket = null; this.socket = null;
this.pendingCommands = []; this.pendingCommands = [];
@ -104,64 +110,60 @@ export class WebSocketApi {
this.readyState = WebSocketApiState.PENDING; this.readyState = WebSocketApiState.PENDING;
} }
removeAllListeners() { removeAllListeners(): void {
this.onOpen.removeAllListeners(); this.onOpen.removeAllListeners();
this.onData.removeAllListeners(); this.onData.removeAllListeners();
this.onClose.removeAllListeners(); this.onClose.removeAllListeners();
} }
send(command: string) { send(command: string): void {
const msg: IMessage = { const msg: Message = {
id: (Math.random() * Date.now()).toString(16).replace(".", ""), id: (Math.random() * Date.now()).toString(16).replace(".", ""),
data: command, data: command,
}; };
if (this.isConnected) { if (this.isConnected) {
this.socket.send(msg.data); this.socket.send(msg.data);
} } else {
else {
this.pendingCommands.push(msg); this.pendingCommands.push(msg);
} }
} }
protected flush() { protected flush(): void {
this.pendingCommands.forEach(msg => this.send(msg.data)); this.pendingCommands.forEach(msg => this.send(msg.data));
this.pendingCommands.length = 0; this.pendingCommands.length = 0;
} }
protected parseMessage(data: string) { protected _onOpen(evt: Event): void {
return data;
}
protected _onOpen(evt: Event) {
this.onOpen.emit(); this.onOpen.emit();
if (this.params.flushOnOpen) this.flush(); if (this.params.flushOnOpen) {
this.flush();
}
this.readyState = WebSocketApiState.OPEN; this.readyState = WebSocketApiState.OPEN;
this.writeLog('%cOPEN', 'color:green;font-weight:bold;', evt); this.writeLog('%cOPEN', 'color:green;font-weight:bold;', evt);
} }
protected _onMessage(evt: MessageEvent) { protected _onMessage(evt: MessageEvent): void {
const data = this.parseMessage(evt.data); const data = evt.data;
this.onData.emit(data); this.onData.emit(data);
this.writeLog('%cMESSAGE', 'color:black;font-weight:bold;', data); this.writeLog('%cMESSAGE', 'color:black;font-weight:bold;', data);
} }
protected _onError(evt: Event) { protected _onError(evt: Event): void {
this.writeLog('%cERROR', 'color:red;font-weight:bold;', evt) this.writeLog('%cERROR', 'color:red;font-weight:bold;', evt);
} }
protected _onClose(evt: CloseEvent) { protected _onClose(evt: CloseEvent): void {
const error = evt.code !== 1000 || !evt.wasClean; const error = evt.code !== 1000 || !evt.wasClean;
if (error) { if (error) {
this.reconnect(); this.reconnect();
} } else {
else {
this.readyState = WebSocketApiState.CLOSED; this.readyState = WebSocketApiState.CLOSED;
this.onClose.emit(); this.onClose.emit();
} }
this.writeLog('%cCLOSE', `color:${error ? "red" : "black"};font-weight:bold;`, evt); this.writeLog('%cCLOSE', `color:${error ? "red" : "black"};font-weight:bold;`, evt);
} }
protected writeLog(...data: any[]) { protected writeLog(...data: any[]): void {
if (this.params.logging) { if (this.params.logging) {
console.log(...data); console.log(...data);
} }

View File

@ -1,48 +1,48 @@
import get from "lodash/get"; import get from "lodash/get";
import { IKubeObjectMetadata, KubeObject } from "./kube-object"; import { KubeObject } from "./kube-object";
interface IToleration { interface Toleration {
key?: string; key?: string;
operator?: string; operator?: string;
effect?: string; effect?: string;
tolerationSeconds?: number; tolerationSeconds?: number;
} }
interface IMatchExpression { interface MatchExpression {
key: string; key: string;
operator: string; operator: string;
values: string[]; values: string[];
} }
interface INodeAffinity { interface NodeAffinity {
nodeSelectorTerms?: { nodeSelectorTerms?: {
matchExpressions: IMatchExpression[]; matchExpressions: MatchExpression[];
}[]; }[];
weight: number; weight: number;
preference: { preference: {
matchExpressions: IMatchExpression[]; matchExpressions: MatchExpression[];
}; };
} }
interface IPodAffinity { interface PodAffinity {
labelSelector: { labelSelector: {
matchExpressions: IMatchExpression[]; matchExpressions: MatchExpression[];
}; };
topologyKey: string; topologyKey: string;
} }
export interface IAffinity { export interface Affinity {
nodeAffinity?: { nodeAffinity?: {
requiredDuringSchedulingIgnoredDuringExecution?: INodeAffinity[]; requiredDuringSchedulingIgnoredDuringExecution?: NodeAffinity[];
preferredDuringSchedulingIgnoredDuringExecution?: INodeAffinity[]; preferredDuringSchedulingIgnoredDuringExecution?: NodeAffinity[];
}; };
podAffinity?: { podAffinity?: {
requiredDuringSchedulingIgnoredDuringExecution?: IPodAffinity[]; requiredDuringSchedulingIgnoredDuringExecution?: PodAffinity[];
preferredDuringSchedulingIgnoredDuringExecution?: IPodAffinity[]; preferredDuringSchedulingIgnoredDuringExecution?: PodAffinity[];
}; };
podAntiAffinity?: { podAntiAffinity?: {
requiredDuringSchedulingIgnoredDuringExecution?: IPodAffinity[]; requiredDuringSchedulingIgnoredDuringExecution?: PodAffinity[];
preferredDuringSchedulingIgnoredDuringExecution?: IPodAffinity[]; preferredDuringSchedulingIgnoredDuringExecution?: PodAffinity[];
}; };
} }
@ -66,17 +66,19 @@ export class WorkloadKubeObject extends KubeObject {
return KubeObject.stringifyLabels(labels); return KubeObject.stringifyLabels(labels);
} }
getTolerations(): IToleration[] { getTolerations(): Toleration[] {
return get(this, "spec.template.spec.tolerations", []) return get(this, "spec.template.spec.tolerations", []);
} }
getAffinity(): IAffinity { getAffinity(): Affinity {
return get(this, "spec.template.spec.affinity") return get(this, "spec.template.spec.affinity");
} }
getAffinityNumber() { getAffinityNumber(): number {
const affinity = this.getAffinity() const affinity = this.getAffinity();
if (!affinity) return 0 if (!affinity) {
return Object.keys(affinity).length return 0;
}
return Object.keys(affinity).length;
} }
} }

View File

@ -2,11 +2,11 @@ import * as React from "react";
import { Notifications } from "./components/notifications"; import { Notifications } from "./components/notifications";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
export function browserCheck() { export function browserCheck(): void {
const ua = window.navigator.userAgent const ua = window.navigator.userAgent;
const msie = ua.indexOf('MSIE ') // IE < 11 const msie = ua.indexOf('MSIE '); // IE < 11
const trident = ua.indexOf('Trident/') // IE 11 const trident = ua.indexOf('Trident/'); // IE 11
const edge = ua.indexOf('Edge') // Edge const edge = ua.indexOf('Edge'); // Edge
if (msie > 0 || trident > 0 || edge > 0) { if (msie > 0 || trident > 0 || edge > 0) {
Notifications.info( Notifications.info(
<p> <p>
@ -15,6 +15,6 @@ export function browserCheck() {
Please consider using another browser. Please consider using another browser.
</Trans> </Trans>
</p> </p>
) );
} }
} }

View File

@ -1 +1 @@
export * from "./not-found" export * from "./not-found";

View File

@ -3,13 +3,13 @@ import { Trans } from "@lingui/macro";
import { MainLayout } from "../layout/main-layout"; import { MainLayout } from "../layout/main-layout";
export class NotFound extends React.Component { export class NotFound extends React.Component {
render() { render(): JSX.Element {
return ( return (
<MainLayout className="NotFound" contentClass="flex" footer={null}> <MainLayout className="NotFound" contentClass="flex" footer={null}>
<p className="box center"> <p className="box center">
<Trans>Page not found</Trans> <Trans>Page not found</Trans>
</p> </p>
</MainLayout> </MainLayout>
) );
} }
} }

View File

@ -16,6 +16,9 @@ import { createInstallChartTab } from "../dock/install-chart.store";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const placeholder = require("./helm-placeholder.svg");
interface Props { interface Props {
chart: HelmChart; chart: HelmChart;
hideDetails(): void; hideDetails(): void;
@ -31,7 +34,9 @@ export class HelmChartDetails extends Component<Props> {
@disposeOnUnmount @disposeOnUnmount
chartSelector = autorun(async () => { chartSelector = autorun(async () => {
if (!this.props.chart) return; if (!this.props.chart) {
return;
}
this.chartVersions = null; this.chartVersions = null;
this.selectedChart = null; this.selectedChart = null;
this.description = null; this.description = null;
@ -43,42 +48,43 @@ export class HelmChartDetails extends Component<Props> {
}); });
}); });
loadChartData(version?: string) { loadChartData(version?: string): void {
const { chart: { name, repo } } = this.props; const { chart: { name, repo } } = this.props;
if (this.chartPromise) this.chartPromise.cancel(); if (this.chartPromise) {
this.chartPromise.cancel();
}
this.chartPromise = helmChartsApi.get(repo, name, version); this.chartPromise = helmChartsApi.get(repo, name, version);
} }
@autobind() @autobind()
onVersionChange(opt: SelectOption) { onVersionChange(opt: SelectOption): void {
const version = opt.value; const version = opt.value;
this.selectedChart = this.chartVersions.find(chart => chart.version === version); this.selectedChart = this.chartVersions.find(chart => chart.version === version);
this.description = null; this.description = null;
this.loadChartData(version); this.loadChartData(version);
this.chartPromise.then(data => { this.chartPromise.then(data => {
this.description = data.readme this.description = data.readme;
}); });
} }
@autobind() @autobind()
install() { install(): void {
createInstallChartTab(this.selectedChart); createInstallChartTab(this.selectedChart);
this.props.hideDetails() this.props.hideDetails();
} }
renderIntroduction() { renderIntroduction(): JSX.Element {
const { selectedChart, chartVersions, onVersionChange } = this; const { selectedChart, chartVersions, onVersionChange } = this;
const placeholder = require("./helm-placeholder.svg");
return ( return (
<div className="introduction flex align-flex-start"> <div className="introduction flex align-flex-start">
<img <img
className="intro-logo" className="intro-logo"
src={selectedChart.getIcon() || placeholder} src={selectedChart.icon || placeholder}
onError={(event) => event.currentTarget.src = placeholder} onError={(event: React.SyntheticEvent<HTMLImageElement, Event>): void => event.currentTarget.src = placeholder}
/> />
<div className="intro-contents box grow"> <div className="intro-contents box grow">
<div className="description flex align-center justify-space-between"> <div className="description flex align-center justify-space-between">
{selectedChart.getDescription()} {selectedChart.description}
<Button primary label={_i18n._(t`Install`)} onClick={this.install}/> <Button primary label={_i18n._(t`Install`)} onClick={this.install}/>
</div> </div>
<DrawerItem name={_i18n._(t`Version`)} className="version" onClick={stopPropagation}> <DrawerItem name={_i18n._(t`Version`)} className="version" onClick={stopPropagation}>
@ -86,16 +92,16 @@ export class HelmChartDetails extends Component<Props> {
themeName="outlined" themeName="outlined"
menuPortalTarget={null} menuPortalTarget={null}
options={chartVersions.map(chart => chart.version)} options={chartVersions.map(chart => chart.version)}
value={selectedChart.getVersion()} value={selectedChart.version}
onChange={onVersionChange} onChange={onVersionChange}
/> />
</DrawerItem> </DrawerItem>
<DrawerItem name={_i18n._(t`Home`)}> <DrawerItem name={_i18n._(t`Home`)}>
<a href={selectedChart.getHome()} target="_blank">{selectedChart.getHome()}</a> <a href={selectedChart.home} target="_blank" rel="noreferrer">{selectedChart.home}</a>
</DrawerItem> </DrawerItem>
<DrawerItem name={_i18n._(t`Maintainers`)} className="maintainers"> <DrawerItem name={_i18n._(t`Maintainers`)} className="maintainers">
{selectedChart.getMaintainers().map(({ name, email, url }) => {selectedChart.getMaintainers().map(({ name, email, url }) =>
<a key={name} href={url ? url : `mailto:${email}`} target="_blank">{name}</a> <a key={name} href={url ? url : `mailto:${email}`} target="_blank" rel="noreferrer">{name}</a>
)} )}
</DrawerItem> </DrawerItem>
{selectedChart.getKeywords().length > 0 && ( {selectedChart.getKeywords().length > 0 && (
@ -108,8 +114,10 @@ export class HelmChartDetails extends Component<Props> {
); );
} }
renderContent() { renderContent(): JSX.Element {
if (this.selectedChart === null || this.description === null) return <Spinner center/>; if (this.selectedChart === null || this.description === null) {
return <Spinner center/>;
}
return ( return (
<div className="box grow"> <div className="box grow">
{this.renderIntroduction()} {this.renderIntroduction()}
@ -120,7 +128,7 @@ export class HelmChartDetails extends Component<Props> {
); );
} }
render() { render(): JSX.Element {
const { chart, hideDetails } = this.props; const { chart, hideDetails } = this.props;
const title = chart ? <Trans>Chart: {chart.getFullName()}</Trans> : ""; const title = chart ? <Trans>Chart: {chart.getFullName()}</Trans> : "";
return ( return (

View File

@ -2,51 +2,48 @@ import { observable } from "mobx";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api"; import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
import { ItemStore } from "../../item.store"; import { ItemStore } from "../../item.store";
import flatten from "lodash/flatten" import flatten from "lodash/flatten";
import compareVersions from 'compare-versions'; import compareVersions from 'compare-versions';
export interface IChartVersion { export interface ChartVersion {
repo: string; repo: string;
version: string; version: string;
} }
@autobind() @autobind()
export class HelmChartStore extends ItemStore<HelmChart> { export class HelmChartStore extends ItemStore<HelmChart> {
@observable versions = observable.map<string, IChartVersion[]>(); @observable versions = observable.map<string, ChartVersion[]>();
loadAll() { async loadAll(): Promise<void> {
return this.loadItems(() => helmChartsApi.list()); await this.loadItems(() => helmChartsApi.list());
} }
getByName(name: string, repo: string) { getByName(desiredName: string, desiredRepo: string): HelmChart {
return this.items.find(chart => chart.getName() === name && chart.getRepository() === repo); return this.items.find(({ name, repo }) => desiredName === name && desiredRepo === repo);
} }
protected sortVersions = (versions: IChartVersion[]) => { protected sortVersions = (versions: ChartVersion[]): ChartVersion[] => {
return versions.sort((first, second) => { return versions.sort((first, second) => compareVersions(second.version, first.version));
return compareVersions(second.version, first.version)
});
}; };
async getVersions(chartName: string, force?: boolean): Promise<IChartVersion[]> { async getVersions(chartName: string, force?: boolean): Promise<ChartVersion[]> {
let versions = this.versions.get(chartName); let versions = this.versions.get(chartName);
if (versions && !force) { if (versions && !force) {
return versions; return versions;
} }
const loadVersions = (repo: string) => { const loadVersions = async (repo: string): Promise<ChartVersion[]> => {
return helmChartsApi.get(repo, chartName).then(({ versions }) => { const { versions } = await helmChartsApi.get(repo, chartName);
return versions.map(chart => ({ return versions.map(({version}) => ({ repo, version, }));
repo: repo,
version: chart.getVersion()
}))
})
}; };
if (!this.isLoaded) { if (!this.isLoaded) {
await this.loadAll(); await this.loadAll();
} }
const repos = this.items const repos = this.items
.filter(chart => chart.getName() === chartName) .filter(chart => chart.getName() === chartName)
.map(chart => chart.getRepository()); .map(({repo}) => repo);
versions = await Promise.all(repos.map(loadVersions)) versions = await Promise.all(repos.map(loadVersions))
.then(flatten) .then(flatten)
.then(this.sortVersions); .then(this.sortVersions);
@ -55,7 +52,7 @@ export class HelmChartStore extends ItemStore<HelmChart> {
return versions; return versions;
} }
reset() { reset(): void {
super.reset(); super.reset();
this.versions.clear(); this.versions.clear();
} }

View File

@ -1,14 +1,14 @@
import { RouteProps } from "react-router" import { RouteProps } from "react-router";
import { appsRoute } from "../+apps/apps.route"; import { appsRoute } from "../+apps/apps.route";
import { buildURL } from "../../navigation"; import { buildURL } from "../../navigation";
export const helmChartsRoute: RouteProps = { export const helmChartsRoute: RouteProps = {
path: appsRoute.path + "/charts/:repo?/:chartName?" path: appsRoute.path + "/charts/:repo?/:chartName?"
} };
export interface IHelmChartsRouteParams { export interface HelmChartsRouteParams {
chartName?: string; chartName?: string;
repo?: string; repo?: string;
} }
export const helmChartsURL = buildURL<IHelmChartsRouteParams>(helmChartsRoute.path) export const helmChartsURL = buildURL<HelmChartsRouteParams>(helmChartsRoute.path);

View File

@ -3,7 +3,7 @@ import "./helm-charts.scss";
import React, { Component } from "react"; import React, { Component } from "react";
import { RouteComponentProps } from "react-router"; import { RouteComponentProps } from "react-router";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { helmChartsURL, IHelmChartsRouteParams } from "./helm-charts.route"; import { helmChartsURL, HelmChartsRouteParams } from "./helm-charts.route";
import { helmChartStore } from "./helm-chart.store"; import { helmChartStore } from "./helm-chart.store";
import { HelmChart } from "../../api/endpoints/helm-charts.api"; import { HelmChart } from "../../api/endpoints/helm-charts.api";
import { HelmChartDetails } from "./helm-chart-details"; import { HelmChartDetails } from "./helm-chart-details";
@ -18,39 +18,38 @@ enum sortBy {
repo = "repo", repo = "repo",
} }
interface Props extends RouteComponentProps<IHelmChartsRouteParams> { interface Props extends RouteComponentProps<HelmChartsRouteParams> {
} }
@observer @observer
export class HelmCharts extends Component<Props> { export class HelmCharts extends Component<Props> {
componentDidMount() { componentDidMount(): void {
helmChartStore.loadAll(); helmChartStore.loadAll();
} }
get selectedChart() { get selectedChart(): HelmChart {
const { match: { params: { chartName, repo } } } = this.props const { match: { params: { chartName, repo } } } = this.props;
return helmChartStore.getByName(chartName, repo); return helmChartStore.getByName(chartName, repo);
} }
showDetails = (chart: HelmChart) => { showDetails = (chart: HelmChart): void => {
if (!chart) { if (!chart) {
navigation.merge(helmChartsURL()) navigation.merge(helmChartsURL());
} } else {
else {
navigation.merge(helmChartsURL({ navigation.merge(helmChartsURL({
params: { params: {
chartName: chart.getName(), chartName: chart.getName(),
repo: chart.getRepository(), repo: chart.repo,
} }
})) }));
} }
} }
hideDetails = () => { hideDetails = (): void => {
this.showDetails(null); this.showDetails(null);
} }
render() { render(): JSX.Element {
return ( return (
<> <>
<ItemListLayout <ItemListLayout
@ -59,19 +58,19 @@ export class HelmCharts extends Component<Props> {
isClusterScoped={true} isClusterScoped={true}
isSelectable={false} isSelectable={false}
sortingCallbacks={{ sortingCallbacks={{
[sortBy.name]: (chart: HelmChart) => chart.getName(), [sortBy.name]: (chart: HelmChart): string => chart.getName(),
[sortBy.repo]: (chart: HelmChart) => chart.getRepository(), [sortBy.repo]: ({repo}: HelmChart): string => repo,
}} }}
searchFilters={[ searchFilters={[
(chart: HelmChart) => chart.getName(), (chart: HelmChart): string => chart.getName(),
(chart: HelmChart) => chart.getVersion(), ({ version }: HelmChart): string => version,
(chart: HelmChart) => chart.getAppVersion(), (chart: HelmChart): string => chart.getAppVersion(),
(chart: HelmChart) => chart.getKeywords(), ({ keywords }: HelmChart): string[] => keywords,
]} ]}
filterItems={[ filterItems={[
(items: HelmChart[]) => items.filter(item => !item.deprecated) (items: HelmChart[]): HelmChart[] => items.filter(item => !item.deprecated)
]} ]}
customizeHeader={() => ( customizeHeader={(): JSX.Element => (
<SearchInput placeholder={_i18n._(t`Search Helm Charts`)}/> <SearchInput placeholder={_i18n._(t`Search Helm Charts`)}/>
)} )}
renderTableHeader={[ renderTableHeader={[
@ -83,18 +82,18 @@ export class HelmCharts extends Component<Props> {
{ title: <Trans>Repository</Trans>, className: "repository", sortBy: sortBy.repo }, { title: <Trans>Repository</Trans>, className: "repository", sortBy: sortBy.repo },
]} ]}
renderTableContents={(chart: HelmChart) => [ renderTableContents={(chart: HelmChart): (HTMLElement | string | React.ReactNode)[] => [
<figure> <figure key="placeholder-img">
<img <img
src={chart.getIcon() || require("./helm-placeholder.svg")} src={chart.icon || require("./helm-placeholder.svg")}
onLoad={evt => evt.currentTarget.classList.add("visible")} onLoad={(evt): void => evt.currentTarget.classList.add("visible")}
/> />
</figure>, </figure>,
chart.getName(), chart.getName(),
chart.getDescription(), chart.description,
chart.getVersion(), chart.version,
chart.getAppVersion(), chart.getAppVersion(),
{ title: chart.getRepository(), className: chart.getRepository().toLowerCase() } { title: chart.repo, className: chart.repo.toLowerCase() }
]} ]}
detailsItem={this.selectedChart} detailsItem={this.selectedChart}
onDetails={this.showDetails} onDetails={this.showDetails}

View File

@ -7,7 +7,7 @@ import { observable, reaction } from "mobx";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { t, Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
import kebabCase from "lodash/kebabCase"; import kebabCase from "lodash/kebabCase";
import { HelmRelease, helmReleasesApi, IReleaseDetails } from "../../api/endpoints/helm-releases.api"; import { HelmRelease, helmReleasesApi, ReleaseInfo } from "../../api/endpoints/helm-releases.api";
import { HelmReleaseMenu } from "./release-menu"; import { HelmReleaseMenu } from "./release-menu";
import { Drawer, DrawerItem, DrawerTitle } from "../drawer"; import { Drawer, DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge"; import { Badge } from "../badge";
@ -35,14 +35,16 @@ interface Props {
@observer @observer
export class ReleaseDetails extends Component<Props> { export class ReleaseDetails extends Component<Props> {
@observable details: IReleaseDetails; @observable details: ReleaseInfo;
@observable values = ""; @observable values = "";
@observable saving = false; @observable saving = false;
@observable releaseSecret: Secret; @observable releaseSecret: Secret;
@disposeOnUnmount @disposeOnUnmount
releaseSelector = reaction(() => this.props.release, release => { releaseSelector = reaction(() => this.props.release, release => {
if (!release) return; if (!release) {
return;
}
this.loadDetails(); this.loadDetails();
this.loadValues(); this.loadValues();
this.releaseSecret = null; this.releaseSecret = null;
@ -51,33 +53,36 @@ export class ReleaseDetails extends Component<Props> {
@disposeOnUnmount @disposeOnUnmount
secretWatcher = reaction(() => secretsStore.items.toJS(), () => { secretWatcher = reaction(() => secretsStore.items.toJS(), () => {
if (!this.props.release) return; if (!this.props.release) {
return;
}
const { getReleaseSecret } = releaseStore; const { getReleaseSecret } = releaseStore;
const { release } = this.props; const { release } = this.props;
const secret = getReleaseSecret(release); const secret = getReleaseSecret(release);
if (this.releaseSecret) { if (this.releaseSecret) {
if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) return; if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) {
return;
}
this.loadDetails(); this.loadDetails();
} }
this.releaseSecret = secret; this.releaseSecret = secret;
}); });
async loadDetails() { async loadDetails(): Promise<void> {
const { release } = this.props; const { release } = this.props;
this.details = null; this.details = null;
this.details = await helmReleasesApi.get(release.getName(), release.getNs()); this.details = await helmReleasesApi.get(release.getName(), release.namespace);
} }
async loadValues() { async loadValues(): Promise<void> {
const { release } = this.props; const { release } = this.props;
this.values = ""; this.values = "";
this.values = await helmReleasesApi.getValues(release.getName(), release.getNs()); this.values = await helmReleasesApi.getValues(release.getName(), release.namespace);
} }
updateValues = async () => { updateValues = async (): Promise<void> => {
const { release } = this.props; const { release } = this.props;
const name = release.getName(); const { namespace, name} = release;
const namespace = release.getNs()
const data = { const data = {
chart: release.getChart(), chart: release.getChart(),
repo: await release.getRepo(), repo: await release.getRepo(),
@ -96,13 +101,13 @@ export class ReleaseDetails extends Component<Props> {
this.saving = false; this.saving = false;
} }
upgradeVersion = () => { upgradeVersion = (): void => {
const { release, hideDetails } = this.props; const { release, hideDetails } = this.props;
createUpgradeChartTab(release); createUpgradeChartTab(release);
hideDetails(); hideDetails();
} }
renderValues() { renderValues(): JSX.Element {
const { values, saving } = this; const { values, saving } = this;
return ( return (
<div className="values"> <div className="values">
@ -111,7 +116,7 @@ export class ReleaseDetails extends Component<Props> {
<AceEditor <AceEditor
mode="yaml" mode="yaml"
value={values} value={values}
onChange={values => this.values = values} onChange={(values): string => this.values = values}
/> />
<Button <Button
primary primary
@ -121,11 +126,13 @@ export class ReleaseDetails extends Component<Props> {
/> />
</div> </div>
</div> </div>
) );
} }
renderNotes() { renderNotes(): JSX.Element {
if (!this.details.info?.notes) return null; if (!this.details.info?.notes) {
return null;
}
const { notes } = this.details.info; const { notes } = this.details.info;
return ( return (
<div className="notes"> <div className="notes">
@ -134,9 +141,11 @@ export class ReleaseDetails extends Component<Props> {
); );
} }
renderResources() { renderResources(): JSX.Element {
const { resources } = this.details; const { resources } = this.details;
if (!resources) return null; if (!resources) {
return null;
}
const groups = groupBy(resources, item => item.kind); const groups = groupBy(resources, item => item.kind);
const tables = Object.entries(groups).map(([kind, items]) => { const tables = Object.entries(groups).map(([kind, items]) => {
return ( return (
@ -177,10 +186,12 @@ export class ReleaseDetails extends Component<Props> {
); );
} }
renderContent() { renderContent(): JSX.Element {
const { release } = this.props; const { release } = this.props;
const { details } = this; const { details } = this;
if (!release) return null; if (!release) {
return null;
}
if (!details) { if (!details) {
return <Spinner center/>; return <Spinner center/>;
} }
@ -201,7 +212,7 @@ export class ReleaseDetails extends Component<Props> {
{release.getUpdated()} <Trans>ago</Trans> ({release.updated}) {release.getUpdated()} <Trans>ago</Trans> ({release.updated})
</DrawerItem> </DrawerItem>
<DrawerItem name={<Trans>Namespace</Trans>}> <DrawerItem name={<Trans>Namespace</Trans>}>
{release.getNs()} {release.namespace}
</DrawerItem> </DrawerItem>
<DrawerItem name={<Trans>Version</Trans>} onClick={stopPropagation}> <DrawerItem name={<Trans>Version</Trans>} onClick={stopPropagation}>
<div className="version flex gaps align-center"> <div className="version flex gaps align-center">
@ -222,13 +233,13 @@ export class ReleaseDetails extends Component<Props> {
<DrawerTitle title={_i18n._(t`Resources`)}/> <DrawerTitle title={_i18n._(t`Resources`)}/>
{this.renderResources()} {this.renderResources()}
</div> </div>
) );
} }
render() { render(): JSX.Element {
const { release, hideDetails } = this.props const { release, hideDetails } = this.props;
const title = release ? <Trans>Release: {release.getName()}</Trans> : "" const title = release ? <Trans>Release: {release.getName()}</Trans> : "";
const toolbar = <HelmReleaseMenu release={release} toolbar hideDetails={hideDetails}/> const toolbar = <HelmReleaseMenu release={release} toolbar hideDetails={hideDetails}/>;
return ( return (
<Drawer <Drawer
className={cssNames("ReleaseDetails", themeStore.activeTheme.type)} className={cssNames("ReleaseDetails", themeStore.activeTheme.type)}
@ -240,6 +251,6 @@ export class ReleaseDetails extends Component<Props> {
> >
{this.renderContent()} {this.renderContent()}
</Drawer> </Drawer>
) );
} }
} }

View File

@ -17,26 +17,28 @@ interface Props extends MenuActionsProps {
export class HelmReleaseMenu extends React.Component<Props> { export class HelmReleaseMenu extends React.Component<Props> {
@autobind() @autobind()
remove() { remove(): Promise<void> {
return releaseStore.remove(this.props.release); return releaseStore.remove(this.props.release);
} }
@autobind() @autobind()
upgrade() { upgrade(): void {
const { release, hideDetails } = this.props; const { release, hideDetails } = this.props;
createUpgradeChartTab(release); createUpgradeChartTab(release);
hideDetails && hideDetails(); hideDetails && hideDetails();
} }
@autobind() @autobind()
rollback() { rollback(): void {
ReleaseRollbackDialog.open(this.props.release); ReleaseRollbackDialog.open(this.props.release);
} }
renderContent() { renderContent(): JSX.Element {
const { release, toolbar } = this.props; const { release, toolbar } = this.props;
if (!release) return; if (!release) {
const hasRollback = release && release.getRevision() > 1; return;
}
const hasRollback = release && release.revision > 1;
return ( return (
<> <>
{hasRollback && ( {hasRollback && (
@ -46,18 +48,19 @@ export class HelmReleaseMenu extends React.Component<Props> {
</MenuItem> </MenuItem>
)} )}
</> </>
) );
} }
render() { render(): JSX.Element {
const { className, release, ...menuProps } = this.props; const { className, release: _release, ...menuProps } = this.props;
return ( return (
<MenuActions <MenuActions
{...menuProps} {...menuProps}
className={cssNames("HelmReleaseMenu", className)} className={cssNames("HelmReleaseMenu", className)}
removeAction={this.remove} removeAction={this.remove}
children={this.renderContent()} >
/> {this.renderContent()}
</MenuActions>
); );
} }
} }

View File

@ -6,11 +6,11 @@ import { observer } from "mobx-react";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { Dialog, DialogProps } from "../dialog"; import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard"; import { Wizard, WizardStep } from "../wizard";
import { HelmRelease, helmReleasesApi, IReleaseRevision } from "../../api/endpoints/helm-releases.api"; import { HelmRelease, helmReleasesApi, ReleaseRevision } from "../../api/endpoints/helm-releases.api";
import { releaseStore } from "./release.store"; import { releaseStore } from "./release.store";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../select";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import orderBy from "lodash/orderBy" import orderBy from "lodash/orderBy";
interface Props extends DialogProps { interface Props extends DialogProps {
} }
@ -21,15 +21,15 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
@observable.ref static release: HelmRelease = null; @observable.ref static release: HelmRelease = null;
@observable isLoading = false; @observable isLoading = false;
@observable revision: IReleaseRevision; @observable revision: ReleaseRevision;
@observable revisions = observable.array<IReleaseRevision>(); @observable revisions = observable.array<ReleaseRevision>();
static open(release: HelmRelease) { static open(release: HelmRelease): void {
ReleaseRollbackDialog.isOpen = true; ReleaseRollbackDialog.isOpen = true;
ReleaseRollbackDialog.release = release; ReleaseRollbackDialog.release = release;
} }
static close() { static close(): void {
ReleaseRollbackDialog.isOpen = false; ReleaseRollbackDialog.isOpen = false;
} }
@ -37,10 +37,10 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
return ReleaseRollbackDialog.release; return ReleaseRollbackDialog.release;
} }
onOpen = async () => { onOpen = async (): Promise<void> => {
this.isLoading = true; this.isLoading = true;
const currentRevision = this.release.getRevision(); const currentRevision = this.release.revision;
let releases = await helmReleasesApi.getHistory(this.release.getName(), this.release.getNs()); let releases = await helmReleasesApi.getHistory(this.release.getName(), this.release.namespace);
releases = releases.filter(item => item.revision !== currentRevision); // remove current releases = releases.filter(item => item.revision !== currentRevision); // remove current
releases = orderBy(releases, "revision", "desc"); // sort releases = orderBy(releases, "revision", "desc"); // sort
this.revisions.replace(releases); this.revisions.replace(releases);
@ -48,24 +48,24 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
this.isLoading = false; this.isLoading = false;
} }
rollback = async () => { rollback = async (): Promise<void> => {
const revisionNumber = this.revision.revision; const revisionNumber = this.revision.revision;
try { try {
await releaseStore.rollback(this.release.getName(), this.release.getNs(), revisionNumber); await releaseStore.rollback(this.release.getName(), this.release.namespace, revisionNumber);
this.close(); this.close();
} catch (err) { } catch (err) {
Notifications.error(err); Notifications.error(err);
} }
}; };
close = () => { close = (): void => {
ReleaseRollbackDialog.close(); ReleaseRollbackDialog.close();
} }
renderContent() { renderContent(): JSX.Element {
const { revision, revisions } = this; const { revision, revisions } = this;
if (!revision) { if (!revision) {
return <p><Trans>No revisions to rollback.</Trans></p> return <p><Trans>No revisions to rollback.</Trans></p>;
} }
return ( return (
<div className="flex gaps align-center"> <div className="flex gaps align-center">
@ -74,17 +74,19 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
themeName="light" themeName="light"
value={revision} value={revision}
options={revisions} options={revisions}
formatOptionLabel={({ value }: SelectOption<IReleaseRevision>) => `${value.revision} - ${value.chart}`} formatOptionLabel={({ value }: SelectOption<ReleaseRevision>): string => `${value.revision} - ${value.chart}`}
onChange={({ value }: SelectOption<IReleaseRevision>) => this.revision = value} onChange={({ value }: SelectOption<ReleaseRevision>): void => {
this.revision = value;
}}
/> />
</div> </div>
) );
} }
render() { render(): JSX.Element {
const { ...dialogProps } = this.props; const { ...dialogProps } = this.props;
const releaseName = this.release ? this.release.getName() : ""; const releaseName = this.release ? this.release.getName() : "";
const header = <h5><Trans>Rollback <b>{releaseName}</b></Trans></h5> const header = <h5><Trans>Rollback <b>{releaseName}</b></Trans></h5>;
return ( return (
<Dialog <Dialog
{...dialogProps} {...dialogProps}
@ -104,6 +106,6 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
</WizardStep> </WizardStep>
</Wizard> </Wizard>
</Dialog> </Dialog>
) );
} }
} }

View File

@ -1,14 +1,14 @@
import { RouteProps } from "react-router" import { RouteProps } from "react-router";
import { appsRoute } from "../+apps/apps.route"; import { appsRoute } from "../+apps/apps.route";
import { buildURL } from "../../navigation"; import { buildURL } from "../../navigation";
export const releaseRoute: RouteProps = { export const releaseRoute: RouteProps = {
path: appsRoute.path + "/releases/:namespace?/:name?" path: appsRoute.path + "/releases/:namespace?/:name?"
} };
export interface IReleaseRouteParams { export interface ReleaseRouteParams {
name?: string; name?: string;
namespace?: string; namespace?: string;
} }
export const releaseURL = buildURL<IReleaseRouteParams>(releaseRoute.path); export const releaseURL = buildURL<ReleaseRouteParams>(releaseRoute.path);

View File

@ -1,11 +1,12 @@
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import { action, observable, when, IReactionDisposer, reaction } from "mobx"; import { action, observable, when, IReactionDisposer, reaction } from "mobx";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayload } from "../../api/endpoints/helm-releases.api"; import { HelmRelease, helmReleasesApi, ReleaseCreatePayload, ReleaseUpdatePayload, ReleaseUpdateDetails } from "../../api/endpoints/helm-releases.api";
import { ItemStore } from "../../item.store"; import { ItemStore } from "../../item.store";
import { configStore } from "../../config.store"; import { configStore } from "../../config.store";
import { secretsStore } from "../+config-secrets/secrets.store"; import { secretsStore } from "../+config-secrets/secrets.store";
import { Secret } from "../../api/endpoints"; import { Secret } from "../../api/endpoints";
import { KubeJsonApiData } from "client/api/kube-json-api";
@autobind() @autobind()
export class ReleaseStore extends ItemStore<HelmRelease> { export class ReleaseStore extends ItemStore<HelmRelease> {
@ -19,47 +20,51 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
}); });
} }
watch() { watch(): void {
this.secretWatcher = reaction(() => secretsStore.items.toJS(), () => { this.secretWatcher = reaction(() => secretsStore.items.toJS(), () => {
if (this.isLoading) return; if (this.isLoading) {
return;
}
const secrets = this.getReleaseSecrets(); const secrets = this.getReleaseSecrets();
const amountChanged = secrets.length !== this.releaseSecrets.length; const amountChanged = secrets.length !== this.releaseSecrets.length;
const labelsChanged = this.releaseSecrets.some(item => { const labelsChanged = this.releaseSecrets.some(item => {
const secret = secrets.find(secret => secret.getId() == item.getId()); const secret = secrets.find(secret => secret.getId() == item.getId());
if (!secret) return; if (!secret) {
return;
}
return !isEqual(item.getLabels(), secret.getLabels()); return !isEqual(item.getLabels(), secret.getLabels());
}); });
if (amountChanged || labelsChanged) { if (amountChanged || labelsChanged) {
this.loadAll(); this.loadAll();
} }
this.releaseSecrets = [...secrets]; this.releaseSecrets = [...secrets];
}) });
} }
unwatch() { unwatch(): void {
this.secretWatcher(); this.secretWatcher();
} }
getReleaseSecrets() { getReleaseSecrets(): Secret[] {
return secretsStore.getByLabel({ owner: "helm" }); return secretsStore.getByLabel({ owner: "helm" });
} }
getReleaseSecret(release: HelmRelease) { getReleaseSecret(release: HelmRelease): Secret {
const labels = { const labels = {
owner: "helm", owner: "helm",
name: release.getName() name: release.getName()
} };
return secretsStore.getByLabel(labels) return secretsStore.getByLabel(labels)
.filter(secret => secret.getNs() == release.getNs())[0]; .filter(secret => secret.getNs() == release.namespace)[0];
} }
@action @action
async loadAll() { async loadAll(): Promise<void> {
this.isLoading = true; this.isLoading = true;
let items; let items: HelmRelease[];
try { try {
const { isClusterAdmin, allowedNamespaces } = configStore; const { isClusterAdmin, allowedNamespaces } = configStore;
items = await this.loadItems(!isClusterAdmin ? allowedNamespaces : null); items = await this.loadItems(...(!isClusterAdmin ? allowedNamespaces : []));
} finally { } finally {
if (items) { if (items) {
items = this.sortItems(items); items = this.sortItems(items);
@ -70,42 +75,49 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
} }
} }
async loadItems(namespaces?: string[]) { async loadItems(...namespaces: any[]): Promise<HelmRelease[]> {
if (!namespaces) { if (!namespaces) {
return helmReleasesApi.list(); return helmReleasesApi.list();
} } else {
else {
return Promise return Promise
.all(namespaces.map(namespace => helmReleasesApi.list(namespace))) .all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
.then(items => items.flat()); .then(items => items.flat());
} }
} }
async create(payload: IReleaseCreatePayload) { async create(payload: ReleaseCreatePayload): Promise<ReleaseUpdateDetails> {
const response = await helmReleasesApi.create(payload); const response = await helmReleasesApi.create(payload);
if (this.isLoaded) this.loadAll(); if (this.isLoaded) {
this.loadAll();
}
return response; return response;
} }
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) { async update(name: string, namespace: string, payload: ReleaseUpdatePayload): Promise<ReleaseUpdateDetails> {
const response = await helmReleasesApi.update(name, namespace, payload); const response = await helmReleasesApi.update(name, namespace, payload);
if (this.isLoaded) this.loadAll(); if (this.isLoaded) {
this.loadAll();
}
return response; return response;
} }
async rollback(name: string, namespace: string, revision: number) { async rollback(name: string, namespace: string, revision: number): Promise<KubeJsonApiData> {
const response = await helmReleasesApi.rollback(name, namespace, revision); const response = await helmReleasesApi.rollback(name, namespace, revision);
if (this.isLoaded) this.loadAll(); if (this.isLoaded) {
this.loadAll();
}
return response; return response;
} }
async remove(release: HelmRelease) { async remove(release: HelmRelease): Promise<void> {
return super.removeItem(release, () => helmReleasesApi.delete(release.getName(), release.getNs())); return super.removeItem(release, () => helmReleasesApi.delete(release.getName(), release.namespace));
} }
async removeSelectedItems() { async removeSelectedItems(): Promise<void> {
if (!this.selectedItems.length) return; if (!this.selectedItems.length) {
return Promise.all(this.selectedItems.map(this.remove)); return;
}
await Promise.all(this.selectedItems.map(this.remove));
} }
} }

View File

@ -6,7 +6,7 @@ import { observer } from "mobx-react";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { RouteComponentProps } from "react-router"; import { RouteComponentProps } from "react-router";
import { releaseStore } from "./release.store"; import { releaseStore } from "./release.store";
import { IReleaseRouteParams, releaseURL } from "./release.route"; import { ReleaseRouteParams, releaseURL } from "./release.route";
import { HelmRelease } from "../../api/endpoints/helm-releases.api"; import { HelmRelease } from "../../api/endpoints/helm-releases.api";
import { ReleaseDetails } from "./release-details"; import { ReleaseDetails } from "./release-details";
import { ReleaseRollbackDialog } from "./release-rollback-dialog"; import { ReleaseRollbackDialog } from "./release-rollback-dialog";
@ -24,59 +24,58 @@ enum sortBy {
updated = "update" updated = "update"
} }
interface Props extends RouteComponentProps<IReleaseRouteParams> { interface Props extends RouteComponentProps<ReleaseRouteParams> {
} }
@observer @observer
export class HelmReleases extends Component<Props> { export class HelmReleases extends Component<Props> {
componentDidMount() { componentDidMount(): void {
// Watch for secrets associated with releases and react to their changes // Watch for secrets associated with releases and react to their changes
releaseStore.watch(); releaseStore.watch();
} }
componentWillUnmount() { componentWillUnmount(): void {
releaseStore.unwatch(); releaseStore.unwatch();
} }
get selectedRelease() { get selectedRelease(): HelmRelease {
const { match: { params: { name, namespace } } } = this.props; const { match: { params: { name, namespace } } } = this.props;
return releaseStore.items.find(release => { return releaseStore.items.find(release => {
return release.getName() == name && release.getNs() == namespace; return release.getName() == name && release.namespace == namespace;
}); });
} }
showDetails = (item: HelmRelease) => { showDetails = (item: HelmRelease): void => {
if (!item) { if (!item) {
navigation.merge(releaseURL()) navigation.merge(releaseURL());
} } else {
else {
navigation.merge(releaseURL({ navigation.merge(releaseURL({
params: { params: {
name: item.getName(), name: item.getName(),
namespace: item.getNs() namespace: item.namespace
} }
})) }));
} }
} }
hideDetails = () => { hideDetails = (): void => {
this.showDetails(null); this.showDetails(null);
} }
renderRemoveDialogMessage(selectedItems: HelmRelease[]) { renderRemoveDialogMessage(selectedItems: HelmRelease[]): JSX.Element {
const releaseNames = selectedItems.map(item => item.getName()).join(", "); const releaseNames = selectedItems.map(item => item.getName()).join(", ");
return ( return (
<div> <div>
<Trans>Remove <b>{releaseNames}</b>?</Trans> <Trans>Remove <b>{releaseNames}</b>?</Trans>
<p className="warning"> <p className="warning">
<Trans>Note: StatefulSet Volumes won't be deleted automatically</Trans> <Trans>Note: StatefulSet Volumes won&apos;t be deleted automatically</Trans>
</p> </p>
</div> </div>
) );
} }
render() { render(): JSX.Element {
return ( return (
<> <>
<ItemListLayout <ItemListLayout
@ -84,19 +83,19 @@ export class HelmReleases extends Component<Props> {
store={releaseStore} store={releaseStore}
dependentStores={[secretsStore]} dependentStores={[secretsStore]}
sortingCallbacks={{ sortingCallbacks={{
[sortBy.name]: (release: HelmRelease) => release.getName(), [sortBy.name]: (release: HelmRelease): string => release.getName(),
[sortBy.namespace]: (release: HelmRelease) => release.getNs(), [sortBy.namespace]: (release: HelmRelease): string => release.namespace,
[sortBy.revision]: (release: HelmRelease) => release.getRevision(), [sortBy.revision]: (release: HelmRelease): number => release.revision,
[sortBy.chart]: (release: HelmRelease) => release.getChart(), [sortBy.chart]: (release: HelmRelease): string => release.getChart(),
[sortBy.status]: (release: HelmRelease) => release.getStatus(), [sortBy.status]: (release: HelmRelease): string => release.status,
[sortBy.updated]: (release: HelmRelease) => release.getUpdated(false, false), [sortBy.updated]: (release: HelmRelease): string | number => release.getUpdated(false, false),
}} }}
searchFilters={[ searchFilters={[
(release: HelmRelease) => release.getName(), (release: HelmRelease): string => release.getName(),
(release: HelmRelease) => release.getNs(), (release: HelmRelease): string => release.namespace,
(release: HelmRelease) => release.getChart(), (release: HelmRelease): string => release.getChart(),
(release: HelmRelease) => release.getStatus(), (release: HelmRelease): string => release.getStatus(),
(release: HelmRelease) => release.getVersion(), (release: HelmRelease): string | number => release.getVersion(),
]} ]}
renderHeaderTitle={<Trans>Releases</Trans>} renderHeaderTitle={<Trans>Releases</Trans>}
renderTableHeader={[ renderTableHeader={[
@ -109,30 +108,30 @@ export class HelmReleases extends Component<Props> {
{ title: <Trans>Status</Trans>, className: "status", sortBy: sortBy.status }, { title: <Trans>Status</Trans>, className: "status", sortBy: sortBy.status },
{ title: <Trans>Updated</Trans>, className: "updated", sortBy: sortBy.updated }, { title: <Trans>Updated</Trans>, className: "updated", sortBy: sortBy.updated },
]} ]}
renderTableContents={(release: HelmRelease) => { renderTableContents={(release: HelmRelease): (string | number | React.ReactNode)[] => {
const version = release.getVersion(); const version = release.getVersion();
return [ return [
release.getName(), release.getName(),
release.getNs(), release.namespace,
release.getChart(), release.getChart(),
release.getRevision(), release.revision,
<> <>
{version} {version}
</>, </>,
release.appVersion, release.appVersion,
{ title: release.getStatus(), className: kebabCase(release.getStatus()) }, { title: release.getStatus(), className: kebabCase(release.getStatus()) },
release.getUpdated(), release.getUpdated(),
] ];
}} }}
renderItemMenu={(release: HelmRelease) => { renderItemMenu={(release: HelmRelease): JSX.Element => {
return ( return (
<HelmReleaseMenu <HelmReleaseMenu
release={release} release={release}
removeConfirmationMessage={this.renderRemoveDialogMessage([release])} removeConfirmationMessage={this.renderRemoveDialogMessage([release])}
/> />
) );
}} }}
customizeRemoveDialog={(selectedItems: HelmRelease[]) => ({ customizeRemoveDialog={(selectedItems: HelmRelease[]): {message: JSX.Element} => ({
message: this.renderRemoveDialogMessage(selectedItems) message: this.renderRemoveDialogMessage(selectedItems)
})} })}
detailsItem={this.selectedRelease} detailsItem={this.selectedRelease}

View File

@ -24,10 +24,10 @@ export class Apps extends React.Component {
url: releaseURL({ query }), url: releaseURL({ query }),
path: releaseRoute.path, path: releaseRoute.path,
}, },
] ];
} }
render() { render(): JSX.Element {
const tabRoutes = Apps.tabRoutes; const tabRoutes = Apps.tabRoutes;
return ( return (
<MainLayout className="Apps" tabs={tabRoutes}> <MainLayout className="Apps" tabs={tabRoutes}>
@ -36,6 +36,6 @@ export class Apps extends React.Component {
<Redirect to={tabRoutes[0].url}/> <Redirect to={tabRoutes[0].url}/>
</Switch> </Switch>
</MainLayout> </MainLayout>
) );
} }
} }

View File

@ -1,4 +1,4 @@
import "./cluster-issues.scss" import "./cluster-issues.scss";
import * as React from "react"; import * as React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
@ -20,7 +20,7 @@ interface Props {
className?: string; className?: string;
} }
interface IWarning extends ItemObject { interface Warning extends ItemObject {
kind: string; kind: string;
message: string; message: string;
selfLink: string; selfLink: string;
@ -34,16 +34,16 @@ enum sortBy {
@observer @observer
export class ClusterIssues extends React.Component<Props> { export class ClusterIssues extends React.Component<Props> {
private sortCallbacks = { private sortCallbacks = {
[sortBy.type]: (warning: IWarning) => warning.kind, [sortBy.type]: (warning: Warning): string => warning.kind,
[sortBy.object]: (warning: IWarning) => warning.getName(), [sortBy.object]: (warning: Warning): string => warning.getName(),
}; };
@computed get warnings() { @computed get warnings(): Warning[] {
const warnings: IWarning[] = []; const warnings: Warning[] = [];
// Node bad conditions // Node bad conditions
nodesStore.items.forEach(node => { nodesStore.items.forEach(node => {
const { kind, selfLink, getId, getName } = node const { kind, selfLink, getId, getName } = node;
node.getWarningConditions().forEach(({ message }) => { node.getWarningConditions().forEach(({ message }) => {
warnings.push({ warnings.push({
kind, kind,
@ -51,8 +51,8 @@ export class ClusterIssues extends React.Component<Props> {
getName, getName,
selfLink, selfLink,
message, message,
}) });
}) });
}); });
// Warning events for Workloads // Warning events for Workloads
@ -67,13 +67,13 @@ export class ClusterIssues extends React.Component<Props> {
kind, kind,
selfLink: lookupApiLink(involvedObject, error), selfLink: lookupApiLink(involvedObject, error),
}); });
}) });
return warnings; return warnings;
} }
@autobind() @autobind()
getTableRow(uid: string) { getTableRow(uid: string): JSX.Element {
const { warnings } = this; const { warnings } = this;
const warning = warnings.find(warn => warn.getId() == uid); const warning = warnings.find(warn => warn.getId() == uid);
const { getId, getName, message, kind, selfLink } = warning; const { getId, getName, message, kind, selfLink } = warning;
@ -97,7 +97,7 @@ export class ClusterIssues extends React.Component<Props> {
); );
} }
renderContent() { renderContent(): JSX.Element {
const { warnings } = this; const { warnings } = this;
if (!eventStore.isLoaded) { if (!eventStore.isLoaded) {
return ( return (
@ -139,7 +139,7 @@ export class ClusterIssues extends React.Component<Props> {
); );
} }
render() { render(): JSX.Element {
return ( return (
<div className={cssNames("ClusterIssues flex column", this.props.className)}> <div className={cssNames("ClusterIssues flex column", this.props.className)}>
{this.renderContent()} {this.renderContent()}

View File

@ -21,7 +21,9 @@ export const ClusterMetricSwitchers = observer(() => {
asButtons asButtons
className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })} className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })}
value={metricNodeRole} value={metricNodeRole}
onChange={(metric: MetricNodeRole) => clusterStore.metricNodeRole = metric} onChange={(metric: MetricNodeRole): void => {
clusterStore.metricNodeRole = metric;
}}
> >
<Radio label={<Trans>Master</Trans>} value={MetricNodeRole.MASTER}/> <Radio label={<Trans>Master</Trans>} value={MetricNodeRole.MASTER}/>
<Radio label={<Trans>Worker</Trans>} value={MetricNodeRole.WORKER}/> <Radio label={<Trans>Worker</Trans>} value={MetricNodeRole.WORKER}/>
@ -32,7 +34,9 @@ export const ClusterMetricSwitchers = observer(() => {
asButtons asButtons
className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })} className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })}
value={metricType} value={metricType}
onChange={(value: MetricType) => clusterStore.metricType = value} onChange={(value: MetricType): void => {
clusterStore.metricType = value;
}}
> >
<Radio label={<Trans>CPU</Trans>} value={MetricType.CPU}/> <Radio label={<Trans>CPU</Trans>} value={MetricType.CPU}/>
<Radio label={<Trans>Memory</Trans>} value={MetricType.MEMORY}/> <Radio label={<Trans>Memory</Trans>} value={MetricType.MEMORY}/>

View File

@ -34,13 +34,13 @@ export const ClusterMetrics = observer(() => {
yAxes: [{ yAxes: [{
ticks: { ticks: {
suggestedMax: cpuCapacity, suggestedMax: cpuCapacity,
callback: (value) => value callback: (value): any => value
} }
}] }]
}, },
tooltips: { tooltips: {
callbacks: { callbacks: {
label: ({ index }, data) => { label: ({ index }, data): string => {
const value = data.datasets[0].data[index] as ChartPoint; const value = data.datasets[0].data[index] as ChartPoint;
return value.y.toString(); return value.y.toString();
} }
@ -52,13 +52,13 @@ export const ClusterMetrics = observer(() => {
yAxes: [{ yAxes: [{
ticks: { ticks: {
suggestedMax: memoryCapacity, suggestedMax: memoryCapacity,
callback: (value: string) => !value ? 0 : bytesToUnits(parseInt(value)) callback: (value: string): string | number => !value ? 0 : bytesToUnits(parseInt(value))
} }
}] }]
}, },
tooltips: { tooltips: {
callbacks: { callbacks: {
label: ({ index }, data) => { label: ({ index }, data): string => {
const value = data.datasets[0].data[index] as ChartPoint; const value = data.datasets[0].data[index] as ChartPoint;
return bytesToUnits(parseInt(value.y as string), 3); return bytesToUnits(parseInt(value.y as string), 3);
} }
@ -67,12 +67,12 @@ export const ClusterMetrics = observer(() => {
}; };
const options = metricType === MetricType.CPU ? cpuOptions : memoryOptions; const options = metricType === MetricType.CPU ? cpuOptions : memoryOptions;
const renderMetrics = () => { const renderMetrics = (): JSX.Element => {
if ((!metricValues.length || !liveMetricValues.length) && !metricsLoaded) { if ((!metricValues.length || !liveMetricValues.length) && !metricsLoaded) {
return <Spinner center/>; return <Spinner center/>;
} }
if (!memoryCapacity || !cpuCapacity) { if (!memoryCapacity || !cpuCapacity) {
return <ClusterNoMetrics className="empty"/> return <ClusterNoMetrics className="empty"/>;
} }
return ( return (
<BarChart <BarChart

View File

@ -7,7 +7,7 @@ interface Props {
className: string; className: string;
} }
export function ClusterNoMetrics({ className }: Props) { export function ClusterNoMetrics({ className }: Props): JSX.Element {
return ( return (
<div className={cssNames("ClusterNoMetrics flex column box grow justify-center align-center", className)}> <div className={cssNames("ClusterNoMetrics flex column box grow justify-center align-center", className)}>
<Icon material="info"/> <Icon material="info"/>

View File

@ -17,16 +17,16 @@ import { getMetricLastPoints } from "../../api/endpoints/metrics.api";
export const ClusterPieCharts = observer(() => { export const ClusterPieCharts = observer(() => {
const { i18n } = useLingui(); const { i18n } = useLingui();
const renderLimitWarning = () => { const renderLimitWarning = (): JSX.Element => {
return ( return (
<div className="node-warning flex gaps align-center"> <div className="node-warning flex gaps align-center">
<Icon material="info"/> <Icon material="info"/>
<p><Trans>Specified limits are higher than node capacity!</Trans></p> <p><Trans>Specified limits are higher than node capacity!</Trans></p>
</div> </div>
); );
} };
const renderCharts = () => { const renderCharts = (): JSX.Element => {
const data = getMetricLastPoints(clusterStore.metrics); const data = getMetricLastPoints(clusterStore.metrics);
const { memoryUsage, memoryRequests, memoryCapacity, memoryLimits } = data; const { memoryUsage, memoryRequests, memoryCapacity, memoryLimits } = data;
const { cpuUsage, cpuRequests, cpuCapacity, cpuLimits } = data; const { cpuUsage, cpuRequests, cpuCapacity, cpuLimits } = data;
@ -35,7 +35,9 @@ export const ClusterPieCharts = observer(() => {
const memoryLimitsOverload = memoryLimits > memoryCapacity; const memoryLimitsOverload = memoryLimits > memoryCapacity;
const defaultColor = themeStore.activeTheme.colors.pieChartDefaultColor; const defaultColor = themeStore.activeTheme.colors.pieChartDefaultColor;
if (!memoryCapacity || !cpuCapacity || !podCapacity) return null; if (!memoryCapacity || !cpuCapacity || !podCapacity) {
return null;
}
const cpuData: ChartData = { const cpuData: ChartData = {
datasets: [ datasets: [
{ {
@ -168,9 +170,9 @@ export const ClusterPieCharts = observer(() => {
</div> </div>
</div> </div>
); );
} };
const renderContent = () => { const renderContent = (): JSX.Element => {
const { masterNodes, workerNodes } = nodesStore; const { masterNodes, workerNodes } = nodesStore;
const { metricNodeRole, metricsLoaded } = clusterStore; const { metricNodeRole, metricsLoaded } = clusterStore;
const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes; const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes;
@ -194,11 +196,11 @@ export const ClusterPieCharts = observer(() => {
return <ClusterNoMetrics className="empty"/>; return <ClusterNoMetrics className="empty"/>;
} }
return renderCharts(); return renderCharts();
} };
return ( return (
<div className="ClusterPieCharts flex"> <div className="ClusterPieCharts flex">
{renderContent()} {renderContent()}
</div> </div>
); );
}) });

View File

@ -3,6 +3,6 @@ import { buildURL } from "../../navigation";
export const clusterRoute: RouteProps = { export const clusterRoute: RouteProps = {
path: "/cluster" path: "/cluster"
} };
export const clusterURL = buildURL(clusterRoute.path) export const clusterURL = buildURL(clusterRoute.path);

View File

@ -1,8 +1,8 @@
import { observable, reaction, when } from "mobx"; import { observable, reaction, when } from "mobx";
import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectStore } from "../../kube-object.store";
import { Cluster, clusterApi, IClusterMetrics } from "../../api/endpoints"; import { Cluster, clusterApi, ClusterMetrics } from "../../api/endpoints";
import { autobind, createStorage } from "../../utils"; import { autobind, StorageHelper } from "../../utils";
import { IMetricsReqParams, normalizeMetrics } from "../../api/endpoints/metrics.api"; import { MetricsReqParams, normalizeMetrics, Metrics } from "../../api/endpoints/metrics.api";
import { nodesStore } from "../+nodes/nodes.store"; import { nodesStore } from "../+nodes/nodes.store";
import { apiManager } from "../../api/api-manager"; import { apiManager } from "../../api/api-manager";
@ -20,8 +20,8 @@ export enum MetricNodeRole {
export class ClusterStore extends KubeObjectStore<Cluster> { export class ClusterStore extends KubeObjectStore<Cluster> {
api = clusterApi api = clusterApi
@observable metrics: Partial<IClusterMetrics> = {}; @observable metrics: Partial<ClusterMetrics> = {};
@observable liveMetrics: Partial<IClusterMetrics> = {}; @observable liveMetrics: Partial<ClusterMetrics> = {};
@observable metricsLoaded = false; @observable metricsLoaded = false;
@observable metricType: MetricType; @observable metricType: MetricType;
@observable metricNodeRole: MetricNodeRole; @observable metricNodeRole: MetricNodeRole;
@ -31,18 +31,20 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
this.resetMetrics(); this.resetMetrics();
// sync user setting with local storage // sync user setting with local storage
const storage = createStorage("cluster_metric_switchers", {}); const storage = new StorageHelper("cluster_metric_switchers", {});
Object.assign(this, storage.get()); Object.assign(this, storage.get());
reaction(() => { reaction(() => {
const { metricType, metricNodeRole } = this; const { metricType, metricNodeRole } = this;
return { metricType, metricNodeRole } return { metricType, metricNodeRole };
}, },
settings => storage.set(settings) settings => storage.set(settings)
); );
// auto-update metrics // auto-update metrics
reaction(() => this.metricNodeRole, () => { reaction(() => this.metricNodeRole, () => {
if (!this.metricsLoaded) return; if (!this.metricsLoaded) {
return;
}
this.metrics = {}; this.metrics = {};
this.liveMetrics = {}; this.liveMetrics = {};
this.metricsLoaded = false; this.metricsLoaded = false;
@ -52,29 +54,33 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
// check which node type to select // check which node type to select
reaction(() => nodesStore.items.length, () => { reaction(() => nodesStore.items.length, () => {
const { masterNodes, workerNodes } = nodesStore; const { masterNodes, workerNodes } = nodesStore;
if (!masterNodes.length) this.metricNodeRole = MetricNodeRole.WORKER; if (!masterNodes.length) {
if (!workerNodes.length) this.metricNodeRole = MetricNodeRole.MASTER; this.metricNodeRole = MetricNodeRole.WORKER;
}
if (!workerNodes.length) {
this.metricNodeRole = MetricNodeRole.MASTER;
}
}); });
} }
async loadMetrics(params?: IMetricsReqParams) { async loadMetrics(params?: MetricsReqParams): Promise<ClusterMetrics<Metrics>> {
await when(() => nodesStore.isLoaded); await when(() => nodesStore.isLoaded);
const { masterNodes, workerNodes } = nodesStore; const { masterNodes, workerNodes } = nodesStore;
const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes; const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes;
return clusterApi.getMetrics(nodes.map(node => node.getName()), params); return clusterApi.getMetrics(nodes.map(node => node.getName()), params);
} }
async getAllMetrics() { async getAllMetrics(): Promise<void> {
await this.getMetrics(); await this.getMetrics();
await this.getLiveMetrics(); await this.getLiveMetrics();
this.metricsLoaded = true; this.metricsLoaded = true;
} }
async getMetrics() { async getMetrics(): Promise<void> {
this.metrics = await this.loadMetrics(); this.metrics = await this.loadMetrics();
} }
async getLiveMetrics() { async getLiveMetrics(): Promise<void> {
const step = 3; const step = 3;
const range = 15; const range = 15;
const end = Date.now() / 1000; const end = Date.now() / 1000;
@ -82,25 +88,25 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
this.liveMetrics = await this.loadMetrics({ start, end, step, range }); this.liveMetrics = await this.loadMetrics({ start, end, step, range });
} }
getMetricsValues(source: Partial<IClusterMetrics>): [number, string][] { getMetricsValues(source: Partial<ClusterMetrics>): [number, string][] {
switch (this.metricType) { switch (this.metricType) {
case MetricType.CPU: case MetricType.CPU:
return normalizeMetrics(source.cpuUsage).data.result[0].values return normalizeMetrics(source.cpuUsage).data.result[0].values;
case MetricType.MEMORY: case MetricType.MEMORY:
return normalizeMetrics(source.memoryUsage).data.result[0].values return normalizeMetrics(source.memoryUsage).data.result[0].values;
default: default:
return []; return [];
} }
} }
resetMetrics() { resetMetrics(): void {
this.metrics = {}; this.metrics = {};
this.metricsLoaded = false; this.metricsLoaded = false;
this.metricType = MetricType.CPU; this.metricType = MetricType.CPU;
this.metricNodeRole = MetricNodeRole.WORKER; this.metricNodeRole = MetricNodeRole.WORKER;
} }
reset() { reset(): void {
super.reset(); super.reset();
this.resetMetrics(); this.resetMetrics();
} }

View File

@ -1,4 +1,4 @@
import "./cluster.scss" import "./cluster.scss";
import React from "react"; import React from "react";
import { computed, reaction } from "mobx"; import { computed, reaction } from "mobx";
@ -6,7 +6,7 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { MainLayout } from "../layout/main-layout"; import { MainLayout } from "../layout/main-layout";
import { ClusterIssues } from "./cluster-issues"; import { ClusterIssues } from "./cluster-issues";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import { cssNames, interval, isElectron } from "../../utils"; import { cssNames, IntervalManager, isElectron } from "../../utils";
import { ClusterPieCharts } from "./cluster-pie-charts"; import { ClusterPieCharts } from "./cluster-pie-charts";
import { ClusterMetrics } from "./cluster-metrics"; import { ClusterMetrics } from "./cluster-metrics";
import { nodesStore } from "../+nodes/nodes.store"; import { nodesStore } from "../+nodes/nodes.store";
@ -18,16 +18,16 @@ import { isAllowedResource } from "../../api/rbac";
@observer @observer
export class Cluster extends React.Component { export class Cluster extends React.Component {
private watchers = [ private watchers = [
interval(60, () => clusterStore.getMetrics()), new IntervalManager(60, () => clusterStore.getMetrics()),
interval(20, () => eventStore.loadAll()) new IntervalManager(20, () => eventStore.loadAll())
]; ];
private dependentStores = [nodesStore, podsStore]; private dependentStores = [nodesStore, podsStore];
async componentDidMount() { async componentDidMount(): Promise<void> {
const { dependentStores } = this; const { dependentStores } = this;
if (!isAllowedResource("nodes")) { if (!isAllowedResource("nodes")) {
dependentStores.splice(dependentStores.indexOf(nodesStore), 1) dependentStores.splice(dependentStores.indexOf(nodesStore), 1);
} }
this.watchers.forEach(watcher => watcher.start(true)); this.watchers.forEach(watcher => watcher.start(true));
@ -38,22 +38,22 @@ export class Cluster extends React.Component {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
...dependentStores.map(store => store.subscribe()), ...dependentStores.map(store => store.subscribe()),
() => this.watchers.forEach(watcher => watcher.stop()), (): void => this.watchers.forEach(watcher => watcher.stop()),
reaction( reaction(
() => clusterStore.metricNodeRole, () => clusterStore.metricNodeRole,
() => this.watchers.forEach(watcher => watcher.restart()) () => this.watchers.forEach(watcher => watcher.restart())
) )
]) ]);
} }
@computed get isLoaded() { @computed get isLoaded(): boolean {
return ( return (
nodesStore.isLoaded && nodesStore.isLoaded &&
podsStore.isLoaded podsStore.isLoaded
) );
} }
render() { render(): JSX.Element {
const { isLoaded } = this; const { isLoaded } = this;
return ( return (
<MainLayout> <MainLayout>
@ -68,6 +68,6 @@ export class Cluster extends React.Component {
)} )}
</div> </div>
</MainLayout> </MainLayout>
) );
} }
} }

View File

@ -1,2 +1,2 @@
export * from "./cluster.routes" export * from "./cluster.routes";

View File

@ -7,7 +7,7 @@ import { DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { KubeObjectDetailsProps } from "../kube-object"; import { KubeObjectDetailsProps } from "../kube-object";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { HorizontalPodAutoscaler, hpaApi, HpaMetricType, IHpaMetric } from "../../api/endpoints/hpa.api"; import { HorizontalPodAutoscaler, hpaApi, HpaMetricType, HpaMetric } from "../../api/endpoints/hpa.api";
import { KubeEventDetails } from "../+events/kube-event-details"; import { KubeEventDetails } from "../+events/kube-event-details";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { Table, TableCell, TableHead, TableRow } from "../table"; import { Table, TableCell, TableHead, TableRow } from "../table";
@ -21,10 +21,10 @@ interface Props extends KubeObjectDetailsProps<HorizontalPodAutoscaler> {
@observer @observer
export class HpaDetails extends React.Component<Props> { export class HpaDetails extends React.Component<Props> {
renderMetrics() { renderMetrics(): JSX.Element {
const { object: hpa } = this.props; const { object: hpa } = this.props;
const renderName = (metric: IHpaMetric) => { const renderName = (metric: HpaMetric): JSX.Element => {
switch (metric.type) { switch (metric.type) {
case HpaMetricType.Resource: case HpaMetricType.Resource:
const addition = metric.resource.targetAverageUtilization ? <Trans>(as a percentage of request)</Trans> : ""; const addition = metric.resource.targetAverageUtilization ? <Trans>(as a percentage of request)</Trans> : "";
@ -51,7 +51,7 @@ export class HpaDetails extends React.Component<Props> {
</Trans> </Trans>
); );
} }
} };
return ( return (
<Table> <Table>
@ -60,7 +60,7 @@ export class HpaDetails extends React.Component<Props> {
<TableCell className="metrics"><Trans>Current / Target</Trans></TableCell> <TableCell className="metrics"><Trans>Current / Target</Trans></TableCell>
</TableHead> </TableHead>
{ {
hpa.getMetrics().map((metric, index) => { hpa.spec.metrics.map((metric, index) => {
const name = renderName(metric); const name = renderName(metric);
const values = hpa.getMetricValues(metric); const values = hpa.getMetricValues(metric);
return ( return (
@ -68,16 +68,18 @@ export class HpaDetails extends React.Component<Props> {
<TableCell className="name">{name}</TableCell> <TableCell className="name">{name}</TableCell>
<TableCell className="metrics">{values}</TableCell> <TableCell className="metrics">{values}</TableCell>
</TableRow> </TableRow>
) );
}) })
} }
</Table> </Table>
); );
} }
render() { render(): JSX.Element {
const { object: hpa } = this.props; const { object: hpa } = this.props;
if (!hpa) return; if (!hpa) {
return;
}
const { scaleTargetRef } = hpa.spec; const { scaleTargetRef } = hpa.spec;
return ( return (
<div className="HpaDetails"> <div className="HpaDetails">
@ -92,20 +94,22 @@ export class HpaDetails extends React.Component<Props> {
</DrawerItem> </DrawerItem>
<DrawerItem name={<Trans>Min Pods</Trans>}> <DrawerItem name={<Trans>Min Pods</Trans>}>
{hpa.getMinPods()} {hpa.spec.minReplicas}
</DrawerItem> </DrawerItem>
<DrawerItem name={<Trans>Max Pods</Trans>}> <DrawerItem name={<Trans>Max Pods</Trans>}>
{hpa.getMaxPods()} {hpa.spec.maxReplicas}
</DrawerItem> </DrawerItem>
<DrawerItem name={<Trans>Replicas</Trans>}> <DrawerItem name={<Trans>Replicas</Trans>}>
{hpa.getReplicas()} {hpa.status.currentReplicas}
</DrawerItem> </DrawerItem>
<DrawerItem name={<Trans>Status</Trans>} labelsOnly> <DrawerItem name={<Trans>Status</Trans>} labelsOnly>
{hpa.getConditions().map(({ type, tooltip, isReady }) => { {hpa.getConditions().map(({ type, tooltip, isReady }) => {
if (!isReady) return null; if (!isReady) {
return null;
}
return ( return (
<Badge <Badge
key={type} key={type}
@ -113,7 +117,7 @@ export class HpaDetails extends React.Component<Props> {
tooltip={tooltip} tooltip={tooltip}
className={cssNames({ [type.toLowerCase()]: isReady })} className={cssNames({ [type.toLowerCase()]: isReady })}
/> />
) );
})} })}
</DrawerItem> </DrawerItem>

View File

@ -3,9 +3,9 @@ import { buildURL } from "../../navigation";
export const hpaRoute: RouteProps = { export const hpaRoute: RouteProps = {
path: "/hpa" path: "/hpa"
};
export interface HpaRouteParams {
} }
export interface IHpaRouteParams { export const hpaURL = buildURL<HpaRouteParams>(hpaRoute.path);
}
export const hpaURL = buildURL<IHpaRouteParams>(hpaRoute.path)

View File

@ -1,4 +1,4 @@
import "./hpa.scss" import "./hpa.scss";
import * as React from "react"; import * as React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
@ -6,7 +6,7 @@ import { RouteComponentProps } from "react-router";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu"; import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { KubeObjectListLayout } from "../kube-object"; import { KubeObjectListLayout } from "../kube-object";
import { IHpaRouteParams } from "./hpa.route"; import { HpaRouteParams } from "./hpa.route";
import { HorizontalPodAutoscaler, hpaApi } from "../../api/endpoints/hpa.api"; import { HorizontalPodAutoscaler, hpaApi } from "../../api/endpoints/hpa.api";
import { hpaStore } from "./hpa.store"; import { hpaStore } from "./hpa.store";
import { Badge } from "../badge"; import { Badge } from "../badge";
@ -22,32 +22,32 @@ enum sortBy {
age = "age", age = "age",
} }
interface Props extends RouteComponentProps<IHpaRouteParams> { interface Props extends RouteComponentProps<HpaRouteParams> {
} }
@observer @observer
export class HorizontalPodAutoscalers extends React.Component<Props> { export class HorizontalPodAutoscalers extends React.Component<Props> {
getTargets(hpa: HorizontalPodAutoscaler) { getTargets(hpa: HorizontalPodAutoscaler): JSX.Element {
const metrics = hpa.getMetrics(); const { metrics } = hpa.spec;
const metricsRemainCount = metrics.length - 1; const metricsRemainCount = metrics.length - 1;
const metricsRemain = metrics.length > 1 ? <Trans>{metricsRemainCount} more...</Trans> : null; const metricsRemain = metrics.length > 1 ? <Trans>{metricsRemainCount} more...</Trans> : null;
const metricValues = hpa.getMetricValues(metrics[0]); const metricValues = hpa.getMetricValues(metrics[0]);
return <p>{metricValues} {metricsRemain && "+"}{metricsRemain}</p>; return <p>{metricValues} {metricsRemain && "+"}{metricsRemain}</p>;
} }
render() { render(): JSX.Element {
return ( return (
<KubeObjectListLayout <KubeObjectListLayout
className="HorizontalPodAutoscalers" store={hpaStore} className="HorizontalPodAutoscalers" store={hpaStore}
sortingCallbacks={{ sortingCallbacks={{
[sortBy.name]: (item: HorizontalPodAutoscaler) => item.getName(), [sortBy.name]: (item: HorizontalPodAutoscaler): string => item.getName(),
[sortBy.namespace]: (item: HorizontalPodAutoscaler) => item.getNs(), [sortBy.namespace]: (item: HorizontalPodAutoscaler): string => item.getNs(),
[sortBy.minPods]: (item: HorizontalPodAutoscaler) => item.getMinPods(), [sortBy.minPods]: (item: HorizontalPodAutoscaler): number => item.spec.minReplicas,
[sortBy.maxPods]: (item: HorizontalPodAutoscaler) => item.getMaxPods(), [sortBy.maxPods]: (item: HorizontalPodAutoscaler): number => item.spec.maxReplicas,
[sortBy.replicas]: (item: HorizontalPodAutoscaler) => item.getReplicas() [sortBy.replicas]: (item: HorizontalPodAutoscaler): number => item.status.currentReplicas
}} }}
searchFilters={[ searchFilters={[
(item: HorizontalPodAutoscaler) => item.getSearchFields() (item: HorizontalPodAutoscaler): string[] => item.getSearchFields()
]} ]}
renderHeaderTitle={<Trans>Horizontal Pod Autoscalers</Trans>} renderHeaderTitle={<Trans>Horizontal Pod Autoscalers</Trans>}
renderTableHeader={[ renderTableHeader={[
@ -60,16 +60,18 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age }, { title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
{ title: <Trans>Status</Trans>, className: "status" }, { title: <Trans>Status</Trans>, className: "status" },
]} ]}
renderTableContents={(hpa: HorizontalPodAutoscaler) => [ renderTableContents={(hpa: HorizontalPodAutoscaler): (string | number | React.ReactNode)[] => [
hpa.getName(), hpa.getName(),
hpa.getNs(), hpa.getNs(),
this.getTargets(hpa), this.getTargets(hpa),
hpa.getMinPods(), hpa.spec.minReplicas,
hpa.getMaxPods(), hpa.spec.maxReplicas,
hpa.getReplicas(), hpa.status.currentReplicas,
hpa.getAge(), hpa.getAge(),
hpa.getConditions().map(({ type, tooltip, isReady }) => { hpa.getConditions().map(({ type, tooltip, isReady }) => {
if (!isReady) return null; if (!isReady) {
return null;
}
return ( return (
<Badge <Badge
key={type} key={type}
@ -77,23 +79,23 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
tooltip={tooltip} tooltip={tooltip}
className={cssNames(type.toLowerCase())} className={cssNames(type.toLowerCase())}
/> />
) );
}) })
]} ]}
renderItemMenu={(item: HorizontalPodAutoscaler) => { renderItemMenu={(item: HorizontalPodAutoscaler): JSX.Element => {
return <HpaMenu object={item}/> return <HpaMenu object={item}/>;
}} }}
/> />
); );
} }
} }
export function HpaMenu(props: KubeObjectMenuProps<HorizontalPodAutoscaler>) { export function HpaMenu(props: KubeObjectMenuProps<HorizontalPodAutoscaler>): JSX.Element {
return ( return (
<KubeObjectMenu {...props}/> <KubeObjectMenu {...props}/>
) );
} }
apiManager.registerViews(hpaApi, { apiManager.registerViews(hpaApi, {
Menu: HpaMenu, Menu: HpaMenu,
}) });

View File

@ -1,3 +1,3 @@
export * from "./hpa" export * from "./hpa";
export * from "./hpa-details" export * from "./hpa-details";
export * from "./hpa.route" export * from "./hpa.route";

View File

@ -23,7 +23,7 @@ export class ConfigMapDetails extends React.Component<Props> {
@observable isSaving = false; @observable isSaving = false;
@observable data = observable.map(); @observable data = observable.map();
async componentDidMount() { componentDidMount(): void {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
autorun(() => { autorun(() => {
const { object: configMap } = this.props; const { object: configMap } = this.props;
@ -31,10 +31,10 @@ export class ConfigMapDetails extends React.Component<Props> {
this.data.replace(configMap.data); // refresh this.data.replace(configMap.data); // refresh
} }
}) })
]) ]);
} }
save = async () => { save = async (): Promise<void> => {
const { object: configMap } = this.props; const { object: configMap } = this.props;
try { try {
this.isSaving = true; this.isSaving = true;
@ -49,9 +49,11 @@ export class ConfigMapDetails extends React.Component<Props> {
} }
} }
render() { render(): JSX.Element {
const { object: configMap } = this.props; const { object: configMap } = this.props;
if (!configMap) return null; if (!configMap) {
return null;
}
const data = Object.entries(this.data.toJSON()); const data = Object.entries(this.data.toJSON());
return ( return (
<div className="ConfigMapDetails"> <div className="ConfigMapDetails">
@ -71,11 +73,13 @@ export class ConfigMapDetails extends React.Component<Props> {
theme="round-black" theme="round-black"
className="box grow" className="box grow"
value={value} value={value}
onChange={v => this.data.set(name, v)} onChange={(v): void => {
this.data.set(name, v);
}}
/> />
</div> </div>
</div> </div>
) );
}) })
} }
<Button <Button
@ -96,4 +100,4 @@ export class ConfigMapDetails extends React.Component<Props> {
apiManager.registerViews(configMapApi, { apiManager.registerViews(configMapApi, {
Details: ConfigMapDetails Details: ConfigMapDetails
}) });

View File

@ -3,9 +3,9 @@ import { buildURL } from "../../navigation";
export const configMapsRoute: RouteProps = { export const configMapsRoute: RouteProps = {
path: "/configmaps" path: "/configmaps"
};
export interface ConfigMapsRouteParams {
} }
export interface IConfigMapsRouteParams { export const configMapsURL = buildURL<ConfigMapsRouteParams>(configMapsRoute.path);
}
export const configMapsURL = buildURL<IConfigMapsRouteParams>(configMapsRoute.path);

View File

@ -1,4 +1,4 @@
import "./config-maps.scss" import "./config-maps.scss";
import * as React from "react"; import * as React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
@ -8,7 +8,7 @@ import { configMapsStore } from "./config-maps.store";
import { ConfigMap, configMapApi } from "../../api/endpoints/configmap.api"; import { ConfigMap, configMapApi } from "../../api/endpoints/configmap.api";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu"; import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { KubeObjectListLayout } from "../kube-object"; import { KubeObjectListLayout } from "../kube-object";
import { IConfigMapsRouteParams } from "./config-maps.route"; import { ConfigMapsRouteParams } from "./config-maps.route";
import { apiManager } from "../../api/api-manager"; import { apiManager } from "../../api/api-manager";
enum sortBy { enum sortBy {
@ -18,24 +18,24 @@ enum sortBy {
age = "age", age = "age",
} }
interface Props extends RouteComponentProps<IConfigMapsRouteParams> { interface Props extends RouteComponentProps<ConfigMapsRouteParams> {
} }
@observer @observer
export class ConfigMaps extends React.Component<Props> { export class ConfigMaps extends React.Component<Props> {
render() { render(): JSX.Element {
return ( return (
<KubeObjectListLayout <KubeObjectListLayout
className="ConfigMaps" store={configMapsStore} className="ConfigMaps" store={configMapsStore}
sortingCallbacks={{ sortingCallbacks={{
[sortBy.name]: (item: ConfigMap) => item.getName(), [sortBy.name]: (item: ConfigMap): string => item.getName(),
[sortBy.namespace]: (item: ConfigMap) => item.getNs(), [sortBy.namespace]: (item: ConfigMap): string => item.getNs(),
[sortBy.keys]: (item: ConfigMap) => item.getKeys(), [sortBy.keys]: (item: ConfigMap): string[] => item.getKeys(),
[sortBy.age]: (item: ConfigMap) => item.metadata.creationTimestamp, [sortBy.age]: (item: ConfigMap): string => item.metadata.creationTimestamp,
}} }}
searchFilters={[ searchFilters={[
(item: ConfigMap) => item.getSearchFields(), (item: ConfigMap): string[] => item.getSearchFields(),
(item: ConfigMap) => item.getKeys() (item: ConfigMap): string[] => item.getKeys()
]} ]}
renderHeaderTitle={<Trans>Config Maps</Trans>} renderHeaderTitle={<Trans>Config Maps</Trans>}
renderTableHeader={[ renderTableHeader={[
@ -44,26 +44,26 @@ export class ConfigMaps extends React.Component<Props> {
{ title: <Trans>Keys</Trans>, className: "keys", sortBy: sortBy.keys }, { title: <Trans>Keys</Trans>, className: "keys", sortBy: sortBy.keys },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age }, { title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]} ]}
renderTableContents={(configMap: ConfigMap) => [ renderTableContents={(configMap: ConfigMap): (string | number)[] => [
configMap.getName(), configMap.getName(),
configMap.getNs(), configMap.getNs(),
configMap.getKeys().join(", "), configMap.getKeys().join(", "),
configMap.getAge(), configMap.getAge(),
]} ]}
renderItemMenu={(item: ConfigMap) => { renderItemMenu={(item: ConfigMap): JSX.Element => {
return <ConfigMapMenu object={item}/> return <ConfigMapMenu object={item}/>;
}} }}
/> />
); );
} }
} }
export function ConfigMapMenu(props: KubeObjectMenuProps<ConfigMap>) { export function ConfigMapMenu(props: KubeObjectMenuProps<ConfigMap>): JSX.Element {
return ( return (
<KubeObjectMenu {...props}/> <KubeObjectMenu {...props}/>
) );
} }
apiManager.registerViews(configMapApi, { apiManager.registerViews(configMapApi, {
Menu: ConfigMapMenu, Menu: ConfigMapMenu,
}) });

View File

@ -1,3 +1,3 @@
export * from "./config-maps.route" export * from "./config-maps.route";
export * from "./config-maps" export * from "./config-maps";
export * from "./config-map-details" export * from "./config-map-details";

View File

@ -9,7 +9,7 @@ import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard"; import { Wizard, WizardStep } from "../wizard";
import { Input } from "../input"; import { Input } from "../input";
import { systemName } from "../input/input.validators"; import { systemName } from "../input/input.validators";
import { IResourceQuotaValues, resourceQuotaApi } from "../../api/endpoints/resource-quota.api"; import { ResourceQuotaValues, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
import { Select } from "../select"; import { Select } from "../select";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Button } from "../button"; import { Button } from "../button";
@ -20,11 +20,16 @@ import { SubTitle } from "../layout/sub-title";
interface Props extends DialogProps { interface Props extends DialogProps {
} }
export interface QuotaOption {
label: string | JSX.Element;
value: string;
}
@observer @observer
export class AddQuotaDialog extends React.Component<Props> { export class AddQuotaDialog extends React.Component<Props> {
@observable static isOpen = false; @observable static isOpen = false;
static defaultQuotas: IResourceQuotaValues = { static defaultQuotas: ResourceQuotaValues = {
"limits.cpu": "", "limits.cpu": "",
"limits.memory": "", "limits.memory": "",
"requests.cpu": "", "requests.cpu": "",
@ -53,43 +58,45 @@ export class AddQuotaDialog extends React.Component<Props> {
@observable namespace = this.defaultNamespace; @observable namespace = this.defaultNamespace;
@observable quotas = AddQuotaDialog.defaultQuotas; @observable quotas = AddQuotaDialog.defaultQuotas;
static open() { static open(): void {
AddQuotaDialog.isOpen = true; AddQuotaDialog.isOpen = true;
} }
static close() { static close(): void {
AddQuotaDialog.isOpen = false; AddQuotaDialog.isOpen = false;
} }
@computed get quotaEntries() { @computed get quotaEntries(): [string, string][] {
return Object.entries(this.quotas) return Object.entries(this.quotas)
.filter(([type, value]) => !!value.trim()); .filter(([_type, value]) => !!value.trim());
} }
@computed get quotaOptions() { @computed get quotaOptions(): QuotaOption[] {
return Object.keys(this.quotas).map(quota => { return Object.keys(this.quotas).map(value => {
const isCompute = quota.endsWith(".cpu") || quota.endsWith(".memory"); const isCompute = value.endsWith(".cpu") || value.endsWith(".memory");
const isStorage = quota.endsWith(".storage") || quota === "persistentvolumeclaims"; const isStorage = value.endsWith(".storage") || value === "persistentvolumeclaims";
const isCount = quota.startsWith("count/"); const isCount = value.startsWith("count/");
const icon = isCompute ? "memory" : isStorage ? "storage" : isCount ? "looks_one" : ""; const icon = isCompute ? "memory" : isStorage ? "storage" : isCount ? "looks_one" : "";
return { return {
label: icon ? <span className="nobr"><Icon material={icon}/> {quota}</span> : quota, label: icon ? <span className="nobr"><Icon material={icon}/> {value}</span> : value,
value: quota, value,
}; };
}); });
} }
setQuota = () => { setQuota = (): void => {
if (!this.quotaSelectValue) return; if (!this.quotaSelectValue) {
return;
}
this.quotas[this.quotaSelectValue] = this.quotaInputValue; this.quotas[this.quotaSelectValue] = this.quotaInputValue;
this.quotaInputValue = ""; this.quotaInputValue = "";
} }
close = () => { close = (): void => {
AddQuotaDialog.close(); AddQuotaDialog.close();
} }
reset = () => { reset = (): void => {
this.quotaName = ""; this.quotaName = "";
this.quotaSelectValue = ""; this.quotaSelectValue = "";
this.quotaInputValue = ""; this.quotaInputValue = "";
@ -97,10 +104,10 @@ export class AddQuotaDialog extends React.Component<Props> {
this.quotas = AddQuotaDialog.defaultQuotas; this.quotas = AddQuotaDialog.defaultQuotas;
} }
addQuota = async () => { addQuota = async (): Promise<void> => {
try { try {
const { quotaName, namespace } = this; const { quotaName, namespace } = this;
const quotas = this.quotaEntries.reduce<IResourceQuotaValues>((quotas, [name, value]) => { const quotas = this.quotaEntries.reduce<ResourceQuotaValues>((quotas, [name, value]) => {
quotas[name] = value; quotas[name] = value;
return quotas; return quotas;
}, {}); }, {});
@ -115,7 +122,7 @@ export class AddQuotaDialog extends React.Component<Props> {
} }
} }
onInputQuota = (evt: React.KeyboardEvent) => { onInputQuota = (evt: React.KeyboardEvent): void => {
switch (evt.key) { switch (evt.key) {
case "Enter": case "Enter":
this.setQuota(); this.setQuota();
@ -124,7 +131,7 @@ export class AddQuotaDialog extends React.Component<Props> {
} }
} }
render() { render(): JSX.Element {
const { ...dialogProps } = this.props; const { ...dialogProps } = this.props;
const header = <h5><Trans>Create ResourceQuota</Trans></h5>; const header = <h5><Trans>Create ResourceQuota</Trans></h5>;
return ( return (
@ -146,7 +153,9 @@ export class AddQuotaDialog extends React.Component<Props> {
required autoFocus required autoFocus
placeholder={_i18n._(t`ResourceQuota name`)} placeholder={_i18n._(t`ResourceQuota name`)}
validators={systemName} validators={systemName}
value={this.quotaName} onChange={v => this.quotaName = v.toLowerCase()} value={this.quotaName} onChange={(v): void => {
this.quotaName = v.toLowerCase();
}}
className="box grow" className="box grow"
/> />
</div> </div>
@ -157,7 +166,7 @@ export class AddQuotaDialog extends React.Component<Props> {
placeholder={_i18n._(t`Namespace`)} placeholder={_i18n._(t`Namespace`)}
themeName="light" themeName="light"
className="box grow" className="box grow"
onChange={({ value }) => this.namespace = value} onChange={({ value }): void => this.namespace = value}
/> />
<SubTitle title={<Trans>Values</Trans>}/> <SubTitle title={<Trans>Values</Trans>}/>
@ -168,13 +177,15 @@ export class AddQuotaDialog extends React.Component<Props> {
placeholder={_i18n._(t`Select a quota..`)} placeholder={_i18n._(t`Select a quota..`)}
options={this.quotaOptions} options={this.quotaOptions}
value={this.quotaSelectValue} value={this.quotaSelectValue}
onChange={({ value }) => this.quotaSelectValue = value} onChange={({ value }): void => this.quotaSelectValue = value}
/> />
<Input <Input
maxLength={10} maxLength={10}
placeholder={_i18n._(t`Value`)} placeholder={_i18n._(t`Value`)}
value={this.quotaInputValue} value={this.quotaInputValue}
onChange={v => this.quotaInputValue = v} onChange={(v): void => {
this.quotaInputValue = v;
}}
onKeyDown={this.onInputQuota} onKeyDown={this.onInputQuota}
className="box grow" className="box grow"
/> />
@ -191,14 +202,16 @@ export class AddQuotaDialog extends React.Component<Props> {
<div key={quota} className="quota flex gaps inline align-center"> <div key={quota} className="quota flex gaps inline align-center">
<div className="name">{quota}</div> <div className="name">{quota}</div>
<div className="value">{value}</div> <div className="value">{value}</div>
<Icon material="clear" onClick={() => this.quotas[quota] = ""}/> <Icon material="clear" onClick={(): void => {
this.quotas[quota] = "";
}}/>
</div> </div>
) );
})} })}
</div> </div>
</WizardStep> </WizardStep>
</Wizard> </Wizard>
</Dialog> </Dialog>
) );
} }
} }

View File

@ -1,3 +1,3 @@
export * from "./resource-quotas.route" export * from "./resource-quotas.route";
export * from "./resource-quotas" export * from "./resource-quotas";
export * from "./resource-quota-details" export * from "./resource-quota-details";

View File

@ -17,22 +17,26 @@ interface Props extends KubeObjectDetailsProps<ResourceQuota> {
@observer @observer
export class ResourceQuotaDetails extends React.Component<Props> { export class ResourceQuotaDetails extends React.Component<Props> {
renderQuotas = (quota: ResourceQuota) => { renderQuotas = (quota: ResourceQuota): JSX.Element[] => {
const { hard, used } = quota.status const { hard, used } = quota.status;
if (!hard || !used) return null if (!hard || !used) {
const transformUnit = (name: string, value: string) => { return null;
}
const transformUnit = (name: string, value: string): number => {
if (name.includes("memory") || name.includes("storage")) { if (name.includes("memory") || name.includes("storage")) {
return unitsToBytes(value) return unitsToBytes(value);
} }
if (name.includes("cpu")) { if (name.includes("cpu")) {
return cpuUnitsToNumber(value) return cpuUnitsToNumber(value);
} }
return parseInt(value) return parseInt(value);
} };
return Object.entries(hard).map(([name, value]) => { return Object.entries(hard).map(([name, value]) => {
if (!used[name]) return null if (!used[name]) {
const current = transformUnit(name, used[name]) return null;
const max = transformUnit(name, value) }
const current = transformUnit(name, used[name]);
const max = transformUnit(name, value);
return ( return (
<div key={name} className={cssNames("param", kebabCase(name))}> <div key={name} className={cssNames("param", kebabCase(name))}>
<span className="title">{name}</span> <span className="title">{name}</span>
@ -45,13 +49,15 @@ export class ResourceQuotaDetails extends React.Component<Props> {
} }
/> />
</div> </div>
) );
}) });
} }
render() { render(): JSX.Element {
const { object: quota } = this.props; const { object: quota } = this.props;
if (!quota) return null; if (!quota) {
return null;
}
return ( return (
<div className="ResourceQuotaDetails"> <div className="ResourceQuotaDetails">
<KubeObjectMeta object={quota}/> <KubeObjectMeta object={quota}/>
@ -91,4 +97,4 @@ export class ResourceQuotaDetails extends React.Component<Props> {
apiManager.registerViews(resourceQuotaApi, { apiManager.registerViews(resourceQuotaApi, {
Details: ResourceQuotaDetails Details: ResourceQuotaDetails
}) });

View File

@ -3,9 +3,9 @@ import { buildURL } from "../../navigation";
export const resourceQuotaRoute: RouteProps = { export const resourceQuotaRoute: RouteProps = {
path: "/resourcequotas" path: "/resourcequotas"
};
export interface ResourceQuotaRouteParams {
} }
export interface IResourceQuotaRouteParams { export const resourceQuotaURL = buildURL<ResourceQuotaRouteParams>(resourceQuotaRoute.path);
}
export const resourceQuotaURL = buildURL<IResourceQuotaRouteParams>(resourceQuotaRoute.path);

View File

@ -9,7 +9,7 @@ import { KubeObjectListLayout } from "../kube-object";
import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api"; import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
import { AddQuotaDialog } from "./add-quota-dialog"; import { AddQuotaDialog } from "./add-quota-dialog";
import { resourceQuotaStore } from "./resource-quotas.store"; import { resourceQuotaStore } from "./resource-quotas.store";
import { IResourceQuotaRouteParams } from "./resource-quotas.route"; import { ResourceQuotaRouteParams } from "./resource-quotas.route";
import { apiManager } from "../../api/api-manager"; import { apiManager } from "../../api/api-manager";
enum sortBy { enum sortBy {
@ -18,24 +18,24 @@ enum sortBy {
age = "age" age = "age"
} }
interface Props extends RouteComponentProps<IResourceQuotaRouteParams> { interface Props extends RouteComponentProps<ResourceQuotaRouteParams> {
} }
@observer @observer
export class ResourceQuotas extends React.Component<Props> { export class ResourceQuotas extends React.Component<Props> {
render() { render(): JSX.Element {
return ( return (
<> <>
<KubeObjectListLayout <KubeObjectListLayout
className="ResourceQuotas" store={resourceQuotaStore} className="ResourceQuotas" store={resourceQuotaStore}
sortingCallbacks={{ sortingCallbacks={{
[sortBy.name]: (item: ResourceQuota) => item.getName(), [sortBy.name]: (item: ResourceQuota): string => item.getName(),
[sortBy.namespace]: (item: ResourceQuota) => item.getNs(), [sortBy.namespace]: (item: ResourceQuota): string => item.getNs(),
[sortBy.age]: (item: ResourceQuota) => item.metadata.creationTimestamp, [sortBy.age]: (item: ResourceQuota): string => item.metadata.creationTimestamp,
}} }}
searchFilters={[ searchFilters={[
(item: ResourceQuota) => item.getSearchFields(), (item: ResourceQuota): string[] => item.getSearchFields(),
(item: ResourceQuota) => item.getName(), (item: ResourceQuota): string => item.getName(),
]} ]}
renderHeaderTitle={<Trans>Resource Quotas</Trans>} renderHeaderTitle={<Trans>Resource Quotas</Trans>}
renderTableHeader={[ renderTableHeader={[
@ -43,16 +43,14 @@ export class ResourceQuotas extends React.Component<Props> {
{ title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace }, { title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age }, { title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]} ]}
renderTableContents={(resourceQuota: ResourceQuota) => [ renderTableContents={(resourceQuota: ResourceQuota): (string | number)[] => [
resourceQuota.getName(), resourceQuota.getName(),
resourceQuota.getNs(), resourceQuota.getNs(),
resourceQuota.getAge(), resourceQuota.getAge(),
]} ]}
renderItemMenu={(item: ResourceQuota) => { renderItemMenu={(item: ResourceQuota): JSX.Element => <ResourceQuotaMenu object={item} />}
return <ResourceQuotaMenu object={item}/>
}}
addRemoveButtons={{ addRemoveButtons={{
onAdd: () => AddQuotaDialog.open(), onAdd: (): void => AddQuotaDialog.open(),
addTooltip: <Trans>Create new ResourceQuota</Trans> addTooltip: <Trans>Create new ResourceQuota</Trans>
}} }}
/> />
@ -62,7 +60,7 @@ export class ResourceQuotas extends React.Component<Props> {
} }
} }
export function ResourceQuotaMenu(props: KubeObjectMenuProps<ResourceQuota>) { export function ResourceQuotaMenu(props: KubeObjectMenuProps<ResourceQuota>): JSX.Element {
return ( return (
<KubeObjectMenu {...props}/> <KubeObjectMenu {...props}/>
); );
@ -70,4 +68,4 @@ export function ResourceQuotaMenu(props: KubeObjectMenuProps<ResourceQuota>) {
apiManager.registerViews(resourceQuotaApi, { apiManager.registerViews(resourceQuotaApi, {
Menu: ResourceQuotaMenu, Menu: ResourceQuotaMenu,
}) });

View File

@ -1,4 +1,4 @@
import "./add-secret-dialog.scss" import "./add-secret-dialog.scss";
import React from "react"; import React from "react";
import { observable } from "mobx"; import { observable } from "mobx";
@ -14,7 +14,7 @@ import { SubTitle } from "../layout/sub-title";
import { NamespaceSelect } from "../+namespaces/namespace-select"; import { NamespaceSelect } from "../+namespaces/namespace-select";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../select";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { IKubeObjectMetadata } from "../../api/kube-object"; import { KubeObjectMetadata } from "../../api/kube-object";
import { base64 } from "../../utils"; import { base64 } from "../../utils";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { showDetails } from "../../navigation"; import { showDetails } from "../../navigation";
@ -23,34 +23,41 @@ import upperFirst from "lodash/upperFirst";
interface Props extends Partial<DialogProps> { interface Props extends Partial<DialogProps> {
} }
interface ISecretTemplateField { interface SecretTemplateField {
key: string; key: string;
value?: string; value?: string;
required?: boolean; required?: boolean;
} }
interface ISecretTemplate { interface SecretTemplate {
[field: string]: ISecretTemplateField[]; [field: string]: SecretTemplateField[];
annotations?: ISecretTemplateField[]; annotations?: SecretTemplateField[];
labels?: ISecretTemplateField[]; labels?: SecretTemplateField[];
data?: ISecretTemplateField[]; data?: SecretTemplateField[];
} }
type ISecretField = keyof ISecretTemplate; type SecretField = keyof SecretTemplate;
type RecursivePartial<T> = {
[P in keyof T]?:
T[P] extends (infer U)[] ? RecursivePartial<U>[] :
T[P] extends object ? RecursivePartial<T[P]> :
T[P];
};
@observer @observer
export class AddSecretDialog extends React.Component<Props> { export class AddSecretDialog extends React.Component<Props> {
@observable static isOpen = false; @observable static isOpen = false;
static open() { static open(): void {
AddSecretDialog.isOpen = true; AddSecretDialog.isOpen = true;
} }
static close() { static close(): void {
AddSecretDialog.isOpen = false; AddSecretDialog.isOpen = false;
} }
private secretTemplate: { [p: string]: ISecretTemplate } = { private secretTemplate: Partial<Record<SecretType, SecretTemplate>> = {
[SecretType.Opaque]: {}, [SecretType.Opaque]: {},
[SecretType.ServiceAccountToken]: { [SecretType.ServiceAccountToken]: {
annotations: [ annotations: [
@ -60,7 +67,7 @@ export class AddSecretDialog extends React.Component<Props> {
}, },
} }
get types() { get types(): SecretType[] {
return Object.keys(this.secretTemplate) as SecretType[]; return Object.keys(this.secretTemplate) as SecretType[];
} }
@ -69,38 +76,39 @@ export class AddSecretDialog extends React.Component<Props> {
@observable namespace = "default"; @observable namespace = "default";
@observable type = SecretType.Opaque; @observable type = SecretType.Opaque;
reset = () => { reset = (): void => {
this.name = ""; this.name = "";
this.secret = this.secretTemplate; this.secret = this.secretTemplate;
} }
close = () => { close = (): void => {
AddSecretDialog.close(); AddSecretDialog.close();
} }
private getDataFromFields = (fields: ISecretTemplateField[] = [], processValue?: (val: string) => string) => { private getDataFromFields = (fields: SecretTemplateField[] = [], processValue?: (val: string) => string): any => {
return fields.reduce<any>((data, field) => { return fields.reduce<any>((data, field) => {
const { key, value } = field; const { key, value } = field;
if (key) { if (key) {
data[key] = processValue ? processValue(value) : value; data[key] = processValue ? processValue(value) : value;
} }
return data; return data;
}, {}) }, {});
} }
createSecret = async () => { createSecret = async (): Promise<void> => {
const { name, namespace, type } = this; const { name, namespace, type } = this;
const { data = [], labels = [], annotations = [] } = this.secret[type]; const { data = [], labels = [], annotations = [] } = this.secret[type];
const metadata: Partial<KubeObjectMetadata> = {
name,
namespace,
annotations: this.getDataFromFields(annotations),
labels: this.getDataFromFields(labels),
};
const secret: Partial<Secret> = { const secret: Partial<Secret> = {
type: type, type: type,
data: this.getDataFromFields(data, val => val ? base64.encode(val) : ""), data: this.getDataFromFields(data, val => val ? base64.encode(val) : ""),
metadata: { metadata: metadata as KubeObjectMetadata,
name: name, };
namespace: namespace,
annotations: this.getDataFromFields(annotations),
labels: this.getDataFromFields(labels),
} as IKubeObjectMetadata
}
try { try {
const newSecret = await secretsApi.create({ namespace, name }, secret); const newSecret = await secretsApi.create({ namespace, name }, secret);
showDetails(newSecret.selfLink); showDetails(newSecret.selfLink);
@ -111,18 +119,18 @@ export class AddSecretDialog extends React.Component<Props> {
} }
} }
addField = (field: ISecretField) => { addField = (field: SecretField): void => {
const fields = this.secret[this.type][field] || []; const fields = this.secret[this.type][field] || [];
fields.push({ key: "", value: "" }); fields.push({ key: "", value: "" });
this.secret[this.type][field] = fields; this.secret[this.type][field] = fields;
} }
removeField = (field: ISecretField, index: number) => { removeField = (field: SecretField, index: number): void => {
const fields = this.secret[this.type][field] || []; const fields = this.secret[this.type][field] || [];
fields.splice(index, 1); fields.splice(index, 1);
} }
renderFields(field: ISecretField) { renderFields(field: SecretField): JSX.Element {
const fields = this.secret[this.type][field] || []; const fields = this.secret[this.type][field] || [];
return ( return (
<> <>
@ -131,7 +139,7 @@ export class AddSecretDialog extends React.Component<Props> {
small small
tooltip={_i18n._(t`Add field`)} tooltip={_i18n._(t`Add field`)}
material="add_circle_outline" material="add_circle_outline"
onClick={() => this.addField(field)} onClick={(): void => this.addField(field)}
/> />
</SubTitle> </SubTitle>
<div className="secret-fields"> <div className="secret-fields">
@ -145,14 +153,18 @@ export class AddSecretDialog extends React.Component<Props> {
title={key} title={key}
tabIndex={required ? -1 : 0} tabIndex={required ? -1 : 0}
readOnly={required} readOnly={required}
value={key} onChange={v => item.key = v} value={key} onChange={(v): void => {
item.key = v;
}}
/> />
<Input <Input
multiLine maxRows={5} multiLine maxRows={5}
required={required} required={required}
className="value" className="value"
placeholder={_i18n._(t`Value`)} placeholder={_i18n._(t`Value`)}
value={value} onChange={v => item.value = v} value={value} onChange={(v): void => {
item.value = v;
}}
/> />
<Icon <Icon
small small
@ -160,17 +172,17 @@ export class AddSecretDialog extends React.Component<Props> {
tooltip={required ? <Trans>Required field</Trans> : <Trans>Remove field</Trans>} tooltip={required ? <Trans>Required field</Trans> : <Trans>Remove field</Trans>}
className="remove-icon" className="remove-icon"
material="remove_circle_outline" material="remove_circle_outline"
onClick={() => this.removeField(field, index)} onClick={(): void => this.removeField(field, index)}
/> />
</div> </div>
) );
})} })}
</div> </div>
</> </>
) );
} }
render() { render(): JSX.Element {
const { ...dialogProps } = this.props; const { ...dialogProps } = this.props;
const { namespace, name, type } = this; const { namespace, name, type } = this;
const header = <h5><Trans>Create Secret</Trans></h5>; const header = <h5><Trans>Create Secret</Trans></h5>;
@ -189,7 +201,9 @@ export class AddSecretDialog extends React.Component<Props> {
autoFocus required autoFocus required
placeholder={_i18n._(t`Name`)} placeholder={_i18n._(t`Name`)}
validators={systemName} validators={systemName}
value={name} onChange={v => this.name = v} value={name} onChange={(v): void => {
this.name = v;
}}
/> />
</div> </div>
<div className="flex auto gaps"> <div className="flex auto gaps">
@ -198,7 +212,7 @@ export class AddSecretDialog extends React.Component<Props> {
<NamespaceSelect <NamespaceSelect
themeName="light" themeName="light"
value={namespace} value={namespace}
onChange={({ value }) => this.namespace = value} onChange={({ value }): void => this.namespace = value}
/> />
</div> </div>
<div className="secret-type"> <div className="secret-type">
@ -206,7 +220,7 @@ export class AddSecretDialog extends React.Component<Props> {
<Select <Select
themeName="light" themeName="light"
options={this.types} options={this.types}
value={type} onChange={({ value }: SelectOption) => this.type = value} value={type} onChange={({ value }: SelectOption): void => this.type = value}
/> />
</div> </div>
</div> </div>
@ -216,6 +230,6 @@ export class AddSecretDialog extends React.Component<Props> {
</WizardStep> </WizardStep>
</Wizard> </Wizard>
</Dialog> </Dialog>
) );
} }
} }

View File

@ -1,4 +1,4 @@
export * from "./secrets.route" export * from "./secrets.route";
export * from "./secrets" export * from "./secrets";
export * from "./secret-details" export * from "./secret-details";

View File

@ -27,7 +27,7 @@ export class SecretDetails extends React.Component<Props> {
@observable data: { [name: string]: string } = {}; @observable data: { [name: string]: string } = {};
@observable revealSecret: { [name: string]: boolean } = {}; @observable revealSecret: { [name: string]: boolean } = {};
async componentDidMount() { componentDidMount(): void {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
autorun(() => { autorun(() => {
const { object: secret } = this.props; const { object: secret } = this.props;
@ -36,10 +36,10 @@ export class SecretDetails extends React.Component<Props> {
this.revealSecret = {}; this.revealSecret = {};
} }
}) })
]) ]);
} }
saveSecret = async () => { saveSecret = async (): Promise<void> => {
const { object: secret } = this.props; const { object: secret } = this.props;
this.isSaving = true; this.isSaving = true;
try { try {
@ -51,13 +51,15 @@ export class SecretDetails extends React.Component<Props> {
this.isSaving = false; this.isSaving = false;
} }
editData = (name: string, value: string, encoded: boolean) => { editData = (name: string, value: string, encoded: boolean): void => {
this.data[name] = encoded ? value : base64.encode(value); this.data[name] = encoded ? value : base64.encode(value);
} }
render() { render(): JSX.Element {
const { object: secret } = this.props; const { object: secret } = this.props;
if (!secret) return null; if (!secret) {
return null;
}
return ( return (
<div className="SecretDetails"> <div className="SecretDetails">
<KubeObjectMeta object={secret}/> <KubeObjectMeta object={secret}/>
@ -86,18 +88,20 @@ export class SecretDetails extends React.Component<Props> {
theme="round-black" theme="round-black"
className="box grow" className="box grow"
value={value || ""} value={value || ""}
onChange={value => this.editData(name, value, !revealSecret)} onChange={(value): void => this.editData(name, value, !revealSecret)}
/> />
{decodedVal && ( {decodedVal && (
<Icon <Icon
material={`visibility${revealSecret ? "" : "_off"}`} material={`visibility${revealSecret ? "" : "_off"}`}
tooltip={revealSecret ? <Trans>Hide</Trans> : <Trans>Show</Trans>} tooltip={revealSecret ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
onClick={() => this.revealSecret[name] = !revealSecret} onClick={(): void => {
this.revealSecret[name] = !revealSecret;
}}
/>) />)
} }
</div> </div>
</div> </div>
) );
}) })
} }
<Button <Button
@ -115,4 +119,4 @@ export class SecretDetails extends React.Component<Props> {
apiManager.registerViews(secretsApi, { apiManager.registerViews(secretsApi, {
Details: SecretDetails, Details: SecretDetails,
}) });

View File

@ -3,9 +3,9 @@ import { buildURL } from "../../navigation";
export const secretsRoute: RouteProps = { export const secretsRoute: RouteProps = {
path: "/secrets" path: "/secrets"
} };
export interface ISecretsRouteParams { export interface SecretsRouteParams {
} }
export const secretsURL = buildURL(secretsRoute.path); export const secretsURL = buildURL(secretsRoute.path);

View File

@ -1,4 +1,4 @@
import "./secrets.scss" import "./secrets.scss";
import * as React from "react"; import * as React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
@ -7,7 +7,7 @@ import { RouteComponentProps } from "react-router";
import { Secret, secretsApi } from "../../api/endpoints"; import { Secret, secretsApi } from "../../api/endpoints";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu"; import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { AddSecretDialog } from "./add-secret-dialog"; import { AddSecretDialog } from "./add-secret-dialog";
import { ISecretsRouteParams } from "./secrets.route"; import { SecretsRouteParams } from "./secrets.route";
import { KubeObjectListLayout } from "../kube-object"; import { KubeObjectListLayout } from "../kube-object";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { secretsStore } from "./secrets.store"; import { secretsStore } from "./secrets.store";
@ -22,27 +22,27 @@ enum sortBy {
age = "age", age = "age",
} }
interface Props extends RouteComponentProps<ISecretsRouteParams> { interface Props extends RouteComponentProps<SecretsRouteParams> {
} }
@observer @observer
export class Secrets extends React.Component<Props> { export class Secrets extends React.Component<Props> {
render() { render(): JSX.Element {
return ( return (
<> <>
<KubeObjectListLayout <KubeObjectListLayout
className="Secrets" store={secretsStore} className="Secrets" store={secretsStore}
sortingCallbacks={{ sortingCallbacks={{
[sortBy.name]: (item: Secret) => item.getName(), [sortBy.name]: (item: Secret): string => item.getName(),
[sortBy.namespace]: (item: Secret) => item.getNs(), [sortBy.namespace]: (item: Secret): string => item.getNs(),
[sortBy.labels]: (item: Secret) => item.getLabels(), [sortBy.labels]: (item: Secret): string[] => item.getLabels(),
[sortBy.keys]: (item: Secret) => item.getKeys(), [sortBy.keys]: (item: Secret): string[] => item.getKeys(),
[sortBy.type]: (item: Secret) => item.type, [sortBy.type]: (item: Secret): string => item.type,
[sortBy.age]: (item: Secret) => item.metadata.creationTimestamp, [sortBy.age]: (item: Secret): string => item.metadata.creationTimestamp,
}} }}
searchFilters={[ searchFilters={[
(item: Secret) => item.getSearchFields(), (item: Secret): string[] => item.getSearchFields(),
(item: Secret) => item.getKeys(), (item: Secret): string[] => item.getKeys(),
]} ]}
renderHeaderTitle={<Trans>Secrets</Trans>} renderHeaderTitle={<Trans>Secrets</Trans>}
renderTableHeader={[ renderTableHeader={[
@ -53,7 +53,7 @@ export class Secrets extends React.Component<Props> {
{ title: <Trans>Type</Trans>, className: "type", sortBy: sortBy.type }, { title: <Trans>Type</Trans>, className: "type", sortBy: sortBy.type },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age }, { title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]} ]}
renderTableContents={(secret: Secret) => [ renderTableContents={(secret: Secret): (string | JSX.Element[] | number)[] => [
secret.getName(), secret.getName(),
secret.getNs(), secret.getNs(),
secret.getLabels().map(label => <Badge key={label} label={label}/>), secret.getLabels().map(label => <Badge key={label} label={label}/>),
@ -61,11 +61,11 @@ export class Secrets extends React.Component<Props> {
secret.type, secret.type,
secret.getAge(), secret.getAge(),
]} ]}
renderItemMenu={(item: Secret) => { renderItemMenu={(item: Secret): JSX.Element => {
return <SecretMenu object={item}/> return <SecretMenu object={item}/>;
}} }}
addRemoveButtons={{ addRemoveButtons={{
onAdd: () => AddSecretDialog.open(), onAdd: (): void => AddSecretDialog.open(),
addTooltip: <Trans>Create new Secret</Trans> addTooltip: <Trans>Create new Secret</Trans>
}} }}
/> />
@ -75,12 +75,12 @@ export class Secrets extends React.Component<Props> {
} }
} }
export function SecretMenu(props: KubeObjectMenuProps<Secret>) { export function SecretMenu(props: KubeObjectMenuProps<Secret>): JSX.Element {
return ( return (
<KubeObjectMenu {...props}/> <KubeObjectMenu {...props}/>
) );
} }
apiManager.registerViews(secretsApi, { apiManager.registerViews(secretsApi, {
Menu: SecretMenu, Menu: SecretMenu,
}) });

View File

@ -1,12 +1,12 @@
import { RouteProps } from "react-router"; import { RouteProps } from "react-router";
import { configMapsURL } from "../+config-maps"; import { configMapsURL } from "../+config-maps";
import { Config } from "./config"; import { Config } from "./config";
import { IURLParams } from "../../navigation"; import { URLParams } from "../../navigation";
export const configRoute: RouteProps = { export const configRoute: RouteProps = {
get path() { get path() {
return Config.tabRoutes.map(({ path }) => path).flat() return Config.tabRoutes.map(({ path }) => path).flat();
} }
} };
export const configURL = (params?: IURLParams) => configMapsURL(params); export const configURL = (params?: URLParams): string => configMapsURL(params);

View File

@ -10,7 +10,7 @@ import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config
import { configURL } from "./config.route"; import { configURL } from "./config.route";
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers"; import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
import { buildURL } from "../../navigation"; import { buildURL } from "../../navigation";
import { isAllowedResource } from "../../api/rbac" import { isAllowedResource } from "../../api/rbac";
export const certificatesURL = buildURL("/certificates"); export const certificatesURL = buildURL("/certificates");
export const issuersURL = buildURL("/issuers"); export const issuersURL = buildURL("/issuers");
@ -19,15 +19,15 @@ export const clusterIssuersURL = buildURL("/clusterissuers");
@observer @observer
export class Config extends React.Component { export class Config extends React.Component {
static get tabRoutes(): TabRoute[] { static get tabRoutes(): TabRoute[] {
const query = namespaceStore.getContextParams() const query = namespaceStore.getContextParams();
const routes: TabRoute[] = [] const routes: TabRoute[] = [];
if (isAllowedResource("configmaps")) { if (isAllowedResource("configmaps")) {
routes.push({ routes.push({
title: <Trans>ConfigMaps</Trans>, title: <Trans>ConfigMaps</Trans>,
component: ConfigMaps, component: ConfigMaps,
url: configMapsURL({ query }), url: configMapsURL({ query }),
path: configMapsRoute.path, path: configMapsRoute.path,
}) });
} }
if (isAllowedResource("secrets")) { if (isAllowedResource("secrets")) {
routes.push({ routes.push({
@ -35,7 +35,7 @@ export class Config extends React.Component {
component: Secrets, component: Secrets,
url: secretsURL({ query }), url: secretsURL({ query }),
path: secretsRoute.path, path: secretsRoute.path,
}) });
} }
if (isAllowedResource("resourcequotas")) { if (isAllowedResource("resourcequotas")) {
routes.push({ routes.push({
@ -43,7 +43,7 @@ export class Config extends React.Component {
component: ResourceQuotas, component: ResourceQuotas,
url: resourceQuotaURL({ query }), url: resourceQuotaURL({ query }),
path: resourceQuotaRoute.path, path: resourceQuotaRoute.path,
}) });
} }
if (isAllowedResource("horizontalpodautoscalers")) { if (isAllowedResource("horizontalpodautoscalers")) {
routes.push({ routes.push({
@ -51,12 +51,12 @@ export class Config extends React.Component {
component: HorizontalPodAutoscalers, component: HorizontalPodAutoscalers,
url: hpaURL({ query }), url: hpaURL({ query }),
path: hpaRoute.path, path: hpaRoute.path,
}) });
} }
return routes; return routes;
} }
render() { render(): JSX.Element {
const tabRoutes = Config.tabRoutes; const tabRoutes = Config.tabRoutes;
return ( return (
<MainLayout className="Config" tabs={tabRoutes}> <MainLayout className="Config" tabs={tabRoutes}>
@ -65,6 +65,6 @@ export class Config extends React.Component {
<Redirect to={configURL({ query: namespaceStore.getContextParams() })}/> <Redirect to={configURL({ query: namespaceStore.getContextParams() })}/>
</Switch> </Switch>
</MainLayout> </MainLayout>
) );
} }
} }

View File

@ -1,2 +1,2 @@
export * from "./config.route" export * from "./config.route";
export * from "./config" export * from "./config";

View File

@ -1,7 +1,7 @@
import "./certificate-details.scss" import "./certificate-details.scss";
import React from "react"; import React from "react";
import moment from "moment" import moment from "moment";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
@ -19,9 +19,11 @@ interface Props extends KubeObjectDetailsProps<Certificate> {
@observer @observer
export class CertificateDetails extends React.Component<Props> { export class CertificateDetails extends React.Component<Props> {
render() { render(): JSX.Element {
const { object: cert, className } = this.props; const { object: cert, className } = this.props;
if (!cert) return; if (!cert) {
return;
}
const { spec, status } = cert; const { spec, status } = cert;
const { acme, isCA, commonName, secretName, dnsNames, duration, ipAddresses, keyAlgorithm, keySize, organization, renewBefore } = spec; const { acme, isCA, commonName, secretName, dnsNames, duration, ipAddresses, keyAlgorithm, keySize, organization, renewBefore } = spec;
const { lastFailureTime, notAfter } = status; const { lastFailureTime, notAfter } = status;
@ -104,7 +106,7 @@ export class CertificateDetails extends React.Component<Props> {
tooltip={tooltip} tooltip={tooltip}
className={cssNames({ [type.toLowerCase()]: isReady })} className={cssNames({ [type.toLowerCase()]: isReady })}
/> />
) );
})} })}
</DrawerItem> </DrawerItem>
@ -126,7 +128,7 @@ export class CertificateDetails extends React.Component<Props> {
</DrawerItem> </DrawerItem>
)} )}
</div> </div>
) );
})} })}
</> </>
)} )}
@ -139,4 +141,4 @@ export class CertificateDetails extends React.Component<Props> {
apiManager.registerViews(certificatesApi, { apiManager.registerViews(certificatesApi, {
Details: CertificateDetails Details: CertificateDetails
}) });

Some files were not shown because too many files have changed in this diff Show More