├── .editorconfig ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── Helper.ts ├── LICENSE ├── README.md ├── SettingsEditor.ts ├── gis.d.ts ├── main.ts ├── package.json ├── readme.md ├── renderer.tsx ├── src ├── AppViewerPage.tsx ├── Loading.tsx └── styles │ └── Loading.css ├── tsconfig.json └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | indent_size = 2 7 | indent_style = space 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # webstorm 107 | .idea 108 | 109 | # Package Lock 110 | package-lock.json 111 | 112 | # Settings 113 | settings.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | CHANGELOG.md 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /Helper.ts: -------------------------------------------------------------------------------- 1 | import { Renderer } from "@k8slens/extensions"; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { Parser } from "simple-text-parser"; 5 | import getColors from 'get-image-colors'; 6 | import arrayBufferToBuffer from 'arraybuffer-to-buffer'; 7 | 8 | export class Helper { 9 | public static getCurrentClusterName() { 10 | try { 11 | return Renderer.Catalog.catalogEntities.activeEntity.metadata.name; 12 | } catch (err) { return null; } 13 | } 14 | public static getCurrentClusterId() { 15 | try { 16 | return Renderer.Catalog.catalogEntities.activeEntity.metadata.uid; 17 | } catch (err) { return null; } 18 | } 19 | public static registerStylesheetFromNodeModule(modulename: string, ...subpaths: string[]) { 20 | var dirPath = __dirname; 21 | if (dirPath.endsWith("dist")) { 22 | dirPath = path.dirname(dirPath); 23 | } 24 | var css = ""; 25 | 26 | for (var subpath of subpaths) { 27 | const targetPath = path.join(dirPath, "node_modules", subpath); 28 | css += fs.readFileSync(targetPath, "utf8"); 29 | } 30 | 31 | const customSelector = `.module_${modulename}`; 32 | const el = window.document.createElement("style"); 33 | 34 | var p = new Parser(); 35 | p.addRule(/,/g, null, "splitter"); 36 | 37 | css = css.replace(/[^\{\}\n]+(?=\{)/gs, (match) => { 38 | var nodes = p.toTree(match); 39 | return nodes.map(text => { 40 | if (text.type === "text") { 41 | text.text = customSelector + " " + text.text; 42 | } 43 | return text.text; 44 | }).join(""); 45 | }); 46 | el.innerText = css; 47 | if (window.document.head.children.length == 0) { 48 | window.document.head.insertBefore(el, window.document.head.children[0]); 49 | } 50 | else { 51 | window.document.head.appendChild(el); 52 | } 53 | } 54 | public static registerStylesheetFromCurrentDir(modulename: string, ...subpaths: string[]) { 55 | subpaths = subpaths.map(item => { 56 | return path.join("..", item); 57 | }); 58 | return Helper.registerStylesheetFromNodeModule(modulename, ...subpaths); 59 | } 60 | public static async haveLogo(id: string): Promise { 61 | var dirpath = path.join(__dirname, "logos"); 62 | if (!fs.existsSync(dirpath)) { 63 | fs.mkdirSync(dirpath); 64 | } 65 | var newPath = path.join(__dirname, "logos", id + ".png"); 66 | var newPathSVG = path.join(__dirname, "logos", id + ".svg"); 67 | if (fs.existsSync(newPath) || fs.existsSync(newPathSVG)) { 68 | return true; 69 | } 70 | else { 71 | return false; 72 | } 73 | } 74 | public static async cacheLogo(id: string, logo: string): Promise<{ logo: string, color: string }> { 75 | var buffer: Buffer = Buffer.from(new ArrayBuffer(0)); 76 | var logoData: any = {}; 77 | var dirpath = path.join(__dirname, "logos"); 78 | if (!fs.existsSync(dirpath)) { 79 | fs.mkdirSync(dirpath); 80 | } 81 | var ext = logo && path.extname(logo).replace('.', ''); 82 | if (!ext) { 83 | ext = fs.readdirSync(dirpath).filter(item => item.indexOf(id) > -1).map(item => path.extname(item).replace('.', ''))[0]; 84 | console.log('EXT:', ext); 85 | } 86 | var newPath = path.join(__dirname, "logos", id + "." + ext); 87 | if (fs.existsSync(newPath)) { 88 | buffer = Buffer.from(fs.readFileSync(newPath, "binary")); 89 | } 90 | else { 91 | await fetch(logo, { 92 | method: 'get', 93 | headers: { 94 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36', 95 | 'Accept': '*' 96 | } 97 | }).then(p => p.arrayBuffer()).then(p => buffer = Buffer.from(p)).catch(console.error); 98 | fs.writeFileSync(newPath, buffer, "binary"); 99 | } 100 | /* await new Promise(async (resolve,reject)=>{ 101 | await getColors(newPath).then(colors => { 102 | logoData.color = colors[0].hex(); 103 | console.log(colors); 104 | }).catch(console.error); 105 | resolve(null); 106 | }).catch(console.error);*/ 107 | logoData.ext = ext; 108 | logoData.logo = await getBase64Async(buffer, ext); 109 | return logoData; 110 | } 111 | } 112 | 113 | async function getBase64Async(file: Buffer, ext: string): Promise { 114 | return new Promise((resolve) => { 115 | if (ext === 'svg') ext = 'svg+xml'; 116 | 117 | resolve(`data:image/${ext};base64,` + file.toString('base64')); 118 | }); 119 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Muhammed Koçyiğit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is App Viewer? 2 | An extension for access services on Lens Ide easily. 3 | 4 | # Installation 5 | ### 1 - Open Extensions Page 6 | ![image](https://user-images.githubusercontent.com/28162520/132977527-9b5736a3-9fcf-4929-ac7e-859c7d658591.png) 7 | ### 2 - Copy & Paste Lens App Viewer Released Tar Archive Link 8 | ![image](https://user-images.githubusercontent.com/28162520/132977575-73ecf1eb-b09f-4f53-94e7-e74c3286ad54.png) 9 | ### 3 - Wait until finish the installation and Enable Extension 10 | ![image](https://user-images.githubusercontent.com/28162520/132977592-f98d636b-a234-4904-8931-745f8ab1ed7c.png) 11 | ![image](https://user-images.githubusercontent.com/28162520/132977603-7154ef04-3672-47a1-a6eb-b96456cf85ee.png) 12 | 13 | # How to Use? 14 | ### 1 - Choose a service port what you want to see it on App Viewer and Press it 15 | ![image](https://user-images.githubusercontent.com/28162520/132977667-e8913552-ad8c-498e-852d-a05837f1d5c5.png) 16 | ### 2 - Go to App Viewer Page 17 | ![image](https://user-images.githubusercontent.com/28162520/132977689-27d46850-e0a2-4f49-aa04-cdb3128e9241.png) 18 | ### 3 - And Ready to Use :) 19 | ![image](https://user-images.githubusercontent.com/28162520/132977694-b9032317-ef81-4003-afe5-0d14287535c5.png) 20 | 21 | # Edit Apps 22 | ### Step by Step 23 | #### 1) ![image](https://user-images.githubusercontent.com/28162520/132977760-3202f3ce-882a-4666-b73e-f2d5406dea28.png) 24 | #### 2) ![image](https://user-images.githubusercontent.com/28162520/132977762-064bc975-40b5-4f09-9de4-4b29278fc076.png) 25 | #### 3) ![image](https://user-images.githubusercontent.com/28162520/132977768-e6af46b1-2ffd-4e3e-9221-eb41a68cec35.png) 26 | #### 4) ![image](https://user-images.githubusercontent.com/28162520/132977779-133b9356-8b39-4966-8609-f8f53471f4fc.png) 27 | 28 | All is that :) 29 | 30 | # What is Next? 31 | > - View App secrets after open (optional) 32 | > - App Online Status 33 | > - Order & Group Apps 34 | -------------------------------------------------------------------------------- /SettingsEditor.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { v4 } from 'uuid'; 4 | const currentDir: string = __dirname; 5 | export class ServicePortSettings { 6 | public port: number; 7 | public targetPort: number; 8 | public name: string; 9 | public protocol: string; 10 | public constructor(initial?: any) { 11 | if (initial) { 12 | this.port = initial.port; 13 | this.targetPort = initial.targetPort; 14 | this.name = initial.name; 15 | this.protocol = initial.protocol; 16 | } 17 | } 18 | } 19 | export class ClusterServiceSettings { 20 | public Name: string; 21 | public Logo: string; 22 | public Namespace: string; 23 | public ServiceName: string; 24 | public Labels: any; 25 | public Port: ServicePortSettings; 26 | public Id: string; 27 | public constructor(initial?: any) { 28 | if (initial) { 29 | this.Id = initial.Id || initial.id; 30 | this.Name = initial.Name || initial.name; 31 | this.Logo = initial.Logo || initial.logo; 32 | this.Namespace = initial.Namespace || initial.namespace; 33 | this.ServiceName = initial.ServiceName || initial.serviceName; 34 | this.Labels = initial.Labels || initial.labels; 35 | this.Port = new ServicePortSettings(initial.Port || initial.port); 36 | } 37 | } 38 | } 39 | export class ClusterSettings { 40 | public Name: string; 41 | public Id: string; 42 | public Services: ClusterServiceSettings[] = []; 43 | public registerService(service: any, port: any) { 44 | var item: ClusterServiceSettings = new ClusterServiceSettings(); 45 | item.Id = v4(); 46 | item.Name = service.metadata.name; 47 | item.Namespace = service.metadata.namespace; 48 | item.ServiceName = service.metadata.name; 49 | item.Labels = service.metadata.labels; 50 | item.Port = port; 51 | this.Services.push(item); 52 | } 53 | public constructor(initial?: any) { 54 | if (initial) { 55 | this.Name = initial.Name || initial.name; 56 | this.Id = initial.Id || initial.id; 57 | this.Services = initial.Services.map((item: any) => new ClusterServiceSettings(item)); 58 | } 59 | } 60 | } 61 | export class Settings { 62 | public Clusters: ClusterSettings[] = []; 63 | public constructor(initial?: any) { 64 | if (initial) { 65 | this.Clusters = initial.Clusters.map((cluster: any) => new ClusterSettings(cluster)); 66 | } 67 | } 68 | public getCluster(id: string): ClusterSettings { 69 | var cluster: ClusterSettings = this.Clusters.find(c => c.Id === id); 70 | if (!cluster) { 71 | this.Clusters.push(cluster = new ClusterSettings()); 72 | cluster.Id = id; 73 | } 74 | return cluster; 75 | } 76 | } 77 | export class SettingsEditor { 78 | public data: Settings; 79 | public path: string; 80 | public content: string; 81 | constructor() { 82 | this.path = path.join(path.dirname(currentDir), "settings.json"); 83 | this.reload(); 84 | } 85 | reload() { 86 | try { 87 | this.content = fs.readFileSync(this.path, 'utf8'); 88 | this.data = new Settings(JSON.parse(this.content)); 89 | if (!this.data) { 90 | this.data = new Settings(); 91 | } 92 | } catch (err) { 93 | this.data = new Settings(); 94 | } 95 | } 96 | save() { 97 | fs.writeFileSync(this.path, JSON.stringify(this.data)); 98 | } 99 | } -------------------------------------------------------------------------------- /gis.d.ts: -------------------------------------------------------------------------------- 1 | declare module "g-i-s"{ 2 | export interface retrieveCallback{ 3 | (err : any, results : {url:string,width:number,height:number}[]):void; 4 | } 5 | export default function retrieve(url:string, callback:retrieveCallback):void; 6 | } 7 | declare module "react-spinner-material"{ 8 | export default class Spinner extends React.Component{ 9 | } 10 | } -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { Main } from "@k8slens/extensions"; 2 | 3 | 4 | export default class MainExtension extends Main.LensExtension { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lens-app-viewer", 3 | "version": "1.0.0", 4 | "author": "https://github.com/kocyigitkim", 5 | "publisher": "kocyigitkim", 6 | "description": "Application Viewer For Lens Ide", 7 | "main": "dist/main.js", 8 | "renderer": "dist/renderer.js", 9 | "scripts": { 10 | "start": "webpack --watch", 11 | "build": "npm run clean && webpack", 12 | "clean": "rimraf ./dist", 13 | "semantic-release": "semantic-release" 14 | }, 15 | "files": [ 16 | "dist/" 17 | ], 18 | "engines": { 19 | "lens": "^5.0.0-beta.7" 20 | }, 21 | "devDependencies": { 22 | "@k8slens/extensions": "^5.0.2", 23 | "@semantic-release/changelog": "^5.0.1", 24 | "@semantic-release/git": "^9.0.0", 25 | "@types/node": "^16.9.1", 26 | "@types/react": "^17.0.20", 27 | "husky": "^7.0.1", 28 | "lint-staged": "^11.0.0", 29 | "prettier": "2.3.2", 30 | "rimraf": "^3.0.2", 31 | "semantic-release": "^17.4.4", 32 | "ts-loader": "^9.2.3", 33 | "typescript": "^4.3.5", 34 | "webpack": "^5.44.0", 35 | "webpack-cli": "^4.8.0" 36 | }, 37 | "license": "MIT", 38 | "keywords": [ 39 | "extension", 40 | "k8slens", 41 | "lens" 42 | ], 43 | "release": { 44 | "branches": [ 45 | "main" 46 | ], 47 | "plugins": [ 48 | "@semantic-release/commit-analyzer", 49 | "@semantic-release/release-notes-generator", 50 | "@semantic-release/changelog", 51 | "@semantic-release/npm", 52 | "@semantic-release/git" 53 | ] 54 | }, 55 | "lint-staged": { 56 | "**/*": "prettier --write --ignore-unknown" 57 | }, 58 | "dependencies": { 59 | "@types/color": "^3.0.2", 60 | "@types/get-image-colors": "^4.0.1", 61 | "@types/string-similarity": "^4.0.0", 62 | "@types/uuid": "^8.3.1", 63 | "arraybuffer-to-buffer": "^0.0.7", 64 | "bootstrap": "^5.1.1", 65 | "bootstrap-dark-5": "^1.1.0", 66 | "color": "^4.0.1", 67 | "g-i-s": "^2.1.6", 68 | "get-image-colors": "^4.0.0", 69 | "react-bootstrap": "^1.6.3", 70 | "react-icons": "^4.2.0", 71 | "react-spinner-material": "^1.3.1", 72 | "simple-text-parser": "^2.1.1", 73 | "string-similarity": "^4.0.4", 74 | "uuid": "^8.3.2" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # lens-app-viewer 2 | -------------------------------------------------------------------------------- /renderer.tsx: -------------------------------------------------------------------------------- 1 | import { Renderer } from "@k8slens/extensions"; 2 | import React from "react"; 3 | import fs from "fs"; 4 | import { SettingsEditor } from "./SettingsEditor"; 5 | import { Helper } from "./Helper"; 6 | import AppViewerPage from "./src/AppViewerPage"; 7 | import { AiTwotoneAppstore } from "react-icons/ai"; 8 | 9 | console.warn('Bootstrap CSS Injected!!!'); 10 | Helper.registerStylesheetFromNodeModule("bootstrap", 11 | //"bootstrap/dist/css/bootstrap.min.css", 12 | "bootstrap-dark-5/dist/css/bootstrap-dark.css" 13 | ); 14 | 15 | 16 | const RegisterServiceButton = ( 17 | props: Renderer.Component.KubeObjectDetailsProps 18 | ) => ( 19 |
30 |
31 |
32 |
40 | App Viewer 41 |
42 |
43 | 50 | You can add these ports to App viewer 51 | 52 |
53 |
62 | {props.object.getPorts().map((port: any) => ( 63 | { 66 | var editor: SettingsEditor = new SettingsEditor(); 67 | var cluster: any = editor.data.getCluster( 68 | Helper.getCurrentClusterId() 69 | ); 70 | cluster.name = Helper.getCurrentClusterName(); 71 | cluster.registerService(props.object, port); 72 | editor.save(); 73 | }} 74 | > 75 | {port.port}:{port.targetPort} 76 | 77 | ))} 78 |
79 |
80 | ); 81 | 82 | export default class UIExtension extends Renderer.LensExtension { 83 | constructor(a: any) { 84 | super(a); 85 | } 86 | clusterPages = [ 87 | { 88 | id: "appviewer", 89 | components: { 90 | Page: (props: any) => ( 91 | 92 | ), 93 | MenuIcon: AiTwotoneAppstore, 94 | }, 95 | }, 96 | ]; 97 | clusterPageMenus = [ 98 | { 99 | target: { pageId: "appviewer" }, 100 | title: "App Viewer", 101 | components: { 102 | Icon: () => ( 103 | 107 | ), 108 | }, 109 | }, 110 | ]; 111 | kubeObjectDetailItems = [ 112 | { 113 | kind: "Service", 114 | apiVersions: ["v1"], 115 | components: { 116 | Details: RegisterServiceButton, 117 | }, 118 | }, 119 | ]; 120 | } 121 | -------------------------------------------------------------------------------- /src/AppViewerPage.tsx: -------------------------------------------------------------------------------- 1 | import { Common, Main, Renderer } from "@k8slens/extensions"; 2 | import cluster from "cluster"; 3 | import React, { useState } from "react"; 4 | import { Helper } from "../Helper"; 5 | import { FcInfo } from "react-icons/fc"; 6 | import gis from "g-i-s"; 7 | import Loading from "./Loading"; 8 | import { GiCubeforce } from "react-icons/gi"; 9 | import stringSimilarity from "string-similarity"; 10 | import { VscEdit, VscSave } from "react-icons/vsc"; 11 | import { AiFillDelete } from "react-icons/ai"; 12 | import {ImCancelCircle} from 'react-icons/im'; 13 | 14 | import { 15 | ClusterServiceSettings, 16 | ClusterSettings, 17 | SettingsEditor, 18 | } from "../SettingsEditor"; 19 | import { Toast, Button, Alert, Row, Col } from "react-bootstrap"; 20 | 21 | export default class AppViewerPage extends React.Component { 22 | props: { 23 | extension: any; 24 | pageProps: any; 25 | }; 26 | settings: SettingsEditor; 27 | state = { 28 | canEdit: false, 29 | loading: true, 30 | }; 31 | 32 | constructor(props: any) { 33 | super(props); 34 | this.settings = new SettingsEditor(); 35 | var timer = setInterval(() => { 36 | var id = Helper.getCurrentClusterId(); 37 | if (id !== null && id !== undefined) { 38 | clearInterval(timer); 39 | this.setState({ loading: false }); 40 | } 41 | }, 200); 42 | } 43 | saveChanges() { 44 | this.setState({ loading: true }); 45 | this.settings.save(); 46 | this.setState({ canEdit: false, loading: false }); 47 | } 48 | cancelChanges() { 49 | this.setState({ loading: true, canEdit: false }); 50 | this.settings.reload(); 51 | this.setState({ loading: false }); 52 | } 53 | edit() { 54 | this.setState({ canEdit: true }); 55 | } 56 | async itemOnDelete(item: ClusterServiceSettings, panel: AppViewerPage) { 57 | panel.setState({ loading: true }); 58 | const data = panel.settings.data; 59 | const cluster = panel.settings.data.Clusters.filter( 60 | (p) => p.Id == Helper.getCurrentClusterId() 61 | )[0]; 62 | cluster.Services = cluster.Services.filter((p) => p.Id !== item.Id); 63 | data.Clusters = [...data.Clusters]; 64 | panel.settings.data = data; 65 | 66 | panel.setState({ loading: false }, () => { 67 | panel.forceUpdate(); 68 | }); 69 | } 70 | render() { 71 | var items: ClusterServiceSettings[]; 72 | var cluster: ClusterSettings; 73 | 74 | try { 75 | cluster = this.settings.data.Clusters.filter( 76 | (p) => p.Id == Helper.getCurrentClusterId() 77 | )[0]; 78 | items = cluster.Services || []; 79 | } catch (err) { 80 | items = []; 81 | } 82 | return ( 83 |
91 | 92 | 93 |
94 |
95 |

App Viewer

96 |
97 | {this.state.canEdit ? ( 98 |
99 | 102 | 109 |
110 | ) : ( 111 | 117 | )} 118 |
119 |
120 |
121 |
122 |
123 |
124 | {items.map((item, i) => { 125 | return ( 126 | 134 | ); 135 | })} 136 |
137 |
138 |
139 | ); 140 | } 141 | } 142 | 143 | class Styles { 144 | public static AppItem: React.CSSProperties = { 145 | padding: 10, 146 | backgroundColor: "var(--sidebarBackground)", 147 | borderRadius: 12, 148 | margin: 10, 149 | boxShadow: "0px 4px 20px var(--boxShadow)", 150 | color: "var(--textColorPrimary)", 151 | border: "1px solid rgba(0,0,0,0.1)", 152 | maxWidth: 250, 153 | minWidth: 250, 154 | width: 250, 155 | minHeight: 250, 156 | maxHeight: 250, 157 | height: 250, 158 | display: "flex", 159 | }; 160 | } 161 | class AppItem extends React.Component { 162 | props: { 163 | item: ClusterServiceSettings; 164 | cluster: ClusterSettings; 165 | canEdit: Boolean; 166 | onDelete: (item: ClusterServiceSettings, panel: AppViewerPage) => void; 167 | panel: AppViewerPage; 168 | }; 169 | state = { 170 | loading: false, 171 | icon: null as string, 172 | id: null as string, 173 | }; 174 | async portForward() { 175 | this.setState({ loading: true }); 176 | const item = this.props.item; 177 | const cluster = this.props.cluster; 178 | const apiPath = `/api/pods/${item.Namespace}/service/${item.ServiceName}/port-forward/${item.Port.port}`; 179 | const apiUrl = `http://localhost:${location.port}`; 180 | try { 181 | var result = await fetch(apiUrl + apiPath, { 182 | method: "post", 183 | headers: { 184 | Host: `${cluster.Id}.localhost:${location.port}`, 185 | "Content-Type": "application/json", 186 | }, 187 | }) 188 | .then((p) => p.json()) 189 | .catch(console.error); 190 | } catch (error) { 191 | console.error(error); 192 | } finally { 193 | setTimeout(() => { 194 | this.setState({ loading: false }); 195 | }, 1000); 196 | } 197 | } 198 | 199 | async openApp() { 200 | this.portForward.call(this); 201 | } 202 | async loadLogo() { 203 | var search = ""; 204 | this.setState({ loading: true }); 205 | const formatName = (v: string) => { 206 | var parts = v 207 | .split(/[-\s]+/g) 208 | .filter((p) => { 209 | if (!isNaN(parseFloat(p))) { 210 | return false; 211 | } 212 | return true; 213 | }) 214 | .join(" ") 215 | .split(" "); 216 | return parts.join(" "); 217 | }; 218 | var searchTerm: string = formatName(this.props.item.Name); 219 | if ( 220 | this.props.item.Labels && 221 | this.props.item.Labels.hasOwnProperty("app.kubernetes.io/name") 222 | ) { 223 | searchTerm = formatName(this.props.item.Labels["app.kubernetes.io/name"]); 224 | } 225 | search = searchTerm + " logo"; 226 | var id = 227 | this.props.cluster.Id + 228 | "_" + 229 | this.props.item.ServiceName + 230 | "_" + 231 | this.props.item.Namespace; 232 | 233 | if (await Helper.haveLogo(id)) { 234 | this.setState({ 235 | icon: await Helper.cacheLogo(id, null) 236 | .then((p) => p.logo) 237 | .catch(console.error), 238 | }); 239 | setTimeout(() => { 240 | this.setState({ loading: false }); 241 | }, 1000); 242 | } else { 243 | gis(search, async (err, res) => { 244 | var matched = res 245 | .filter((p) => p.url.endsWith(".svg")) 246 | .map((p) => p.url)[0]; 247 | if (!matched) { 248 | matched = res 249 | .filter((p) => p.url.endsWith(".png")) 250 | .map((p) => p.url)[0]; 251 | } 252 | 253 | if (matched) { 254 | this.setState({ 255 | icon: await Helper.cacheLogo(id, matched) 256 | .then((p) => p.logo) 257 | .catch(console.error), 258 | }); 259 | } 260 | setTimeout(() => { 261 | this.setState({ loading: false }); 262 | }, 1000); 263 | }); 264 | } 265 | } 266 | componentDidMount() { 267 | this.loadLogo.call(this); 268 | } 269 | componentDidUpdate() { 270 | if (this.props.item.Id !== this.state.id) { 271 | this.setState({ id: this.props.item.Id, icon: null }); 272 | this.loadLogo.call(this); 273 | } 274 | } 275 | render() { 276 | const item = this.props.item; 277 | const canEdit = this.props.canEdit; 278 | return ( 279 |
280 |
281 | 282 | 283 |
293 | {canEdit && ( 294 |
298 | 309 |
310 | )} 311 |
321 | {this.state.icon ? ( 322 | { 324 | this.setState({ icon: null }); 325 | }} 326 | style={{ maxHeight: 80, margin: "auto" }} 327 | src={this.state.icon} 328 | > 329 | ) : ( 330 | 331 | )} 332 |
333 |
342 | {canEdit ? ( 343 |
344 | { 358 | item.Name = evt.target.value; 359 | this.forceUpdate(); 360 | }} 361 | /> 362 |
363 | ) : ( 364 |
372 | {item.Name} 373 |
374 | )} 375 |
376 |
377 |
378 |
379 | ); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /src/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import Spinner from 'react-spinner-material'; 4 | import color from 'color'; 5 | import { Helper } from '../Helper'; 6 | 7 | Helper.registerStylesheetFromCurrentDir("loading", "src/styles/Loading.css"); 8 | export default class Loading extends Component { 9 | props: { 10 | show: boolean, 11 | bg?: string, 12 | color?: string, 13 | children?: any 14 | }; 15 | componentDidMount() { 16 | var internalFiber = (this as any)._reactInternalFiber || (this as any)._reactInternals; 17 | if (!internalFiber) return; 18 | 19 | (this as any).parentNode = internalFiber.return.stateNode; 20 | (this as any).currentNode = internalFiber.child.stateNode; 21 | (this as any).parentNode.style.position = 'relative'; 22 | 23 | 24 | var computedParent = window.getComputedStyle((this as any).parentNode); 25 | (this as any).currentNode.style.borderRadius = computedParent.borderRadius; 26 | } 27 | render() { 28 | const backColor = color((this as any).props.bg || 'white').alpha(0.5); 29 | 30 | return
31 |
32 |
33 | 34 |
35 |
36 | {this.props.children} 37 |
38 |
39 |
; 40 | } 41 | } -------------------------------------------------------------------------------- /src/styles/Loading.css: -------------------------------------------------------------------------------- 1 | .__react__loadingcontent { 2 | position: absolute; 3 | left: 0px; 4 | top: 0px; 5 | right: 0px; 6 | bottom: 0px; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | transition: all 300ms ease; 11 | 12 | background-image: url(data:image/png;base64,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); 13 | 14 | background-color: rgba(0,0,0, 0.5); 15 | z-index: 3; 16 | backdrop-filter: blur(3px); 17 | box-shadow: inset 0px 10px 50px rgba(0, 0, 0, 0.1); 18 | overflow: hidden !important; 19 | } 20 | .__react__loadingcontent::before { 21 | display: block; 22 | content: ' '; 23 | position: absolute; 24 | z-index: 2; 25 | background: linear-gradient(90deg, transparent, transparent); 26 | width: 100%; 27 | backdrop-filter: blur(10px); 28 | left: 0px; 29 | top: 0px; 30 | bottom: 0px; 31 | animation: __react__loadingcontent_ani 1s infinite ease; 32 | } 33 | 34 | @keyframes __react__loadingcontent_ani { 35 | 0% { 36 | left: -100%; 37 | } 38 | 100% { 39 | left: 100%; 40 | } 41 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "CommonJS", 5 | "target": "ES2017", 6 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 7 | "moduleResolution": "Node", 8 | "sourceMap": false, 9 | "declaration": false, 10 | "strict": false, 11 | "noImplicitAny": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "experimentalDecorators": true, 16 | "jsx": "react", 17 | "allowJs": true 18 | }, 19 | "include": ["./*.ts", "./*.tsx"], 20 | "exclude": ["node_modules", "*.js"] 21 | } 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = [ 4 | { 5 | entry: "./main.ts", 6 | context: __dirname, 7 | target: "electron-main", 8 | mode: "production", 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.tsx?$/, 13 | use: "ts-loader", 14 | exclude: /node_modules/, 15 | }, 16 | ], 17 | }, 18 | externals: [ 19 | { 20 | "@k8slens/extensions": "var global.LensExtensions", 21 | mobx: "var global.Mobx", 22 | react: "var global.React", 23 | }, 24 | ], 25 | resolve: { 26 | extensions: [".tsx", ".ts", ".js"], 27 | }, 28 | output: { 29 | libraryTarget: "commonjs2", 30 | filename: "main.js", 31 | path: path.resolve(__dirname, "dist"), 32 | }, 33 | }, 34 | { 35 | entry: "./renderer.tsx", 36 | context: __dirname, 37 | target: "electron-renderer", 38 | mode: "production", 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.tsx?$/, 43 | use: "ts-loader", 44 | exclude: /node_modules/, 45 | }, 46 | ], 47 | }, 48 | externals: [ 49 | { 50 | browser: "var global.currentBrowser", 51 | "@k8slens/extensions": "var global.LensExtensions", 52 | react: "var global.React", 53 | mobx: "var global.Mobx", 54 | }, 55 | ], 56 | resolve: { 57 | extensions: [".tsx", ".ts", ".js"], 58 | }, 59 | output: { 60 | libraryTarget: "commonjs2", 61 | globalObject: "this", 62 | filename: "renderer.js", 63 | path: path.resolve(__dirname, "dist"), 64 | }, 65 | node: { 66 | __dirname: false, 67 | __filename: false, 68 | }, 69 | }, 70 | ]; 71 | --------------------------------------------------------------------------------