@@ -108,6 +154,8 @@ function MenuItem(item) {
function Group({children = []}) {
if (!Array.isArray(children)) children = [children]; // eslint-disable-line no-param-reassign
+ children = children.filter(Boolean); // eslint-disable-line no-param-reassign
+
if (children.length === 0) return null;
const paths = children.flatMap(x => getPaths(x.props));
diff --git a/client/src/router.js b/client/src/router.js
index 9599bf3..a615653 100644
--- a/client/src/router.js
+++ b/client/src/router.js
@@ -1,5 +1,5 @@
import React from 'react';
-import {hasToken} from './services/apiProxy';
+import {hasToken} from './services/auth';
import Account from './views/account';
import Auth from './views/auth';
import ClusterRole from './views/clusterRole';
diff --git a/client/src/services/api.js b/client/src/services/api.js
index 155df14..b2c0f6e 100644
--- a/client/src/services/api.js
+++ b/client/src/services/api.js
@@ -1,9 +1,35 @@
import _ from 'lodash';
import {Base64} from 'js-base64';
-import {request, stream, streamResult, streamResults} from './apiProxy';
+import {request, post, stream, apiFactory, apiFactoryWithNamespace} from './apiProxy';
import log from '../utils/log';
-const JSON_HEADERS = {Accept: 'application/json', 'Content-Type': 'application/json'};
+const configMap = apiFactoryWithNamespace('', 'v1', 'configmaps');
+const event = apiFactoryWithNamespace('', 'v1', 'events');
+const namespaceService = apiFactory('', 'v1', 'namespaces');
+const node = apiFactory('', 'v1', 'nodes');
+const persistentVolume = apiFactory('', 'v1', 'persistentvolumes');
+const persistentVolumeClaim = apiFactoryWithNamespace('', 'v1', 'persistentvolumeclaims');
+const pod = apiFactoryWithNamespace('', 'v1', 'pods');
+const secret = apiFactoryWithNamespace('', 'v1', 'secrets');
+const serviceAccount = apiFactoryWithNamespace('', 'v1', 'serviceaccounts');
+const serviceService = apiFactoryWithNamespace('', 'v1', 'services');
+
+const clusterRole = apiFactory('rbac.authorization.k8s.io', 'v1', 'clusterroles');
+const clusterRoleBinding = apiFactory('rbac.authorization.k8s.io', 'v1', 'clusterrolebindings');
+const role = apiFactoryWithNamespace('rbac.authorization.k8s.io', 'v1', 'roles');
+const roleBinding = apiFactoryWithNamespace('rbac.authorization.k8s.io', 'v1', 'rolebindings');
+
+const daemonSet = apiFactoryWithNamespace('apps', 'v1', 'daemonsets');
+const deployment = apiFactoryWithNamespace('apps', 'v1', 'deployments', true);
+const replicaSet = apiFactoryWithNamespace('apps', 'v1', 'replicasets', true);
+const statefulSet = apiFactoryWithNamespace('apps', 'v1', 'statefulsets', true);
+
+const cronJob = apiFactoryWithNamespace('batch', 'v1beta1', 'cronjobs');
+const job = apiFactoryWithNamespace('batch', 'v1', 'jobs');
+
+const ingress = apiFactoryWithNamespace('extensions', 'v1beta1', 'ingresses');
+
+const storageClass = apiFactory('storage.k8s.io', 'v1', 'storageclasses');
const apis = {
apply,
@@ -15,31 +41,28 @@ const apis = {
metrics: metricsFactory(),
oidc: oidcFactory(),
- // Non-namespaced apis
- clusterRole: apiFactory('/apis/rbac.authorization.k8s.io/v1', 'clusterroles'),
- namespace: apiFactory('/api/v1', 'namespaces'),
- node: apiFactory('/api/v1', 'nodes'),
- persistentVolume: apiFactory('/api/v1', 'persistentvolumes'),
- storageClass: apiFactory('/apis/storage.k8s.io/v1', 'storageclasses'),
- clusterRoleBinding: apiFactory('/apis/rbac.authorization.k8s.io/v1', 'clusterrolebindings'),
-
- // Namespaced apis
- configMap: apiFactoryWithNamespace('/api/v1', 'configmaps'),
- cronJob: apiFactoryWithNamespace('/apis/batch/v1beta1', 'cronjobs'),
- daemonSet: apiFactoryWithNamespace('/apis/apps/v1', 'daemonsets'),
- deployment: apiFactoryWithNamespace('/apis/apps/v1', 'deployments', true),
- event: apiFactoryWithNamespace('/api/v1', 'events'),
- ingress: apiFactoryWithNamespace('/apis/extensions/v1beta1', 'ingresses'),
- job: apiFactoryWithNamespace('/apis/batch/v1', 'jobs'),
- persistentVolumeClaim: apiFactoryWithNamespace('/api/v1', 'persistentvolumeclaims'),
- pod: apiFactoryWithNamespace('/api/v1', 'pods'),
- replicaSet: apiFactoryWithNamespace('/apis/apps/v1', 'replicasets', true),
- role: apiFactoryWithNamespace('/apis/rbac.authorization.k8s.io/v1', 'roles'),
- secret: apiFactoryWithNamespace('/api/v1', 'secrets'),
- service: apiFactoryWithNamespace('/api/v1', 'services'),
- serviceAccount: apiFactoryWithNamespace('/api/v1', 'serviceaccounts'),
- statefulSet: apiFactoryWithNamespace('/apis/apps/v1', 'statefulsets', true),
- roleBinding: apiFactoryWithNamespace('/apis/rbac.authorization.k8s.io/v1', 'rolebindings'),
+ clusterRole,
+ namespace: namespaceService,
+ node,
+ persistentVolume,
+ storageClass,
+ clusterRoleBinding,
+ configMap,
+ cronJob,
+ daemonSet,
+ deployment,
+ event,
+ ingress,
+ job,
+ persistentVolumeClaim,
+ pod,
+ replicaSet,
+ role,
+ secret,
+ service: serviceService,
+ serviceAccount,
+ statefulSet,
+ roleBinding,
};
async function testAuth() {
@@ -110,48 +133,6 @@ function metrics(url, cb) {
}
}
-function apiFactory(apiType, kind) {
- const url = `${apiType}/${kind}`;
- return {
- list: (cb, errCb) => streamResults(url, cb, errCb),
- get: (name, cb, errCb) => streamResult(url, name, cb, errCb),
- post: body => post(url, body),
- put: body => put(`${url}/${body.metadata.name}`, body),
- delete: name => del(`${url}/${name}`),
- };
-}
-
-function apiFactoryWithNamespace(apiType, kind, includeScale) {
- const results = {
- list: (namespace, cb, errCb) => streamResults(url(namespace), cb, errCb),
- get: (namespace, name, cb, errCb) => streamResult(url(namespace), name, cb, errCb),
- post: body => post(url(body.metadata.namespace), body),
- put: body => put(`${url(body.metadata.namespace)}/${body.metadata.name}`, body),
- delete: (namespace, name) => del(`${url(namespace)}/${name}`),
- };
-
- if (includeScale) {
- results.scale = apiScaleFactory(apiType, kind);
- }
-
- return results;
-
- function url(namespace) {
- return namespace ? `${apiType}/namespaces/${namespace}/${kind}` : `${apiType}/${kind}`;
- }
-}
-
-function apiScaleFactory(apiType, kind) {
- return {
- get: (namespace, name) => request(url(namespace, name)),
- put: body => put(url(body.metadata.namespace, body.metadata.name), body),
- };
-
- function url(namespace, name) {
- return `${apiType}/namespaces/${namespace}/${kind}/${name}/scale`;
- }
-}
-
function swagger() {
return request('/openapi/v2');
}
@@ -181,21 +162,4 @@ function logs(namespace, name, container, tailLines, showPrevious, cb) {
}
}
-function post(url, json, autoLogoutOnAuthError = true) {
- const body = JSON.stringify(json);
- const opts = {method: 'POST', body, headers: JSON_HEADERS};
- return request(url, opts, autoLogoutOnAuthError);
-}
-
-function put(url, json, autoLogoutOnAuthError = true) {
- const body = JSON.stringify(json);
- const opts = {method: 'PUT', body, headers: JSON_HEADERS};
- return request(url, opts, autoLogoutOnAuthError);
-}
-
-function del(url) {
- const opts = {method: 'DELETE', headers: JSON_HEADERS};
- return request(url, opts);
-}
-
export default apis;
diff --git a/client/src/services/apiProxy.js b/client/src/services/apiProxy.js
index 85cfb65..bcd9ef3 100644
--- a/client/src/services/apiProxy.js
+++ b/client/src/services/apiProxy.js
@@ -1,4 +1,4 @@
-import * as cookie from 'js-cookie';
+import {getToken, logout} from './auth';
import log from '../utils/log';
const {host, href, hash, search} = window.location;
@@ -6,38 +6,7 @@ const nonHashedUrl = href.replace(hash, '').replace(search, '');
const isDev = process.env.NODE_ENV !== 'production';
const BASE_HTTP_URL = isDev && host === 'localhost:4653' ? 'http://localhost:4654' : nonHashedUrl;
const BASE_WS_URL = BASE_HTTP_URL.replace('http', 'ws');
-
-const authorizationCookie = cookie.get('Authorization');
-if (authorizationCookie) {
- setToken(authorizationCookie);
- cookie.remove('Authorization');
-}
-
-export function getToken() {
- return localStorage.authToken;
-}
-
-export function getUserInfo() {
- const user = getToken().split('.')[1];
- return JSON.parse(atob(user));
-}
-
-export function hasToken() {
- return !!getToken();
-}
-
-export function setToken(token) {
- localStorage.authToken = token;
-}
-
-export function deleteToken() {
- delete localStorage.authToken;
-}
-
-export function logout() {
- deleteToken();
- window.location.reload();
-}
+const JSON_HEADERS = {Accept: 'application/json', 'Content-Type': 'application/json'};
export async function request(path, params, autoLogoutOnAuthError = true) {
const opts = Object.assign({headers: {}}, params);
@@ -71,6 +40,73 @@ export async function request(path, params, autoLogoutOnAuthError = true) {
return response.json();
}
+export function apiFactory(group, version, resource) {
+ const apiRoot = getApiRoot(group, version);
+ const url = `${apiRoot}/${resource}`;
+ return {
+ resource: {group, resource},
+ list: (cb, errCb) => streamResults(url, cb, errCb),
+ get: (name, cb, errCb) => streamResult(url, name, cb, errCb),
+ post: body => post(url, body),
+ put: body => put(`${url}/${body.metadata.name}`, body),
+ delete: name => remove(`${url}/${name}`),
+ };
+}
+
+export function apiFactoryWithNamespace(group, version, resource, includeScale) {
+ const apiRoot = getApiRoot(group, version);
+ const results = {
+ resource: {group, resource},
+ list: (namespace, cb, errCb) => streamResults(url(namespace), cb, errCb),
+ get: (namespace, name, cb, errCb) => streamResult(url(namespace), name, cb, errCb),
+ post: body => post(url(body.metadata.namespace), body),
+ put: body => put(`${url(body.metadata.namespace)}/${body.metadata.name}`, body),
+ delete: (namespace, name) => remove(`${url(namespace)}/${name}`),
+ };
+
+ if (includeScale) {
+ results.scale = apiScaleFactory(apiRoot, resource);
+ }
+
+ return results;
+
+ function url(namespace) {
+ return namespace ? `${apiRoot}/namespaces/${namespace}/${resource}` : `${apiRoot}/${resource}`;
+ }
+}
+
+function getApiRoot(group, version) {
+ return group ? `/apis/${group}/${version}` : `api/${version}`;
+}
+
+function apiScaleFactory(apiRoot, resource) {
+ return {
+ get: (namespace, name) => request(url(namespace, name)),
+ put: body => put(url(body.metadata.namespace, body.metadata.name), body),
+ };
+
+ function url(namespace, name) {
+ return `${apiRoot}/namespaces/${namespace}/${resource}/${name}/scale`;
+ }
+}
+
+export function post(url, json, autoLogoutOnAuthError = true) {
+ const body = JSON.stringify(json);
+ const opts = {method: 'POST', body, headers: JSON_HEADERS};
+ return request(url, opts, autoLogoutOnAuthError);
+}
+
+export function put(url, json, autoLogoutOnAuthError = true) {
+ const body = JSON.stringify(json);
+ const opts = {method: 'PUT', body, headers: JSON_HEADERS};
+ return request(url, opts, autoLogoutOnAuthError);
+}
+
+export function remove(url) {
+ const opts = {method: 'DELETE', headers: JSON_HEADERS};
+ return request(url, opts);
+}
+
export async function streamResult(url, name, cb, errCb) {
let isCancelled = false;
let socket;
diff --git a/client/src/services/auth.js b/client/src/services/auth.js
new file mode 100644
index 0000000..8dfcf82
--- /dev/null
+++ b/client/src/services/auth.js
@@ -0,0 +1,39 @@
+import * as cookie from 'js-cookie';
+
+// If we have an "Authorization" cookie, use that as the token for future api calls
+const authorizationCookie = cookie.get('Authorization');
+if (authorizationCookie) {
+ setToken(authorizationCookie);
+ cookie.remove('Authorization');
+}
+
+export function getToken() {
+ // This line deals with backwards compatability from when we used to only store the actual jwt
+ if (localStorage.authToken && !localStorage.authToken.startsWith('Bearer ')) {
+ localStorage.authToken = `Bearer ${localStorage.authToken}`;
+ }
+
+ return localStorage.authToken;
+}
+
+export function getUserInfo() {
+ const user = getToken().split('.')[1];
+ return JSON.parse(atob(user));
+}
+
+export function hasToken() {
+ return !!getToken();
+}
+
+export function setToken(token) {
+ localStorage.authToken = token;
+}
+
+export function deleteToken() {
+ delete localStorage.authToken;
+}
+
+export function logout() {
+ deleteToken();
+ window.location.reload();
+}
diff --git a/client/src/views/account.js b/client/src/views/account.js
index 9698ed9..26c0312 100644
--- a/client/src/views/account.js
+++ b/client/src/views/account.js
@@ -1,7 +1,7 @@
import React from 'react';
import Button from '../components/button';
import ItemHeader from '../components/itemHeader';
-import {getUserInfo, logout} from '../services/apiProxy';
+import {getUserInfo, logout} from '../services/auth';
import LogoutSvg from '../art/logoutSvg';
const Account = () => (
diff --git a/client/src/views/auth.js b/client/src/views/auth.js
index ada47ad..ea0bc3b 100644
--- a/client/src/views/auth.js
+++ b/client/src/views/auth.js
@@ -1,7 +1,7 @@
import './auth.scss';
import React from 'react';
import {addUserNotification} from '../components/notifier';
-import {setToken, deleteToken} from '../services/apiProxy';
+import {setToken, deleteToken} from '../services/auth';
import api from '../services/api';
import Base from '../components/base';
import Button from '../components/button';
diff --git a/client/src/views/configMap.js b/client/src/views/configMap.js
index d0f14b3..513d877 100644
--- a/client/src/views/configMap.js
+++ b/client/src/views/configMap.js
@@ -1,3 +1,4 @@
+import _ from 'lodash';
import React from 'react';
import Base from '../components/base';
import api from '../services/api';
@@ -48,7 +49,7 @@ export default class ConfigMap extends Base {
- {!item ? : Object.entries(item.data || {}).map(([key, value]) => (
+ {!item ? : _.map(item.data, (value, key) => (
))}