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

YAML Templates in Create Resource dock tab

Signed-off-by: Pavel Ashevskiy <pavel.ashevskiy@ifellow.ru>
This commit is contained in:
Pavel Ashevskiy 2021-03-12 16:40:39 +04:00 committed by Pavel Ashevskii
parent b852384e85
commit f74d365ee4
21 changed files with 521 additions and 2 deletions

View File

@ -1,2 +1,25 @@
.CreateResource {
}
.Select {
.Select__single-value {
width: 100%
}
&.TemplateSelect {
min-width: 200px;
}
}
}
.select-template-item{
justify-content: space-between;
align-items: baseline;
.select-template-badge {
text-transform: none;
font-weight: normal;
}
}
.select-template-group {
span {
font-size: 13px
}
}

View File

@ -1,14 +1,99 @@
import fs from "fs-extra";
import path from "path";
import os from "os"
import chokidar from "chokidar"
import filehound from "filehound"
import { autobind } from "../../utils";
import { DockTabStore } from "./dock-tab.store";
import { dockStore, IDockTab, TabKind } from "./dock.store";
import { GroupSelectOption, SelectOption} from "../select";
export interface BadgedSelectOption extends SelectOption {
badge?: string;
}
export interface BadgedGroupSelectOption extends GroupSelectOption<BadgedSelectOption> {
badge?: string;
}
@autobind()
export class CreateResourceStore extends DockTabStore<string> {
constructor() {
super({
storageKey: "create_resource"
});
}
get lensTemplatesFolder():string {
return path.resolve(__static, "../templates/create-resource");
}
get userTemplatesFolder():string {
return path.join(os.homedir(), ".k8slens", "templates");
}
async getTemplates(templatesPath: string):Promise<GroupSelectOption[]>{
const ungroupedTemplates:SelectOption[] = [];
const templates:GroupSelectOption[] = [];
let groups = await fs.readdir(templatesPath);
await Promise.all(groups.map(async (group) =>{
const groupAbsPath = path.join(templatesPath, group);
if((await fs.stat(groupAbsPath)).isDirectory()){
const files = await fs.readdir(groupAbsPath);
const templateSelectOptions = await Promise.all(files.map(
(template) => ({
value: path.join(groupAbsPath,template),
label: path.parse(template).name
})
)
);
templates.push({label: group.replace("-", " "), options: templateSelectOptions});
} else {
ungroupedTemplates.push({ value: groupAbsPath, label: path.parse(group).name});
}
}))
if (ungroupedTemplates.length > 0){
templates.push({label: "ungrouped", options: ungroupedTemplates})
}
return templates;
}
async getMergedTemplates():Promise<BadgedGroupSelectOption[]> {
const lensTemplates = await this.getTemplates(this.lensTemplatesFolder);
const userTemplates = await this.getTemplates(this.userTemplatesFolder);
const userGroupOptions:BadgedGroupSelectOption[] = userTemplates.filter(
({label}) => !lensTemplates.map(({label}) => label.toString().toLowerCase()).includes(label.toString().toLowerCase())
).map(({label,options}) => ({label, badge:"user", options}));
// merge lens and user templates within lens groups, then merge lens and user groups
const badgedTemplates:BadgedGroupSelectOption[] = lensTemplates.map( ({label, options}) => {
const userOptions = userTemplates.find(userTemplate => userTemplate.label.toString().toLowerCase() == label.toString().toLowerCase())
return {
label,
options: userOptions ? options.concat(userOptions.options.map((userOption) => ({label: userOption.label, badge: "user", value: userOption.value}))) : options
};
})
return this.orderTemplatesGroups(userGroupOptions).concat(this.orderTemplatesGroups(badgedTemplates));
}
async watchUserTemplates(calback: ()=> void){
chokidar.watch(this.userTemplatesFolder).on('all', () => {
calback();
});
}
orderTemplatesGroups(templates:BadgedGroupSelectOption[]): BadgedGroupSelectOption[] {
const compare = (a:BadgedGroupSelectOption|BadgedSelectOption, b:BadgedGroupSelectOption|BadgedSelectOption) => {
return a.label.toString() > b.label.toString() ? 1 : a.label.toString() < b.label.toString() ? -1 : 0
}
return templates.map( group => {
group.options = group.options.sort((a, b) => compare(a,b));
return group;
}).sort((a, b) => compare(a,b));
}
}
export const createResourceStore = new CreateResourceStore();

View File

@ -1,17 +1,20 @@
import "./create-resource.scss";
import React from "react";
import fs from "fs-extra";
import {Select, GroupSelectOption, SelectOption} from "../select";
import jsYaml from "js-yaml";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { cssNames } from "../../utils";
import { createResourceStore } from "./create-resource.store";
import { createResourceStore, BadgedGroupSelectOption } from "./create-resource.store";
import { IDockTab } from "./dock.store";
import { EditorPanel } from "./editor-panel";
import { InfoPanel } from "./info-panel";
import { resourceApplierApi } from "../../api/endpoints/resource-applier.api";
import { JsonApiErrorParsed } from "../../api/json-api";
import { Notifications } from "../notifications";
import { Badge } from "../badge";
interface Props {
className?: string;
@ -21,6 +24,31 @@ interface Props {
@observer
export class CreateResource extends React.Component<Props> {
@observable error = "";
@observable templates:GroupSelectOption<SelectOption>[] = [];
async componentDidMount(){
createResourceStore.watchUserTemplates(()=> {
this.templates = [];
createResourceStore.getMergedTemplates().then(
badgedTemplates => {
badgedTemplates.map((group) => {
this.templates.push(this.convertBadgedGroup(group));
})
})
})
}
convertBadgedGroup(badgedGroup: BadgedGroupSelectOption):GroupSelectOption {
const options = badgedGroup.options.map(({label, badge, value }) => ({
label: this.renderSelectOption(label.toString(), badge),
value,
}));
return ({
label: this.renderSelectGroup(badgedGroup.label.toString(), badgedGroup.badge),
options
});
}
get tabId() {
return this.props.tab.id;
@ -35,6 +63,10 @@ export class CreateResource extends React.Component<Props> {
this.error = error;
};
onSelectTemplate = (item: SelectOption) => {
fs.readFile(item.value,"utf8").then(v => createResourceStore.setData(this.tabId,v));
};
create = async () => {
if (this.error) return;
if (!this.data.trim()) return; // do not save when field is empty
@ -67,6 +99,40 @@ export class CreateResource extends React.Component<Props> {
return successMessage;
};
renderSelectOption(label: string, badge?: string){
return this.renderSelectItem(label,"select-template-option", badge );
}
renderSelectGroup(label: string, badge?: string){
return this.renderSelectItem(label,"select-template-group", badge );
}
renderSelectItem(label: string, className: string, badge?: string) {
return(
<div className={cssNames("flex select-template-item", className)}>
<span>{label}</span>
{badge && <Badge className="select-template-badge" label={badge} />}
</div>
);
}
renderControls(){
return (
<div className="flex gaps align-center">
<Select
autoConvertOptions = {false}
className="TemplateSelect"
placeholder="Select Template ..."
options={this.templates}
menuPlacement="top"
themeName="outlined"
onChange={v => this.onSelectTemplate(v)}
/>
</div>
);
}
render() {
const { tabId, data, error, create, onChange } = this;
const { className } = this.props;
@ -76,6 +142,7 @@ export class CreateResource extends React.Component<Props> {
<InfoPanel
tabId={tabId}
error={error}
controls={this.renderControls()}
submit={create}
submitLabel="Create"
showNotifications={false}

View File

@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
# "namespace" omitted since ClusterRoles are not namespaced
name: secret-reader
rules:
- apiGroups: [""]
#
# at the HTTP level, the name of the resource for accessing Secret
# objects is "secrets"
resources: ["secrets"]
verbs: ["get", "watch", "list"]

View File

@ -0,0 +1,13 @@
apiVersion: rbac.authorization.k8s.io/v1
# This cluster role binding allows anyone in the "manager" group to read secrets in any namespace.
kind: ClusterRoleBinding
metadata:
name: read-secrets-global
subjects:
- kind: Group
name: manager # Name is case sensitive
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: secret-reader
apiGroup: rbac.authorization.k8s.io

View File

@ -0,0 +1,9 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""] # "" indicates the core API group
resources: ["pods"]
verbs: ["get", "watch", "list"]

View File

@ -0,0 +1,17 @@
apiVersion: rbac.authorization.k8s.io/v1
# This role binding allows "jane" to read pods in the "default" namespace.
# You need to already have a Role named "pod-reader" in that namespace.
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
# You can specify more than one "subject"
- kind: User
name: jane # "name" is case sensitive
apiGroup: rbac.authorization.k8s.io
roleRef:
# "roleRef" specifies the binding to a Role / ClusterRole
kind: Role #this must be Role or ClusterRole
name: pod-reader # this must match the name of the Role or ClusterRole you wish to bind to
apiGroup: rbac.authorization.k8s.io

View File

@ -0,0 +1,17 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: game-demo
data:
# property-like keys; each key maps to a simple value
player_initial_lives: "3"
ui_properties_file_name: "user-interface.properties"
# file-like keys
game.properties: |
enemy.types=aliens,monsters
player.maximum-lives=5
user-interface.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true

View File

@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-basic-auth
type: kubernetes.io/basic-auth
stringData:
username: admin
password: t0p-Secret

View File

@ -0,0 +1,19 @@
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: hello
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -c
- date; echo Hello from the Kubernetes cluster
restartPolicy: OnFailure

View File

@ -0,0 +1,44 @@
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd-elasticsearch
namespace: kube-system
labels:
k8s-app: fluentd-logging
spec:
selector:
matchLabels:
name: fluentd-elasticsearch
template:
metadata:
labels:
name: fluentd-elasticsearch
spec:
tolerations:
# this toleration is to have the daemonset runnable on master nodes
# remove it if your masters can't run pods
- key: node-role.kubernetes.io/master
effect: NoSchedule
containers:
- name: fluentd-elasticsearch
image: quay.io/fluentd_elasticsearch/fluentd:v2.5.2
resources:
limits:
memory: 200Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers

View File

@ -0,0 +1,21 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80

View File

@ -0,0 +1,13 @@
apiVersion: batch/v1
kind: Job
metadata:
name: pi
spec:
template:
spec:
containers:
- name: pi
image: perl
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
restartPolicy: Never
backoffLimit: 4

View File

@ -0,0 +1,21 @@
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: frontend
labels:
app: guestbook
tier: frontend
spec:
# modify replicas according to your case
replicas: 3
selector:
matchLabels:
tier: frontend
template:
metadata:
labels:
tier: frontend
spec:
containers:
- name: php-redis
image: gcr.io/google_samples/gb-frontend:v3

View File

@ -0,0 +1,19 @@
apiVersion: v1
kind: ReplicationController
metadata:
name: nginx
spec:
replicas: 3
selector:
app: nginx
template:
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80

View File

@ -0,0 +1,34 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
selector:
matchLabels:
app: nginx # has to match .spec.template.metadata.labels
serviceName: "nginx"
replicas: 3 # by default is 1
template:
metadata:
labels:
app: nginx # has to match .spec.selector.matchLabels
spec:
terminationGracePeriodSeconds: 10
containers:
- name: nginx
image: k8s.gcr.io/nginx-slim:0.8
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "my-storage-class"
resources:
requests:
storage: 1Gi

View File

@ -0,0 +1,17 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /testpath
pathType: Prefix
backend:
service:
name: test
port:
number: 80

View File

@ -0,0 +1,34 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: test-network-policy
namespace: default
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- ipBlock:
cidr: 172.17.0.0/16
except:
- 172.17.1.0/24
- namespaceSelector:
matchLabels:
project: myproject
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 6379
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/24
ports:
- protocol: TCP
port: 5978

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376

View File

@ -0,0 +1,18 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv0003
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
storageClassName: slow
mountOptions:
- hard
- nfsvers=4.1
nfs:
path: /tmp
server: 172.17.0.2

View File

@ -0,0 +1,17 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: myclaim
spec:
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
storage: 8Gi
storageClassName: slow
selector:
matchLabels:
release: "stable"
matchExpressions:
- {key: environment, operator: In, values: [dev]}