Lens app source code (#119)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2020-03-15 09:52:02 +02:00
1 parent 936cbd53d4
commit 1d0815abd2
797 files changed
+73714 -215

No files matched your search

@@ -0,0 +1,38 @@
.AddQuotaDialog {
.quota-select {
flex-basis: 55%;
}
.quota-entries {
margin: -$margin / 2;
&:empty {
display: none;
}
.quota {
--flex-gap: #{$padding};
border: 1px solid $halfGray;
border-radius: $radius;
margin: $margin / 2;
padding: $padding / 2 $padding;
transition: all 150ms ease;
&:hover {
box-shadow: inset 0 0 0 1px $borderColor;
}
.name {
font-weight: $font-weight-bold;
}
.value {
color: $contentColor;
}
.Icon:hover {
color: black;
}
}
}
}
@@ -0,0 +1,204 @@
import "./add-quota-dialog.scss";
import React from "react";
import { computed, observable } from "mobx";
import { observer } from "mobx-react";
import { t, Trans } from "@lingui/macro";
import { _i18n } from "../../i18n";
import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { Input } from "../input";
import { systemName } from "../input/input.validators";
import { IResourceQuotaValues, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
import { Select } from "../select";
import { Icon } from "../icon";
import { Button } from "../button";
import { Notifications } from "../notifications";
import { NamespaceSelect } from "../+namespaces/namespace-select";
import { SubTitle } from "../layout/sub-title";
interface Props extends DialogProps {
}
@observer
export class AddQuotaDialog extends React.Component<Props> {
@observable static isOpen = false;
static defaultQuotas: IResourceQuotaValues = {
"limits.cpu": "",
"limits.memory": "",
"requests.cpu": "",
"requests.memory": "",
"requests.storage": "",
"persistentvolumeclaims": "",
"count/pods": "",
"count/persistentvolumeclaims": "",
"count/services": "",
"count/secrets": "",
"count/configmaps": "",
"count/replicationcontrollers": "",
"count/deployments.apps": "",
"count/replicasets.apps": "",
"count/statefulsets.apps": "",
"count/jobs.batch": "",
"count/cronjobs.batch": "",
"count/deployments.extensions": "",
};
public defaultNamespace = "default"
@observable quotaName = "";
@observable quotaSelectValue = "";
@observable quotaInputValue = "";
@observable namespace = this.defaultNamespace;
@observable quotas = AddQuotaDialog.defaultQuotas;
static open() {
AddQuotaDialog.isOpen = true;
}
static close() {
AddQuotaDialog.isOpen = false;
}
@computed get quotaEntries() {
return Object.entries(this.quotas)
.filter(([type, value]) => !!value.trim());
}
@computed get quotaOptions() {
return Object.keys(this.quotas).map(quota => {
const isCompute = quota.endsWith(".cpu") || quota.endsWith(".memory");
const isStorage = quota.endsWith(".storage") || quota === "persistentvolumeclaims";
const isCount = quota.startsWith("count/");
const icon = isCompute ? "memory" : isStorage ? "storage" : isCount ? "looks_one" : "";
return {
label: icon ? <span className="nobr"><Icon material={icon}/> {quota}</span> : quota,
value: quota,
};
});
}
setQuota = () => {
if (!this.quotaSelectValue) return;
this.quotas[this.quotaSelectValue] = this.quotaInputValue;
this.quotaInputValue = "";
}
close = () => {
AddQuotaDialog.close();
}
reset = () => {
this.quotaName = "";
this.quotaSelectValue = "";
this.quotaInputValue = "";
this.namespace = this.defaultNamespace;
this.quotas = AddQuotaDialog.defaultQuotas;
}
addQuota = async () => {
try {
const { quotaName, namespace } = this;
const quotas = this.quotaEntries.reduce<IResourceQuotaValues>((quotas, [name, value]) => {
quotas[name] = value;
return quotas;
}, {});
await resourceQuotaApi.create({ namespace, name: quotaName }, {
spec: {
hard: quotas
}
});
this.close();
} catch (err) {
Notifications.error(err);
}
}
onInputQuota = (evt: React.KeyboardEvent) => {
switch (evt.key) {
case "Enter":
this.setQuota();
evt.preventDefault(); // don't submit form
break;
}
}
render() {
const { ...dialogProps } = this.props;
const header = <h5><Trans>Create ResourceQuota</Trans></h5>;
return (
<Dialog
{...dialogProps}
className="AddQuotaDialog"
isOpen={AddQuotaDialog.isOpen}
close={this.close}
>
<Wizard header={header} done={this.close}>
<WizardStep
contentClass="flex gaps column"
disabledNext={!this.namespace}
nextLabel={<Trans>Create</Trans>}
next={this.addQuota}
>
<div className="flex gaps">
<Input
required autoFocus
placeholder={_i18n._(t`ResourceQuota name`)}
validators={systemName}
value={this.quotaName} onChange={v => this.quotaName = v.toLowerCase()}
className="box grow"
/>
</div>
<SubTitle title={<Trans>Namespace</Trans>}/>
<NamespaceSelect
value={this.namespace}
placeholder={_i18n._(t`Namespace`)}
themeName="light"
className="box grow"
onChange={({ value }) => this.namespace = value}
/>
<SubTitle title={<Trans>Values</Trans>}/>
<div className="flex gaps align-center">
<Select
className="quota-select"
themeName="light"
placeholder={_i18n._(t`Select a quota..`)}
options={this.quotaOptions}
value={this.quotaSelectValue}
onChange={({ value }) => this.quotaSelectValue = value}
/>
<Input
maxLength={10}
placeholder={_i18n._(t`Value`)}
value={this.quotaInputValue}
onChange={v => this.quotaInputValue = v}
onKeyDown={this.onInputQuota}
className="box grow"
/>
<Button round primary onClick={this.setQuota}>
<Icon
material={this.quotas[this.quotaSelectValue] ? "edit" : "add"}
tooltip={_i18n._(t`Set quota`)}
/>
</Button>
</div>
<div className="quota-entries">
{this.quotaEntries.map(([quota, value]) => {
return (
<div key={quota} className="quota flex gaps inline align-center">
<div className="name">{quota}</div>
<div className="value">{value}</div>
<Icon material="clear" onClick={() => this.quotas[quota] = ""}/>
</div>
)
})}
</div>
</WizardStep>
</Wizard>
</Dialog>
)
}
}
@@ -0,0 +1,3 @@
export * from "./resource-quotas.route"
export * from "./resource-quotas"
export * from "./resource-quota-details"
@@ -0,0 +1,16 @@
.ResourceQuotaDetails {
.quota-list {
.param {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding-bottom: $padding * 2;
.LineProgress {
margin-top: 3px;
width: 100%;
color: $colorInfo;
}
}
}
}
@@ -0,0 +1,94 @@
import "./resource-quota-details.scss";
import React from "react";
import kebabCase from "lodash/kebabCase";
import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { DrawerItem, DrawerTitle } from "../drawer";
import { cpuUnitsToNumber, cssNames, unitsToBytes } from "../../utils";
import { KubeObjectDetailsProps } from "../kube-object";
import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
import { LineProgress } from "../line-progress";
import { Table, TableCell, TableHead, TableRow } from "../table";
import { apiManager } from "../../api/api-manager";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
interface Props extends KubeObjectDetailsProps<ResourceQuota> {
}
@observer
export class ResourceQuotaDetails extends React.Component<Props> {
renderQuotas = (quota: ResourceQuota) => {
const { hard, used } = quota.status
if (!hard || !used) return null
const transformUnit = (name: string, value: string) => {
if (name.includes("memory") || name.includes("storage")) {
return unitsToBytes(value)
}
if (name.includes("cpu")) {
return cpuUnitsToNumber(value)
}
return parseInt(value)
}
return Object.entries(hard).map(([name, value]) => {
if (!used[name]) return null
const current = transformUnit(name, used[name])
const max = transformUnit(name, value)
return (
<div key={name} className={cssNames("param", kebabCase(name))}>
<span className="title">{name}</span>
<span className="value">{used[name]} / {value}</span>
<LineProgress
max={max}
value={current}
tooltip={
<p><Trans>Set</Trans>: {value}. <Trans>Used</Trans>: {Math.ceil(current / max * 100) + "%"}</p>
}
/>
</div>
)
})
}
render() {
const { object: quota } = this.props;
if (!quota) return null;
return (
<div className="ResourceQuotaDetails">
<KubeObjectMeta object={quota}/>
<DrawerItem name={<Trans>Quotas</Trans>} className="quota-list">
{this.renderQuotas(quota)}
</DrawerItem>
{quota.getScopeSelector().length > 0 && (
<>
<DrawerTitle title={<Trans>Scope Selector</Trans>}/>
<Table className="paths">
<TableHead>
<TableCell><Trans>Operator</Trans></TableCell>
<TableCell><Trans>Scope name</Trans></TableCell>
<TableCell><Trans>Values</Trans></TableCell>
</TableHead>
{
quota.getScopeSelector().map((selector, index) => {
const { operator, scopeName, values } = selector;
return (
<TableRow key={index}>
<TableCell>{operator}</TableCell>
<TableCell>{scopeName}</TableCell>
<TableCell>{values.join(", ")}</TableCell>
</TableRow>
);
})
}
</Table>
</>
)}
</div>
);
}
}
apiManager.registerViews(resourceQuotaApi, {
Details: ResourceQuotaDetails
})
@@ -0,0 +1,11 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
export const resourceQuotaRoute: RouteProps = {
path: "/resourcequotas"
}
export interface IResourceQuotaRouteParams {
}
export const resourceQuotaURL = buildURL<IResourceQuotaRouteParams>(resourceQuotaRoute.path);
@@ -0,0 +1,2 @@
.ResourceQuotas {
}
@@ -0,0 +1,12 @@
import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils";
import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
import { apiManager } from "../../api/api-manager";
@autobind()
export class ResourceQuotasStore extends KubeObjectStore<ResourceQuota> {
api = resourceQuotaApi
}
export const resourceQuotaStore = new ResourceQuotasStore();
apiManager.registerStore(resourceQuotaApi, resourceQuotaStore);
@@ -0,0 +1,73 @@
import "./resource-quotas.scss";
import * as React from "react";
import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { RouteComponentProps } from "react-router";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { KubeObjectListLayout } from "../kube-object";
import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
import { AddQuotaDialog } from "./add-quota-dialog";
import { resourceQuotaStore } from "./resource-quotas.store";
import { IResourceQuotaRouteParams } from "./resource-quotas.route";
import { apiManager } from "../../api/api-manager";
enum sortBy {
name = "name",
namespace = "namespace",
age = "age"
}
interface Props extends RouteComponentProps<IResourceQuotaRouteParams> {
}
@observer
export class ResourceQuotas extends React.Component<Props> {
render() {
return (
<>
<KubeObjectListLayout
className="ResourceQuotas" store={resourceQuotaStore}
sortingCallbacks={{
[sortBy.name]: (item: ResourceQuota) => item.getName(),
[sortBy.namespace]: (item: ResourceQuota) => item.getNs(),
[sortBy.age]: (item: ResourceQuota) => item.getAge(false),
}}
searchFilters={[
(item: ResourceQuota) => item.getSearchFields(),
(item: ResourceQuota) => item.getName(),
]}
renderHeaderTitle={<Trans>Resource Quotas</Trans>}
renderTableHeader={[
{ title: <Trans>Name</Trans>, className: "name", sortBy: sortBy.name },
{ title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]}
renderTableContents={(resourceQuota: ResourceQuota) => [
resourceQuota.getName(),
resourceQuota.getNs(),
resourceQuota.getAge(),
]}
renderItemMenu={(item: ResourceQuota) => {
return <ResourceQuotaMenu object={item}/>
}}
addRemoveButtons={{
onAdd: () => AddQuotaDialog.open(),
addTooltip: <Trans>Create new ResourceQuota</Trans>
}}
/>
<AddQuotaDialog/>
</>
);
}
}
export function ResourceQuotaMenu(props: KubeObjectMenuProps<ResourceQuota>) {
return (
<KubeObjectMenu {...props}/>
);
}
apiManager.registerViews(resourceQuotaApi, {
Menu: ResourceQuotaMenu,
})