mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into documentation-typedocs
# Conflicts: # mkdocs.yml # package.json # yarn.lock
This commit is contained in:
commit
57c00e7298
@ -37,6 +37,10 @@ jobs:
|
|||||||
displayName: Cache Yarn packages
|
displayName: Cache Yarn packages
|
||||||
- script: make install-deps
|
- script: make install-deps
|
||||||
displayName: Install dependencies
|
displayName: Install dependencies
|
||||||
|
- script: make build-npm
|
||||||
|
displayName: Generate npm package
|
||||||
|
- script: make build-extensions
|
||||||
|
displayName: Build bundled extensions
|
||||||
- script: make integration-win
|
- script: make integration-win
|
||||||
displayName: Run integration tests
|
displayName: Run integration tests
|
||||||
- script: make test-extensions
|
- script: make test-extensions
|
||||||
@ -76,6 +80,10 @@ jobs:
|
|||||||
condition: eq(variables.CACHE_RESTORED, 'true')
|
condition: eq(variables.CACHE_RESTORED, 'true')
|
||||||
- script: make install-deps
|
- script: make install-deps
|
||||||
displayName: Install dependencies
|
displayName: Install dependencies
|
||||||
|
- script: make build-npm
|
||||||
|
displayName: Generate npm package
|
||||||
|
- script: make build-extensions
|
||||||
|
displayName: Build bundled extensions
|
||||||
- script: make test
|
- script: make test
|
||||||
displayName: Run tests
|
displayName: Run tests
|
||||||
- script: make integration-mac
|
- script: make integration-mac
|
||||||
@ -127,6 +135,10 @@ jobs:
|
|||||||
displayName: Run In-tree Extension tests
|
displayName: Run In-tree Extension tests
|
||||||
- script: make lint
|
- script: make lint
|
||||||
displayName: Lint
|
displayName: Lint
|
||||||
|
- script: make build-npm
|
||||||
|
displayName: Generate npm package
|
||||||
|
- script: make build-extensions
|
||||||
|
displayName: Build bundled extensions
|
||||||
- script: make test
|
- script: make test
|
||||||
displayName: Run tests
|
displayName: Run tests
|
||||||
- bash: |
|
- bash: |
|
||||||
|
|||||||
@ -17,11 +17,11 @@ There are many sites where you can vote, recommend, favorite and star us.
|
|||||||
|
|
||||||
Here are some nice blog posts and videos about our project for you to get some inspiration:
|
Here are some nice blog posts and videos about our project for you to get some inspiration:
|
||||||
|
|
||||||
[Onboard AWS EKS Cluster on Lens(Kubernetes IDE)](https://dev.to/himwad05/onboard-aws-eks-cluster-on-lens-kubernetes-ide-492l)
|
* [Onboard AWS EKS Cluster on Lens(Kubernetes IDE)](https://dev.to/himwad05/onboard-aws-eks-cluster-on-lens-kubernetes-ide-492l)
|
||||||
[Using Lens to Manage All Your Kubernetes Cluster](https://medium.com/@magicmagnate/using-lens-to-manage-all-your-kubernetes-cluster-c1ef88fdb476)
|
* [Using Lens to Manage All Your Kubernetes Cluster](https://medium.com/@magicmagnate/using-lens-to-manage-all-your-kubernetes-cluster-c1ef88fdb476)
|
||||||
[Kontena Lens - Beautiful Kubernetes UI](https://www.youtube.com/watch?v=YGgaiGdYfdI)
|
* [Kontena Lens - Beautiful Kubernetes UI](https://www.youtube.com/watch?v=YGgaiGdYfdI)
|
||||||
[Gerenciando Kubernetes com Lens e Octant](https://www.youtube.com/watch?v=h9ZqDelJLQQ)
|
* [Gerenciando Kubernetes com Lens e Octant](https://www.youtube.com/watch?v=h9ZqDelJLQQ)
|
||||||
[Walkthrough of Kubernetes IDE - Lens](https://www.youtube.com/watch?v=602aHZSdEfY)
|
* [Walkthrough of Kubernetes IDE - Lens](https://www.youtube.com/watch?v=602aHZSdEfY)
|
||||||
[LENS - Interfaz Gráfica para Kubernetes en 1 PASO.](https://www.youtube.com/watch?v=DFMKcR4BqwM)
|
* [LENS - Interfaz Gráfica para Kubernetes en 1 PASO.](https://www.youtube.com/watch?v=DFMKcR4BqwM)
|
||||||
|
|
||||||
Psst... If you have created some content around Lens, let us know!
|
Psst... If you have created some content around Lens, let us know!
|
||||||
|
|||||||
20
docs/extensions/get-started/overview.md
Normal file
20
docs/extensions/get-started/overview.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Extension Development Overview
|
||||||
|
|
||||||
|
This is a general overview to how the development of an extension will procede. For building extensions there will be a few things that you should have installed, and some that might help.
|
||||||
|
|
||||||
|
### Required:
|
||||||
|
- [Node.js](https://www.nodejs.org/en/)
|
||||||
|
- [Git](https://www.git-scm.com/)
|
||||||
|
- Some sort of text editor, we recommend [VSCode](https://code.visualstudio.com/)
|
||||||
|
- We use [Webpack](https://www.webpack.js.org/) for compilation. All extension need to be at least compatable with a webpack system.
|
||||||
|
|
||||||
|
### Recommened:
|
||||||
|
-
|
||||||
|
|
||||||
|
All *Lens* extensions are javascript packages. We recommend that you program in [Typescript](https://www.typescriptlang.org/) because it catches quite a few easily to make errors around passing data around in javascript.
|
||||||
|
|
||||||
|
*Lens* is a standard [Electron](https://www.electronjs.org/) application which both main and renderer processes. An extension is made up of two parts, one for each of *Lens*'s core processes. When an extension is loaded each part is loaded and then notified that it has been loaded. From there the extension can start doing is work.
|
||||||
|
|
||||||
|
*Lens* uses [React](https://www.reactjs.org/) as it UI framework and even provides some of our own components for reuse by extensions. An extension is resonsible for the lifetime of any resources it spins up. If an extension's main part starts new processes they all must be stopped and cleaned up when the extension is deactivated or unloaded.
|
||||||
|
|
||||||
|
See [Your First Extension](your-first-extension.md) to get started.
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# Your First Extension
|
# Your First Extension
|
||||||
|
|
||||||
In this topic, we'll teach you the fundamental concepts for building extensions. Make sure you have [Node.js](https://nodejs.org/en/) and [Git](https://git-scm.com/) installed....
|
In this topic, we'll teach you the fundamental concepts for building extensions.
|
||||||
|
|
||||||
## Installing and Building the extension
|
## Installing and Building the extension
|
||||||
|
|
||||||
|
|||||||
@ -410,6 +410,40 @@ describe("Lens integration tests", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("viewing pod logs", () => {
|
||||||
|
beforeEach(appStartAddCluster, 40000)
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (app && app.isRunning()) {
|
||||||
|
return util.tearDown(app)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`shows a logs for a pod`, async () => {
|
||||||
|
expect(clusterAdded).toBe(true)
|
||||||
|
// Go to Pods page
|
||||||
|
await app.client.click(".sidebar-nav #workloads span.link-text")
|
||||||
|
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods")
|
||||||
|
await app.client.click('a[href^="/pods"]')
|
||||||
|
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver")
|
||||||
|
// Open logs tab in dock
|
||||||
|
await app.client.click(".list .TableRow:first-child")
|
||||||
|
await app.client.waitForVisible(".Drawer")
|
||||||
|
await app.client.click(".drawer-title .Menu li:nth-child(2)")
|
||||||
|
// Check if controls are available
|
||||||
|
await app.client.waitForVisible(".PodLogs .VirtualList")
|
||||||
|
await app.client.waitForVisible(".PodLogControls")
|
||||||
|
await app.client.waitForVisible(".PodLogControls .SearchInput")
|
||||||
|
await app.client.waitForVisible(".PodLogControls .SearchInput input")
|
||||||
|
// Search for semicolon
|
||||||
|
await app.client.keys(":")
|
||||||
|
await app.client.waitForVisible(".PodLogs .list span.active")
|
||||||
|
// Click through controls
|
||||||
|
await app.client.click(".PodLogControls .timestamps-icon")
|
||||||
|
await app.client.click(".PodLogControls .undo-icon")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("cluster operations", () => {
|
describe("cluster operations", () => {
|
||||||
beforeEach(appStartAddCluster, 40000)
|
beforeEach(appStartAddCluster, 40000)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -35,7 +35,13 @@ nav:
|
|||||||
- Publishing Extensions: extensions/testing-and-publishing/publishing.md
|
- Publishing Extensions: extensions/testing-and-publishing/publishing.md
|
||||||
- Bundling Extensions: extensions/testing-and-publishing/bundling.md
|
- Bundling Extensions: extensions/testing-and-publishing/bundling.md
|
||||||
- API Reference: extensions/api/modules/_src_extensions_extension_api_.md
|
- API Reference: extensions/api/modules/_src_extensions_extension_api_.md
|
||||||
- Contributing: contributing/README.md
|
- Contributing:
|
||||||
|
- Overview: contributing/README.md
|
||||||
|
- Development: contributing/development.md
|
||||||
|
- Documentation: contributing/documentation.md
|
||||||
|
- Maintainers: contributing/maintainers.md
|
||||||
|
- Promotion: contributing/promotion.md
|
||||||
|
|
||||||
- FAQ: faq/README.md
|
- FAQ: faq/README.md
|
||||||
theme:
|
theme:
|
||||||
name: 'material'
|
name: 'material'
|
||||||
|
|||||||
11
package.json
11
package.json
@ -15,7 +15,7 @@
|
|||||||
"dev-build": "concurrently yarn:compile:*",
|
"dev-build": "concurrently yarn:compile:*",
|
||||||
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"",
|
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"",
|
||||||
"dev:main": "yarn compile:main --watch",
|
"dev:main": "yarn compile:main --watch",
|
||||||
"dev:renderer": "yarn compile:renderer --watch",
|
"dev:renderer": "yarn webpack-dev-server --config webpack.renderer.ts",
|
||||||
"dev:extension-types": "yarn compile:extension-types --watch",
|
"dev:extension-types": "yarn compile:extension-types --watch",
|
||||||
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
|
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
|
||||||
"compile:main": "webpack --config webpack.main.ts",
|
"compile:main": "webpack --config webpack.main.ts",
|
||||||
@ -269,6 +269,7 @@
|
|||||||
"@lingui/macro": "^3.0.0-13",
|
"@lingui/macro": "^3.0.0-13",
|
||||||
"@lingui/react": "^3.0.0-13",
|
"@lingui/react": "^3.0.0-13",
|
||||||
"@material-ui/core": "^4.10.1",
|
"@material-ui/core": "^4.10.1",
|
||||||
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||||
"@rollup/plugin-json": "^4.1.0",
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
"@testing-library/jest-dom": "^5.11.5",
|
"@testing-library/jest-dom": "^5.11.5",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
@ -280,6 +281,7 @@
|
|||||||
"@types/electron-window-state": "^2.0.34",
|
"@types/electron-window-state": "^2.0.34",
|
||||||
"@types/fs-extra": "^9.0.1",
|
"@types/fs-extra": "^9.0.1",
|
||||||
"@types/hapi": "^18.0.3",
|
"@types/hapi": "^18.0.3",
|
||||||
|
"@types/hard-source-webpack-plugin": "^1.0.1",
|
||||||
"@types/hoist-non-react-statics": "^3.3.1",
|
"@types/hoist-non-react-statics": "^3.3.1",
|
||||||
"@types/html-webpack-plugin": "^3.2.3",
|
"@types/html-webpack-plugin": "^3.2.3",
|
||||||
"@types/http-proxy": "^1.17.4",
|
"@types/http-proxy": "^1.17.4",
|
||||||
@ -316,6 +318,7 @@
|
|||||||
"@types/uuid": "^8.0.0",
|
"@types/uuid": "^8.0.0",
|
||||||
"@types/webdriverio": "^4.13.0",
|
"@types/webdriverio": "^4.13.0",
|
||||||
"@types/webpack": "^4.41.17",
|
"@types/webpack": "^4.41.17",
|
||||||
|
"@types/webpack-dev-server": "^3.11.1",
|
||||||
"@types/webpack-env": "^1.15.2",
|
"@types/webpack-env": "^1.15.2",
|
||||||
"@types/webpack-node-externals": "^1.7.1",
|
"@types/webpack-node-externals": "^1.7.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.0.0",
|
"@typescript-eslint/eslint-plugin": "^4.0.0",
|
||||||
@ -340,6 +343,7 @@
|
|||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
"flex.box": "^3.4.4",
|
"flex.box": "^3.4.4",
|
||||||
"fork-ts-checker-webpack-plugin": "^5.0.0",
|
"fork-ts-checker-webpack-plugin": "^5.0.0",
|
||||||
|
"hard-source-webpack-plugin": "^0.13.1",
|
||||||
"hoist-non-react-statics": "^3.3.2",
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
"html-webpack-plugin": "^4.3.0",
|
"html-webpack-plugin": "^4.3.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
@ -361,6 +365,7 @@
|
|||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
"react-beautiful-dnd": "^13.0.0",
|
"react-beautiful-dnd": "^13.0.0",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
|
"react-refresh": "^0.9.0",
|
||||||
"react-router": "^5.2.0",
|
"react-router": "^5.2.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-select": "^3.1.0",
|
"react-select": "^3.1.0",
|
||||||
@ -379,11 +384,13 @@
|
|||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
"typedoc": "^0.19.2",
|
"typedoc": "^0.19.2",
|
||||||
"typedoc-plugin-markdown": "^3.0.11",
|
"typedoc-plugin-markdown": "^3.0.11",
|
||||||
|
"type-fest": "^0.18.0",
|
||||||
"typeface-roboto": "^0.0.75",
|
"typeface-roboto": "^0.0.75",
|
||||||
"typescript": "^4.0.2",
|
"typescript": "^4.0.2",
|
||||||
"url-loader": "^4.1.0",
|
"url-loader": "^4.1.0",
|
||||||
"webpack": "^4.43.0",
|
"webpack": "^4.44.2",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^3.3.11",
|
||||||
|
"webpack-dev-server": "^3.11.0",
|
||||||
"webpack-node-externals": "^1.7.2",
|
"webpack-node-externals": "^1.7.2",
|
||||||
"xterm": "^4.6.0",
|
"xterm": "^4.6.0",
|
||||||
"xterm-addon-fit": "^0.4.0"
|
"xterm-addon-fit": "^0.4.0"
|
||||||
|
|||||||
80
src/common/__tests__/search-store.test.ts
Normal file
80
src/common/__tests__/search-store.test.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SearchStore } from "../search-store"
|
||||||
|
|
||||||
|
let searchStore: SearchStore = null;
|
||||||
|
|
||||||
|
const logs = [
|
||||||
|
"1:M 30 Oct 2020 16:17:41.553 # Connection with replica 172.17.0.12:6379 lost",
|
||||||
|
"1:M 30 Oct 2020 16:17:41.623 * Replica 172.17.0.12:6379 asks for synchronization",
|
||||||
|
"1:M 30 Oct 2020 16:17:41.623 * Starting Partial resynchronization request from 172.17.0.12:6379 accepted. Sending 0 bytes of backlog starting from offset 14407."
|
||||||
|
]
|
||||||
|
|
||||||
|
describe("search store tests", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
searchStore = new SearchStore();
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does nothing with empty search query", () => {
|
||||||
|
searchStore.onSearch([], "");
|
||||||
|
expect(searchStore.occurrences).toEqual([]);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("doesn't break if no text provided", () => {
|
||||||
|
searchStore.onSearch(null, "replica");
|
||||||
|
expect(searchStore.occurrences).toEqual([]);
|
||||||
|
|
||||||
|
searchStore.onSearch([], "replica");
|
||||||
|
expect(searchStore.occurrences).toEqual([]);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("find 3 occurences across 3 lines", () => {
|
||||||
|
searchStore.onSearch(logs, "172");
|
||||||
|
expect(searchStore.occurrences).toEqual([0, 1, 2]);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("find occurences within 1 line (case-insensitive)", () => {
|
||||||
|
searchStore.onSearch(logs, "Starting");
|
||||||
|
expect(searchStore.occurrences).toEqual([2, 2]);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sets overlay index equal to first occurence", () => {
|
||||||
|
searchStore.onSearch(logs, "Replica");
|
||||||
|
expect(searchStore.activeOverlayIndex).toBe(0);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("set overlay index to next occurence", () => {
|
||||||
|
searchStore.onSearch(logs, "172");
|
||||||
|
searchStore.setNextOverlayActive();
|
||||||
|
expect(searchStore.activeOverlayIndex).toBe(1);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sets overlay to last occurence", () => {
|
||||||
|
searchStore.onSearch(logs, "172");
|
||||||
|
searchStore.setPrevOverlayActive();
|
||||||
|
expect(searchStore.activeOverlayIndex).toBe(2);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("gets line index where overlay is located", () => {
|
||||||
|
searchStore.onSearch(logs, "synchronization");
|
||||||
|
expect(searchStore.activeOverlayLine).toBe(1);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("escapes string for using in regex", () => {
|
||||||
|
const regex = searchStore.escapeRegex("some.interesting-query\\#?()[]");
|
||||||
|
expect(regex).toBe("some\\.interesting\\-query\\\\\\#\\?\\(\\)\\[\\]");
|
||||||
|
})
|
||||||
|
|
||||||
|
it("gets active find number", () => {
|
||||||
|
searchStore.onSearch(logs, "172");
|
||||||
|
searchStore.setNextOverlayActive();
|
||||||
|
expect(searchStore.activeFind).toBe(2);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("gets total finds number", () => {
|
||||||
|
searchStore.onSearch(logs, "Starting");
|
||||||
|
expect(searchStore.totalFinds).toBe(2);
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -39,6 +39,7 @@ export interface ClusterModel {
|
|||||||
preferences?: ClusterPreferences;
|
preferences?: ClusterPreferences;
|
||||||
metadata?: ClusterMetadata;
|
metadata?: ClusterMetadata;
|
||||||
ownerRef?: string;
|
ownerRef?: string;
|
||||||
|
accessibleNamespaces?: string[];
|
||||||
|
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
kubeConfig?: string; // yaml
|
kubeConfig?: string; // yaml
|
||||||
@ -179,8 +180,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addCluster(model: ClusterModel | Cluster ): Cluster {
|
addCluster(model: ClusterModel | Cluster): Cluster {
|
||||||
appEventBus.emit({name: "cluster", action: "add"})
|
appEventBus.emit({ name: "cluster", action: "add" })
|
||||||
let cluster = model as Cluster;
|
let cluster = model as Cluster;
|
||||||
if (!(model instanceof Cluster)) {
|
if (!(model instanceof Cluster)) {
|
||||||
cluster = new Cluster(model)
|
cluster = new Cluster(model)
|
||||||
@ -195,7 +196,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
async removeById(clusterId: ClusterId) {
|
async removeById(clusterId: ClusterId) {
|
||||||
appEventBus.emit({name: "cluster", action: "remove"})
|
appEventBus.emit({ name: "cluster", action: "remove" })
|
||||||
const cluster = this.getById(clusterId);
|
const cluster = this.getById(clusterId);
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
this.clusters.delete(clusterId);
|
this.clusters.delete(clusterId);
|
||||||
|
|||||||
126
src/common/search-store.ts
Normal file
126
src/common/search-store.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { action, computed, observable } from "mobx";
|
||||||
|
import { autobind } from "../renderer/utils";
|
||||||
|
|
||||||
|
export class SearchStore {
|
||||||
|
@observable searchQuery = ""; // Text in the search input
|
||||||
|
@observable occurrences: number[] = []; // Array with line numbers, eg [0, 0, 10, 21, 21, 40...]
|
||||||
|
@observable activeOverlayIndex = -1; // Index withing the occurences array. Showing where is activeOverlay currently located
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets default activeOverlayIndex
|
||||||
|
* @param text An array of any textual data (logs, for example)
|
||||||
|
* @param query Search query from input
|
||||||
|
*/
|
||||||
|
@action
|
||||||
|
onSearch(text: string[], query = this.searchQuery) {
|
||||||
|
this.searchQuery = query;
|
||||||
|
if (!query) {
|
||||||
|
this.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.occurrences = this.findOccurences(text, query);
|
||||||
|
if (!this.occurrences.length) return;
|
||||||
|
|
||||||
|
// If new highlighted keyword in exact same place as previous one, then no changing in active overlay
|
||||||
|
if (this.occurrences[this.activeOverlayIndex] !== undefined) return;
|
||||||
|
this.activeOverlayIndex = this.getNextOverlay(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does searching within text array, create a list of search keyword occurences.
|
||||||
|
* Each keyword "occurency" is saved as index of the the line where keyword founded
|
||||||
|
* @param text An array of any textual data (logs, for example)
|
||||||
|
* @param query Search query from input
|
||||||
|
* @returns {Array} Array of line indexes [0, 0, 14, 17, 17, 17, 20...]
|
||||||
|
*/
|
||||||
|
findOccurences(text: string[], query: string) {
|
||||||
|
if (!text) return [];
|
||||||
|
const occurences: number[] = [];
|
||||||
|
text.forEach((line, index) => {
|
||||||
|
const regex = new RegExp(this.escapeRegex(query), "gi");
|
||||||
|
const matches = [...line.matchAll(regex)];
|
||||||
|
matches.forEach(() => occurences.push(index));
|
||||||
|
});
|
||||||
|
return occurences;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getting next overlay index within the occurences array
|
||||||
|
* @param loopOver Allows to jump from last element to first
|
||||||
|
* @returns {number} next overlay index
|
||||||
|
*/
|
||||||
|
getNextOverlay(loopOver = false) {
|
||||||
|
const next = this.activeOverlayIndex + 1;
|
||||||
|
if (next > this.occurrences.length - 1) {
|
||||||
|
return loopOver ? 0 : this.activeOverlayIndex;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getting previous overlay index within the occurences array of occurences
|
||||||
|
* @param loopOver Allows to jump from first element to last one
|
||||||
|
* @returns {number} prev overlay index
|
||||||
|
*/
|
||||||
|
getPrevOverlay(loopOver = false) {
|
||||||
|
const prev = this.activeOverlayIndex - 1;
|
||||||
|
if (prev < 0) {
|
||||||
|
return loopOver ? this.occurrences.length - 1 : this.activeOverlayIndex;
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind()
|
||||||
|
setNextOverlayActive() {
|
||||||
|
this.activeOverlayIndex = this.getNextOverlay(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind()
|
||||||
|
setPrevOverlayActive() {
|
||||||
|
this.activeOverlayIndex = this.getPrevOverlay(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets line index of where active overlay is located
|
||||||
|
* @returns {number} A line index within the text/logs array
|
||||||
|
*/
|
||||||
|
@computed get activeOverlayLine(): number {
|
||||||
|
return this.occurrences[this.activeOverlayIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get activeFind(): number {
|
||||||
|
return this.activeOverlayIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get totalFinds(): number {
|
||||||
|
return this.occurrences.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if overlay is active (to highlight it with orange background usually)
|
||||||
|
* @param line Index of the line where overlay is located
|
||||||
|
* @param occurence Number of the overlay within one line
|
||||||
|
*/
|
||||||
|
@autobind()
|
||||||
|
isActiveOverlay(line: number, occurence: number) {
|
||||||
|
const firstLineIndex = this.occurrences.findIndex(item => item === line);
|
||||||
|
return firstLineIndex + occurence === this.activeOverlayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An utility methods escaping user string to safely pass it into new Regex(variable)
|
||||||
|
* @param value Unescaped string
|
||||||
|
*/
|
||||||
|
escapeRegex(value: string) {
|
||||||
|
return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" );
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
reset() {
|
||||||
|
this.searchQuery = "";
|
||||||
|
this.activeOverlayIndex = -1;
|
||||||
|
this.occurrences = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const searchStore = new SearchStore;
|
||||||
@ -22,6 +22,7 @@ export const mainDir = path.join(contextDir, "src/main");
|
|||||||
export const rendererDir = path.join(contextDir, "src/renderer");
|
export const rendererDir = path.join(contextDir, "src/renderer");
|
||||||
export const htmlTemplate = path.resolve(rendererDir, "template.html");
|
export const htmlTemplate = path.resolve(rendererDir, "template.html");
|
||||||
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
|
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
|
||||||
|
export const webpackDevServerPort = 9009
|
||||||
|
|
||||||
// Special runtime paths
|
// Special runtime paths
|
||||||
defineGlobal("__static", {
|
defineGlobal("__static", {
|
||||||
|
|||||||
@ -1,23 +1,38 @@
|
|||||||
// TODO: add more common re-usable UI components + refactor interfaces (Props -> ComponentProps)
|
// Common UI components
|
||||||
|
|
||||||
export * from "../../renderer/components/icon"
|
// layouts
|
||||||
export * from "../../renderer/components/checkbox"
|
|
||||||
export * from "../../renderer/components/tooltip"
|
|
||||||
export * from "../../renderer/components/button"
|
|
||||||
export * from "../../renderer/components/tabs"
|
|
||||||
export * from "../../renderer/components/badge"
|
|
||||||
export * from "../../renderer/components/layout/page-layout"
|
export * from "../../renderer/components/layout/page-layout"
|
||||||
|
export * from "../../renderer/components/layout/wizard-layout"
|
||||||
|
export * from "../../renderer/components/layout/tab-layout"
|
||||||
|
|
||||||
|
// form-controls
|
||||||
|
export * from "../../renderer/components/button"
|
||||||
|
export * from "../../renderer/components/checkbox"
|
||||||
|
export * from "../../renderer/components/radio"
|
||||||
|
export * from "../../renderer/components/select"
|
||||||
|
export * from "../../renderer/components/slider"
|
||||||
|
export * from "../../renderer/components/input/input"
|
||||||
|
|
||||||
|
// other components
|
||||||
|
export * from "../../renderer/components/icon"
|
||||||
|
export * from "../../renderer/components/tooltip"
|
||||||
|
export * from "../../renderer/components/tabs"
|
||||||
|
export * from "../../renderer/components/table"
|
||||||
|
export * from "../../renderer/components/badge"
|
||||||
export * from "../../renderer/components/drawer"
|
export * from "../../renderer/components/drawer"
|
||||||
|
export * from "../../renderer/components/dialog"
|
||||||
|
export * from "../../renderer/components/confirm-dialog";
|
||||||
|
export * from "../../renderer/components/line-progress"
|
||||||
|
export * from "../../renderer/components/menu"
|
||||||
|
export * from "../../renderer/components/notifications"
|
||||||
|
export * from "../../renderer/components/spinner"
|
||||||
|
export * from "../../renderer/components/stepper"
|
||||||
|
|
||||||
// kube helpers
|
// kube helpers
|
||||||
export { KubeObjectDetailsProps, KubeObjectMenuProps } from "../../renderer/components/kube-object"
|
export * from "../../renderer/components/kube-object"
|
||||||
export { KubeObjectMeta } from "../../renderer/components/kube-object/kube-object-meta"
|
export * from "../../renderer/components/+events/kube-event-details"
|
||||||
export { KubeObjectListLayout, KubeObjectListLayoutProps } from "../../renderer/components/kube-object/kube-object-list-layout";
|
|
||||||
export { KubeEventDetails } from "../../renderer/components/+events/kube-event-details"
|
|
||||||
|
|
||||||
// specific exports
|
// specific exports
|
||||||
export { ConfirmDialog } from "../../renderer/components/confirm-dialog";
|
export * from "../../renderer/components/status-brick";
|
||||||
export { MenuItem, SubMenu } from "../../renderer/components/menu";
|
|
||||||
export { StatusBrick } from "../../renderer/components/status-brick";
|
|
||||||
export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store";
|
export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store";
|
||||||
export { createPodLogsTab } from "../../renderer/components/dock/pod-logs.store";
|
export { createPodLogsTab } from "../../renderer/components/dock/pod-logs.store";
|
||||||
|
|||||||
@ -80,13 +80,14 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
@observable metadata: ClusterMetadata = {};
|
@observable metadata: ClusterMetadata = {};
|
||||||
@observable allowedNamespaces: string[] = [];
|
@observable allowedNamespaces: string[] = [];
|
||||||
@observable allowedResources: string[] = [];
|
@observable allowedResources: string[] = [];
|
||||||
|
@observable accessibleNamespaces: string[] = [];
|
||||||
|
|
||||||
@computed get available() {
|
@computed get available() {
|
||||||
return this.accessible && !this.disconnected;
|
return this.accessible && !this.disconnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
get version(): string {
|
get version(): string {
|
||||||
return String(this.metadata?.version) || ""
|
return String(this.metadata?.version) || ""
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(model: ClusterModel) {
|
constructor(model: ClusterModel) {
|
||||||
@ -149,7 +150,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async activate(force = false ) {
|
async activate(force = false) {
|
||||||
if (this.activated && !force) {
|
if (this.activated && !force) {
|
||||||
return this.pushState();
|
return this.pushState();
|
||||||
}
|
}
|
||||||
@ -340,7 +341,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
for (const w of warnings) {
|
for (const w of warnings) {
|
||||||
if (w.involvedObject.kind === 'Pod') {
|
if (w.involvedObject.kind === 'Pod') {
|
||||||
try {
|
try {
|
||||||
const pod = (await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace)).body;
|
const { body: pod } = await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace);
|
||||||
logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`)
|
logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`)
|
||||||
if (podHasIssues(pod)) {
|
if (podHasIssues(pod)) {
|
||||||
uniqEventSources.add(w.involvedObject.uid);
|
uniqEventSources.add(w.involvedObject.uid);
|
||||||
@ -351,11 +352,10 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
uniqEventSources.add(w.involvedObject.uid);
|
uniqEventSources.add(w.involvedObject.uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let nodeNotificationCount = 0;
|
|
||||||
const nodes = (await client.listNode()).body.items;
|
const nodes = (await client.listNode()).body.items;
|
||||||
nodes.map(n => {
|
const nodeNotificationCount = nodes
|
||||||
nodeNotificationCount = nodeNotificationCount + getNodeWarningConditions(n).length
|
.map(getNodeWarningConditions)
|
||||||
});
|
.reduce((sum, conditions) => sum + conditions.length, 0);
|
||||||
return uniqEventSources.size + nodeNotificationCount;
|
return uniqEventSources.size + nodeNotificationCount;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to fetch event count: " + JSON.stringify(error))
|
logger.error("Failed to fetch event count: " + JSON.stringify(error))
|
||||||
@ -371,7 +371,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
workspace: this.workspace,
|
workspace: this.workspace,
|
||||||
preferences: this.preferences,
|
preferences: this.preferences,
|
||||||
metadata: this.metadata,
|
metadata: this.metadata,
|
||||||
ownerRef: this.ownerRef
|
ownerRef: this.ownerRef,
|
||||||
|
accessibleNamespaces: this.accessibleNamespaces,
|
||||||
};
|
};
|
||||||
return toJS(model, {
|
return toJS(model, {
|
||||||
recurseEverything: true
|
recurseEverything: true
|
||||||
@ -426,6 +427,10 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async getAllowedNamespaces() {
|
protected async getAllowedNamespaces() {
|
||||||
|
if (this.accessibleNamespaces.length) {
|
||||||
|
return this.accessibleNamespaces
|
||||||
|
}
|
||||||
|
|
||||||
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api)
|
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api)
|
||||||
try {
|
try {
|
||||||
const namespaceList = await api.listNamespace()
|
const namespaceList = await api.listNamespace()
|
||||||
@ -442,7 +447,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName)
|
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName)
|
||||||
if (ctx.namespace) return [ctx.namespace]
|
if (ctx.namespace) return [ctx.namespace]
|
||||||
return []
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import http from "http"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { readFile } from "fs-extra"
|
import { readFile } from "fs-extra"
|
||||||
import { Cluster } from "./cluster"
|
import { Cluster } from "./cluster"
|
||||||
import { apiPrefix, appName, publicPath } from "../common/vars";
|
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
|
||||||
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
|
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
|
||||||
|
|
||||||
export interface RouterRequestOpts {
|
export interface RouterRequestOpts {
|
||||||
@ -94,22 +94,34 @@ export class Router {
|
|||||||
return mimeTypes[path.extname(filename).slice(1)] || "text/plain"
|
return mimeTypes[path.extname(filename).slice(1)] || "text/plain"
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleStaticFile(filePath: string, res: http.ServerResponse) {
|
async handleStaticFile(filePath: string, res: http.ServerResponse, req: http.IncomingMessage) {
|
||||||
const asset = path.join(__static, filePath);
|
const asset = path.join(__static, filePath);
|
||||||
try {
|
try {
|
||||||
|
const filename = path.basename(req.url);
|
||||||
|
// redirect requests to [appName].js, [appName].html /sockjs-node/ to webpack-dev-server (for hot-reload support)
|
||||||
|
const toWebpackDevServer = filename.includes(appName) || filename.includes('hot-update') || req.url.includes('sockjs-node');
|
||||||
|
if (isDevelopment && toWebpackDevServer) {
|
||||||
|
const redirectLocation = `http://localhost:${webpackDevServerPort}` + req.url;
|
||||||
|
res.statusCode = 307;
|
||||||
|
res.setHeader('Location', redirectLocation);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data = await readFile(asset);
|
const data = await readFile(asset);
|
||||||
res.setHeader("Content-Type", this.getMimeType(asset));
|
res.setHeader("Content-Type", this.getMimeType(asset));
|
||||||
res.write(data)
|
res.write(data);
|
||||||
res.end()
|
res.end();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.handleStaticFile(`${publicPath}/${appName}.html`, res);
|
this.handleStaticFile(`${publicPath}/${appName}.html`, res, req);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected addRoutes() {
|
protected addRoutes() {
|
||||||
// Static assets
|
// Static assets
|
||||||
this.router.add({ method: 'get', path: '/{path*}' }, ({ params, response }: LensApiRequest) => {
|
this.router.add(
|
||||||
this.handleStaticFile(params.path, response);
|
{ method: 'get', path: '/{path*}' },
|
||||||
|
({ params, response, path, raw: { req }}: LensApiRequest) => {
|
||||||
|
this.handleStaticFile(params.path, response, req);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute))
|
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute))
|
||||||
|
|||||||
@ -1,22 +1,13 @@
|
|||||||
import type { KubeObjectStore } from "../kube-object.store";
|
import type { KubeObjectStore } from "../kube-object.store";
|
||||||
import type { KubeObjectDetailsProps, KubeObjectListLayoutProps, KubeObjectMenuProps } from "../components/kube-object";
|
|
||||||
import type React from "react";
|
|
||||||
|
|
||||||
import { observable } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
import { autobind } from "../utils";
|
import { autobind } from "../utils";
|
||||||
import { KubeApi } from "./kube-api";
|
import { KubeApi } from "./kube-api";
|
||||||
|
|
||||||
export interface ApiComponents {
|
|
||||||
List?: React.ComponentType<KubeObjectListLayoutProps>;
|
|
||||||
Menu?: React.ComponentType<KubeObjectMenuProps>;
|
|
||||||
Details?: React.ComponentType<KubeObjectDetailsProps>;
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class ApiManager {
|
export class ApiManager {
|
||||||
private apis = observable.map<string, KubeApi>();
|
private apis = observable.map<string, KubeApi>();
|
||||||
private stores = observable.map<KubeApi, KubeObjectStore>();
|
private stores = observable.map<KubeApi, KubeObjectStore>();
|
||||||
private views = observable.map<KubeApi, ApiComponents>();
|
|
||||||
|
|
||||||
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) {
|
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) {
|
||||||
if (typeof pathOrCallback === "string") {
|
if (typeof pathOrCallback === "string") {
|
||||||
@ -46,8 +37,11 @@ export class ApiManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerStore(api: KubeApi, store: KubeObjectStore) {
|
@action
|
||||||
|
registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) {
|
||||||
|
apis.forEach(api => {
|
||||||
this.stores.set(api, store);
|
this.stores.set(api, store);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getStore(api: string | KubeApi): KubeObjectStore {
|
getStore(api: string | KubeApi): KubeObjectStore {
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import { kubeWatchApi } from "./kube-watch-api";
|
|||||||
import { apiManager } from "./api-manager";
|
import { apiManager } from "./api-manager";
|
||||||
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
|
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
|
||||||
import { apiKubePrefix, isDevelopment } from "../../common/vars";
|
import { apiKubePrefix, isDevelopment } from "../../common/vars";
|
||||||
import * as URL from "url"
|
|
||||||
|
|
||||||
export interface IKubeApiOptions<T extends KubeObject> {
|
export interface IKubeApiOptions<T extends KubeObject> {
|
||||||
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"
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { navigation } from "../../navigation";
|
|||||||
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
||||||
import { t, Trans } from "@lingui/macro";
|
import { t, Trans } from "@lingui/macro";
|
||||||
import { _i18n } from "../../i18n";
|
import { _i18n } from "../../i18n";
|
||||||
import { SearchInput } from "../input";
|
import { SearchInputUrl } from "../input";
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
name = "name",
|
name = "name",
|
||||||
@ -72,7 +72,7 @@ export class HelmCharts extends Component<Props> {
|
|||||||
(items: HelmChart[]) => items.filter(item => !item.deprecated)
|
(items: HelmChart[]) => items.filter(item => !item.deprecated)
|
||||||
]}
|
]}
|
||||||
customizeHeader={() => (
|
customizeHeader={() => (
|
||||||
<SearchInput placeholder={_i18n._(t`Search Helm Charts`)} />
|
<SearchInputUrl placeholder={_i18n._(t`Search Helm Charts`)} />
|
||||||
)}
|
)}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ className: "icon" },
|
{ className: "icon" },
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { Cluster } from "../../../../main/cluster";
|
||||||
|
import { SubTitle } from "../../layout/sub-title";
|
||||||
|
import { EditableList } from "../../editable-list";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
import { _i18n } from "../../../i18n";
|
||||||
|
import { Trans } from "@lingui/macro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
cluster: Cluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class ClusterAccessibleNamespaces extends React.Component<Props> {
|
||||||
|
@observable namespaces = new Set(this.props.cluster.accessibleNamespaces);
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SubTitle title="Accessible Namespaces" />
|
||||||
|
<p><Trans>This setting is useful for manually specifying which namespaces you have access to. This is useful when you don't have permissions to list namespaces.</Trans></p>
|
||||||
|
<EditableList
|
||||||
|
placeholder={_i18n._("Add new namespace...")}
|
||||||
|
add={(newNamespace) => {
|
||||||
|
this.namespaces.add(newNamespace);
|
||||||
|
this.props.cluster.accessibleNamespaces = Array.from(this.namespaces);
|
||||||
|
}}
|
||||||
|
items={Array.from(this.namespaces)}
|
||||||
|
remove={({ oldItem: oldNamesapce }) => {
|
||||||
|
this.namespaces.delete(oldNamesapce);
|
||||||
|
this.props.cluster.accessibleNamespaces = Array.from(this.namespaces);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import { ClusterIconSetting } from "./components/cluster-icon-setting";
|
|||||||
import { ClusterProxySetting } from "./components/cluster-proxy-setting";
|
import { ClusterProxySetting } from "./components/cluster-proxy-setting";
|
||||||
import { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
|
import { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
|
||||||
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
|
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
|
||||||
|
import { ClusterAccessibleNamespaces } from "./components/cluster-accessible-namespaces";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
@ -21,6 +22,7 @@ export class General extends React.Component<Props> {
|
|||||||
<ClusterProxySetting cluster={this.props.cluster} />
|
<ClusterProxySetting cluster={this.props.cluster} />
|
||||||
<ClusterPrometheusSetting cluster={this.props.cluster} />
|
<ClusterPrometheusSetting cluster={this.props.cluster} />
|
||||||
<ClusterHomeDirSetting cluster={this.props.cluster} />
|
<ClusterHomeDirSetting cluster={this.props.cluster} />
|
||||||
|
<ClusterAccessibleNamespaces cluster={this.props.cluster} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,4 +107,4 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const clusterStore = new ClusterStore();
|
export const clusterStore = new ClusterStore();
|
||||||
apiManager.registerStore(clusterApi, clusterStore);
|
apiManager.registerStore(clusterStore);
|
||||||
|
|||||||
@ -9,4 +9,4 @@ export class HPAStore extends KubeObjectStore<HorizontalPodAutoscaler> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const hpaStore = new HPAStore();
|
export const hpaStore = new HPAStore();
|
||||||
apiManager.registerStore(hpaApi, hpaStore);
|
apiManager.registerStore(hpaStore);
|
||||||
|
|||||||
@ -9,4 +9,4 @@ export class ConfigMapsStore extends KubeObjectStore<ConfigMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const configMapsStore = new ConfigMapsStore();
|
export const configMapsStore = new ConfigMapsStore();
|
||||||
apiManager.registerStore(configMapApi, configMapsStore);
|
apiManager.registerStore(configMapsStore);
|
||||||
|
|||||||
@ -9,4 +9,4 @@ export class PodDisruptionBudgetsStore extends KubeObjectStore<PodDisruptionBudg
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const podDisruptionBudgetsStore = new PodDisruptionBudgetsStore();
|
export const podDisruptionBudgetsStore = new PodDisruptionBudgetsStore();
|
||||||
apiManager.registerStore(pdbApi, podDisruptionBudgetsStore);
|
apiManager.registerStore(podDisruptionBudgetsStore);
|
||||||
|
|||||||
@ -9,4 +9,4 @@ export class ResourceQuotasStore extends KubeObjectStore<ResourceQuota> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const resourceQuotaStore = new ResourceQuotasStore();
|
export const resourceQuotaStore = new ResourceQuotasStore();
|
||||||
apiManager.registerStore(resourceQuotaApi, resourceQuotaStore);
|
apiManager.registerStore(resourceQuotaStore);
|
||||||
|
|||||||
@ -9,4 +9,4 @@ export class SecretsStore extends KubeObjectStore<Secret> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const secretsStore = new SecretsStore();
|
export const secretsStore = new SecretsStore();
|
||||||
apiManager.registerStore(secretsApi, secretsStore);
|
apiManager.registerStore(secretsStore);
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { KubeObject } from "../../api/kube-object";
|
|||||||
import { ICRDRouteParams } from "./crd.route";
|
import { ICRDRouteParams } from "./crd.route";
|
||||||
import { autorun, computed } from "mobx";
|
import { autorun, computed } from "mobx";
|
||||||
import { crdStore } from "./crd.store";
|
import { crdStore } from "./crd.store";
|
||||||
import { SortingCallback } from "../table";
|
import { TableSortCallback } from "../table";
|
||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<ICRDRouteParams> {
|
interface Props extends RouteComponentProps<ICRDRouteParams> {
|
||||||
@ -50,7 +50,7 @@ export class CrdResources extends React.Component<Props> {
|
|||||||
if (!crd) return null;
|
if (!crd) return null;
|
||||||
const isNamespaced = crd.isNamespaced();
|
const isNamespaced = crd.isNamespaced();
|
||||||
const extraColumns = crd.getPrinterColumns(false); // Cols with priority bigger than 0 are shown in details
|
const extraColumns = crd.getPrinterColumns(false); // Cols with priority bigger than 0 are shown in details
|
||||||
const sortingCallbacks: { [sortBy: string]: SortingCallback } = {
|
const sortingCallbacks: { [sortBy: string]: TableSortCallback } = {
|
||||||
[sortBy.name]: (item: KubeObject) => item.getName(),
|
[sortBy.name]: (item: KubeObject) => item.getName(),
|
||||||
[sortBy.namespace]: (item: KubeObject) => item.getNs(),
|
[sortBy.namespace]: (item: KubeObject) => item.getNs(),
|
||||||
[sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp,
|
[sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp,
|
||||||
|
|||||||
@ -14,7 +14,7 @@ function initStore(crd: CustomResourceDefinition) {
|
|||||||
const api = apiManager.getApi(apiBase) || new KubeApi({ apiBase, kind, isNamespaced });
|
const api = apiManager.getApi(apiBase) || new KubeApi({ apiBase, kind, isNamespaced });
|
||||||
|
|
||||||
if (!apiManager.getStore(api)) {
|
if (!apiManager.getStore(api)) {
|
||||||
apiManager.registerStore(api, new CRDResourceStore(api));
|
apiManager.registerStore(new CRDResourceStore(api));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,4 +64,4 @@ export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
|
|||||||
|
|
||||||
export const crdStore = new CRDStore();
|
export const crdStore = new CRDStore();
|
||||||
|
|
||||||
apiManager.registerStore(crdApi, crdStore);
|
apiManager.registerStore(crdStore);
|
||||||
|
|||||||
@ -49,4 +49,4 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const eventStore = new EventStore();
|
export const eventStore = new EventStore();
|
||||||
apiManager.registerStore(eventApi, eventStore);
|
apiManager.registerStore(eventStore);
|
||||||
|
|||||||
@ -6,15 +6,14 @@ import { Trans } from "@lingui/macro";
|
|||||||
import { KubeObject } from "../../api/kube-object";
|
import { KubeObject } from "../../api/kube-object";
|
||||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Icon } from "../icon";
|
|
||||||
import { eventStore } from "./event.store";
|
import { eventStore } from "./event.store";
|
||||||
|
|
||||||
interface Props {
|
export interface KubeEventDetailsProps {
|
||||||
object: KubeObject;
|
object: KubeObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class KubeEventDetails extends React.Component<Props> {
|
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
eventStore.loadAll();
|
eventStore.loadAll();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,4 +95,4 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const namespaceStore = new NamespaceStore();
|
export const namespaceStore = new NamespaceStore();
|
||||||
apiManager.registerStore(namespacesApi, namespaceStore);
|
apiManager.registerStore(namespaceStore);
|
||||||
|
|||||||
@ -9,4 +9,4 @@ export class EndpointStore extends KubeObjectStore<Endpoint> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const endpointStore = new EndpointStore();
|
export const endpointStore = new EndpointStore();
|
||||||
apiManager.registerStore(endpointApi, endpointStore);
|
apiManager.registerStore(endpointStore);
|
||||||
|
|||||||
@ -19,4 +19,4 @@ export class IngressStore extends KubeObjectStore<Ingress> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ingressStore = new IngressStore();
|
export const ingressStore = new IngressStore();
|
||||||
apiManager.registerStore(ingressApi, ingressStore);
|
apiManager.registerStore(ingressStore);
|
||||||
|
|||||||
@ -9,4 +9,4 @@ export class NetworkPolicyStore extends KubeObjectStore<NetworkPolicy> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const networkPolicyStore = new NetworkPolicyStore();
|
export const networkPolicyStore = new NetworkPolicyStore();
|
||||||
apiManager.registerStore(networkPolicyApi, networkPolicyStore);
|
apiManager.registerStore(networkPolicyStore);
|
||||||
|
|||||||
@ -9,4 +9,4 @@ export class ServiceStore extends KubeObjectStore<Service> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const serviceStore = new ServiceStore();
|
export const serviceStore = new ServiceStore();
|
||||||
apiManager.registerStore(serviceApi, serviceStore);
|
apiManager.registerStore(serviceStore);
|
||||||
|
|||||||
@ -69,4 +69,4 @@ export class NodesStore extends KubeObjectStore<Node> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const nodesStore = new NodesStore()
|
export const nodesStore = new NodesStore()
|
||||||
apiManager.registerStore(nodesApi, nodesStore);
|
apiManager.registerStore(nodesStore);
|
||||||
|
|||||||
@ -9,4 +9,4 @@ export class PodSecurityPoliciesStore extends KubeObjectStore<PodSecurityPolicy>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const podSecurityPoliciesStore = new PodSecurityPoliciesStore()
|
export const podSecurityPoliciesStore = new PodSecurityPoliciesStore()
|
||||||
apiManager.registerStore(pspApi, podSecurityPoliciesStore);
|
apiManager.registerStore(podSecurityPoliciesStore);
|
||||||
|
|||||||
@ -9,4 +9,4 @@ export class StorageClassStore extends KubeObjectStore<StorageClass> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const storageClassStore = new StorageClassStore();
|
export const storageClassStore = new StorageClassStore();
|
||||||
apiManager.registerStore(storageClassApi, storageClassStore);
|
apiManager.registerStore(storageClassStore);
|
||||||
|
|||||||
@ -20,4 +20,4 @@ export class VolumeClaimStore extends KubeObjectStore<PersistentVolumeClaim> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const volumeClaimStore = new VolumeClaimStore();
|
export const volumeClaimStore = new VolumeClaimStore();
|
||||||
apiManager.registerStore(pvcApi, volumeClaimStore);
|
apiManager.registerStore(volumeClaimStore);
|
||||||
|
|||||||
@ -9,4 +9,4 @@ export class PersistentVolumesStore extends KubeObjectStore<PersistentVolume> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const volumesStore = new PersistentVolumesStore();
|
export const volumesStore = new PersistentVolumesStore();
|
||||||
apiManager.registerStore(persistentVolumeApi, volumesStore);
|
apiManager.registerStore(volumesStore);
|
||||||
|
|||||||
@ -30,8 +30,7 @@ export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
|||||||
return Promise.all(
|
return Promise.all(
|
||||||
namespaces.map(namespace => roleBindingApi.list({ namespace }))
|
namespaces.map(namespace => roleBindingApi.list({ namespace }))
|
||||||
).then(items => items.flat())
|
).then(items => items.flat())
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return Promise.all([clusterRoleBindingApi.list(), roleBindingApi.list()])
|
return Promise.all([clusterRoleBindingApi.list(), roleBindingApi.list()])
|
||||||
.then(items => items.flat())
|
.then(items => items.flat())
|
||||||
}
|
}
|
||||||
@ -40,8 +39,7 @@ export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
|||||||
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<RoleBinding>) {
|
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<RoleBinding>) {
|
||||||
if (params.namespace) {
|
if (params.namespace) {
|
||||||
return roleBindingApi.create(params, data)
|
return roleBindingApi.create(params, data)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return clusterRoleBindingApi.create(params, data)
|
return clusterRoleBindingApi.create(params, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -58,8 +56,7 @@ export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
|||||||
newSubjects = uniqBy(currentSubjects.concat(addSubjects), ({ kind, name, namespace }) => {
|
newSubjects = uniqBy(currentSubjects.concat(addSubjects), ({ kind, name, namespace }) => {
|
||||||
return [kind, name, namespace].join("-");
|
return [kind, name, namespace].join("-");
|
||||||
})
|
})
|
||||||
}
|
} else if (removeSubjects) {
|
||||||
else if (removeSubjects) {
|
|
||||||
newSubjects = difference(currentSubjects, removeSubjects);
|
newSubjects = difference(currentSubjects, removeSubjects);
|
||||||
}
|
}
|
||||||
return this.update(roleBinding, {
|
return this.update(roleBinding, {
|
||||||
@ -71,5 +68,7 @@ export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
|||||||
|
|
||||||
export const roleBindingsStore = new RoleBindingsStore();
|
export const roleBindingsStore = new RoleBindingsStore();
|
||||||
|
|
||||||
apiManager.registerStore(roleBindingApi, roleBindingsStore);
|
apiManager.registerStore(roleBindingsStore, [
|
||||||
apiManager.registerStore(clusterRoleBindingApi, roleBindingsStore);
|
roleBindingApi,
|
||||||
|
clusterRoleBindingApi,
|
||||||
|
]);
|
||||||
|
|||||||
@ -28,8 +28,7 @@ export class RolesStore extends KubeObjectStore<Role> {
|
|||||||
return Promise.all(
|
return Promise.all(
|
||||||
namespaces.map(namespace => roleApi.list({ namespace }))
|
namespaces.map(namespace => roleApi.list({ namespace }))
|
||||||
).then(items => items.flat())
|
).then(items => items.flat())
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return Promise.all([clusterRoleApi.list(), roleApi.list()])
|
return Promise.all([clusterRoleApi.list(), roleApi.list()])
|
||||||
.then(items => items.flat())
|
.then(items => items.flat())
|
||||||
}
|
}
|
||||||
@ -38,8 +37,7 @@ export class RolesStore extends KubeObjectStore<Role> {
|
|||||||
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<Role>) {
|
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<Role>) {
|
||||||
if (params.namespace) {
|
if (params.namespace) {
|
||||||
return roleApi.create(params, data)
|
return roleApi.create(params, data)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return clusterRoleApi.create(params, data)
|
return clusterRoleApi.create(params, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,5 +45,7 @@ export class RolesStore extends KubeObjectStore<Role> {
|
|||||||
|
|
||||||
export const rolesStore = new RolesStore();
|
export const rolesStore = new RolesStore();
|
||||||
|
|
||||||
apiManager.registerStore(roleApi, rolesStore);
|
apiManager.registerStore(rolesStore, [
|
||||||
apiManager.registerStore(clusterRoleApi, rolesStore);
|
roleApi,
|
||||||
|
clusterRoleApi,
|
||||||
|
]);
|
||||||
@ -14,4 +14,4 @@ export class ServiceAccountsStore extends KubeObjectStore<ServiceAccount> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const serviceAccountsStore = new ServiceAccountsStore();
|
export const serviceAccountsStore = new ServiceAccountsStore();
|
||||||
apiManager.registerStore(serviceAccountsApi, serviceAccountsStore);
|
apiManager.registerStore(serviceAccountsStore);
|
||||||
|
|||||||
@ -30,4 +30,4 @@ export class CronJobStore extends KubeObjectStore<CronJob> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const cronJobStore = new CronJobStore();
|
export const cronJobStore = new CronJobStore();
|
||||||
apiManager.registerStore(cronJobApi, cronJobStore);
|
apiManager.registerStore(cronJobStore);
|
||||||
|
|||||||
@ -43,4 +43,4 @@ export class DaemonSetStore extends KubeObjectStore<DaemonSet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const daemonSetStore = new DaemonSetStore();
|
export const daemonSetStore = new DaemonSetStore();
|
||||||
apiManager.registerStore(daemonSetApi, daemonSetStore);
|
apiManager.registerStore(daemonSetStore);
|
||||||
|
|||||||
@ -50,4 +50,4 @@ export class DeploymentStore extends KubeObjectStore<Deployment> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const deploymentStore = new DeploymentStore();
|
export const deploymentStore = new DeploymentStore();
|
||||||
apiManager.registerStore(deploymentApi, deploymentStore);
|
apiManager.registerStore(deploymentStore);
|
||||||
|
|||||||
@ -42,4 +42,4 @@ export class JobStore extends KubeObjectStore<Job> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const jobStore = new JobStore();
|
export const jobStore = new JobStore();
|
||||||
apiManager.registerStore(jobApi, jobStore);
|
apiManager.registerStore(jobStore);
|
||||||
|
|||||||
@ -76,4 +76,4 @@ export class PodsStore extends KubeObjectStore<Pod> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const podsStore = new PodsStore();
|
export const podsStore = new PodsStore();
|
||||||
apiManager.registerStore(podsApi, podsStore);
|
apiManager.registerStore(podsStore);
|
||||||
|
|||||||
@ -31,4 +31,4 @@ export class ReplicaSetStore extends KubeObjectStore<ReplicaSet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const replicaSetStore = new ReplicaSetStore();
|
export const replicaSetStore = new ReplicaSetStore();
|
||||||
apiManager.registerStore(replicaSetApi, replicaSetStore);
|
apiManager.registerStore(replicaSetStore);
|
||||||
|
|||||||
@ -42,4 +42,4 @@ export class StatefulSetStore extends KubeObjectStore<StatefulSet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const statefulSetStore = new StatefulSetStore();
|
export const statefulSetStore = new StatefulSetStore();
|
||||||
apiManager.registerStore(statefulSetApi, statefulSetStore);
|
apiManager.registerStore(statefulSetStore);
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { Icon } from "../icon";
|
|||||||
import { Input } from "../input";
|
import { Input } from "../input";
|
||||||
import { cssNames, prevDefault } from "../../utils";
|
import { cssNames, prevDefault } from "../../utils";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { isRequired, Validator } from "../input/input_validators";
|
import { isRequired, InputValidator } from "../input/input_validators";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Workspaces extends React.Component {
|
export class Workspaces extends React.Component {
|
||||||
@ -122,7 +122,7 @@ export class Workspaces extends React.Component {
|
|||||||
editing: isEditing,
|
editing: isEditing,
|
||||||
default: isDefault,
|
default: isDefault,
|
||||||
});
|
});
|
||||||
const existenceValidator: Validator = {
|
const existenceValidator: InputValidator = {
|
||||||
message: () => `Workspace '${name}' already exists`,
|
message: () => `Workspace '${name}' already exists`,
|
||||||
validate: value => !workspaceStore.getByName(value.trim())
|
validate: value => !workspaceStore.getByName(value.trim())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,13 +4,13 @@ import React from "react";
|
|||||||
import { cssNames } from "../../utils/cssNames";
|
import { cssNames } from "../../utils/cssNames";
|
||||||
import { TooltipDecoratorProps, withTooltip } from "../tooltip";
|
import { TooltipDecoratorProps, withTooltip } from "../tooltip";
|
||||||
|
|
||||||
interface Props extends React.HTMLAttributes<any>, TooltipDecoratorProps {
|
export interface BadgeProps extends React.HTMLAttributes<any>, TooltipDecoratorProps {
|
||||||
small?: boolean;
|
small?: boolean;
|
||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@withTooltip
|
@withTooltip
|
||||||
export class Badge extends React.Component<Props> {
|
export class Badge extends React.Component<BadgeProps> {
|
||||||
render() {
|
render() {
|
||||||
const { className, label, small, children, ...elemProps } = this.props;
|
const { className, label, small, children, ...elemProps } = this.props;
|
||||||
return <>
|
return <>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import './checkbox.scss'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { autobind, cssNames } from "../../utils";
|
import { autobind, cssNames } from "../../utils";
|
||||||
|
|
||||||
interface Props<T = boolean> {
|
export interface CheckboxProps<T = boolean> {
|
||||||
theme?: "dark" | "light";
|
theme?: "dark" | "light";
|
||||||
className?: string;
|
className?: string;
|
||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
@ -12,7 +12,7 @@ interface Props<T = boolean> {
|
|||||||
onChange?(value: T, evt: React.ChangeEvent<HTMLInputElement>): void;
|
onChange?(value: T, evt: React.ChangeEvent<HTMLInputElement>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Checkbox extends React.PureComponent<Props> {
|
export class Checkbox extends React.PureComponent<CheckboxProps> {
|
||||||
private input: HTMLInputElement;
|
private input: HTMLInputElement;
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
|
|||||||
@ -9,7 +9,10 @@ import { Button, ButtonProps } from "../button";
|
|||||||
import { Dialog, DialogProps } from "../dialog";
|
import { Dialog, DialogProps } from "../dialog";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
|
|
||||||
export interface IConfirmDialogParams {
|
export interface ConfirmDialogProps extends Partial<DialogProps> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfirmDialogParams {
|
||||||
ok?: () => void;
|
ok?: () => void;
|
||||||
labelOk?: ReactNode;
|
labelOk?: ReactNode;
|
||||||
labelCancel?: ReactNode;
|
labelCancel?: ReactNode;
|
||||||
@ -19,17 +22,14 @@ export interface IConfirmDialogParams {
|
|||||||
cancelButtonProps?: Partial<ButtonProps>
|
cancelButtonProps?: Partial<ButtonProps>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends Partial<DialogProps> {
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ConfirmDialog extends React.Component<Props> {
|
export class ConfirmDialog extends React.Component<ConfirmDialogProps> {
|
||||||
@observable static isOpen = false;
|
@observable static isOpen = false;
|
||||||
@observable.ref static params: IConfirmDialogParams;
|
@observable.ref static params: ConfirmDialogParams;
|
||||||
|
|
||||||
@observable isSaving = false;
|
@observable isSaving = false;
|
||||||
|
|
||||||
static open(params: IConfirmDialogParams) {
|
static open(params: ConfirmDialogParams) {
|
||||||
ConfirmDialog.isOpen = true;
|
ConfirmDialog.isOpen = true;
|
||||||
ConfirmDialog.params = params;
|
ConfirmDialog.params = params;
|
||||||
}
|
}
|
||||||
@ -38,14 +38,14 @@ export class ConfirmDialog extends React.Component<Props> {
|
|||||||
ConfirmDialog.isOpen = false;
|
ConfirmDialog.isOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public defaultParams: IConfirmDialogParams = {
|
public defaultParams: ConfirmDialogParams = {
|
||||||
ok: noop,
|
ok: noop,
|
||||||
labelOk: <Trans>Ok</Trans>,
|
labelOk: <Trans>Ok</Trans>,
|
||||||
labelCancel: <Trans>Cancel</Trans>,
|
labelCancel: <Trans>Cancel</Trans>,
|
||||||
icon: <Icon big material="warning"/>,
|
icon: <Icon big material="warning"/>,
|
||||||
};
|
};
|
||||||
|
|
||||||
get params(): IConfirmDialogParams {
|
get params(): ConfirmDialogParams {
|
||||||
return Object.assign({}, this.defaultParams, ConfirmDialog.params);
|
return Object.assign({}, this.defaultParams, ConfirmDialog.params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,22 +8,26 @@ import { Icon } from "../icon";
|
|||||||
import { _i18n } from "../../i18n";
|
import { _i18n } from "../../i18n";
|
||||||
import { cssNames, downloadFile } from "../../utils";
|
import { cssNames, downloadFile } from "../../utils";
|
||||||
import { Pod } from "../../api/endpoints";
|
import { Pod } from "../../api/endpoints";
|
||||||
|
import { PodLogSearch, PodLogSearchProps } from "./pod-log-search";
|
||||||
|
|
||||||
interface Props {
|
interface Props extends PodLogSearchProps {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
tabId: string
|
tabId: string
|
||||||
tabData: IPodLogsData
|
tabData: IPodLogsData
|
||||||
logs: string[][]
|
logs: string[]
|
||||||
save: (data: Partial<IPodLogsData>) => void
|
save: (data: Partial<IPodLogsData>) => void
|
||||||
reload: () => void
|
reload: () => void
|
||||||
|
onSearch: (query: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PodLogControls = observer((props: Props) => {
|
export const PodLogControls = observer((props: Props) => {
|
||||||
if (!props.ready) return null;
|
if (!props.ready) return null;
|
||||||
const { tabData, tabId, save, reload, logs } = props;
|
const { tabData, save, reload, tabId, logs } = props;
|
||||||
const { selectedContainer, showTimestamps, previous } = tabData;
|
const { selectedContainer, showTimestamps, previous } = tabData;
|
||||||
const since = podLogsStore.getTimestamps(podLogsStore.logs.get(tabId)[0]);
|
const rawLogs = podLogsStore.logs.get(tabId);
|
||||||
|
const since = rawLogs.length ? podLogsStore.getTimestamps(rawLogs[0]) : null;
|
||||||
const pod = new Pod(tabData.pod);
|
const pod = new Pod(tabData.pod);
|
||||||
|
|
||||||
const toggleTimestamps = () => {
|
const toggleTimestamps = () => {
|
||||||
save({ showTimestamps: !showTimestamps });
|
save({ showTimestamps: !showTimestamps });
|
||||||
}
|
}
|
||||||
@ -35,8 +39,7 @@ export const PodLogControls = observer((props: Props) => {
|
|||||||
|
|
||||||
const downloadLogs = () => {
|
const downloadLogs = () => {
|
||||||
const fileName = selectedContainer ? selectedContainer.name : pod.getName();
|
const fileName = selectedContainer ? selectedContainer.name : pod.getName();
|
||||||
const [oldLogs, newLogs] = logs;
|
downloadFile(fileName + ".log", logs.join("\n"), "text/plain");
|
||||||
downloadFile(fileName + ".log", [...oldLogs, ...newLogs].join("\n"), "text/plain");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onContainerChange = (option: SelectOption) => {
|
const onContainerChange = (option: SelectOption) => {
|
||||||
@ -92,7 +95,7 @@ export const PodLogControls = observer((props: Props) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gaps">
|
<div className="flex box grow gaps align-center">
|
||||||
<Icon
|
<Icon
|
||||||
material="av_timer"
|
material="av_timer"
|
||||||
onClick={toggleTimestamps}
|
onClick={toggleTimestamps}
|
||||||
@ -109,7 +112,9 @@ export const PodLogControls = observer((props: Props) => {
|
|||||||
material="get_app"
|
material="get_app"
|
||||||
onClick={downloadLogs}
|
onClick={downloadLogs}
|
||||||
tooltip={_i18n._(t`Save`)}
|
tooltip={_i18n._(t`Save`)}
|
||||||
|
className="download-icon"
|
||||||
/>
|
/>
|
||||||
|
<PodLogSearch {...props} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
10
src/renderer/components/dock/pod-log-search.scss
Normal file
10
src/renderer/components/dock/pod-log-search.scss
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.PodLogsSearch {
|
||||||
|
.SearchInput {
|
||||||
|
min-width: 150px;
|
||||||
|
width: 150px;
|
||||||
|
|
||||||
|
.find-count {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/renderer/components/dock/pod-log-search.tsx
Normal file
87
src/renderer/components/dock/pod-log-search.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import "./pod-log-search.scss";
|
||||||
|
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { SearchInput } from "../input";
|
||||||
|
import { searchStore } from "../../../common/search-store";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { _i18n } from "../../i18n";
|
||||||
|
import { t } from "@lingui/macro";
|
||||||
|
|
||||||
|
export interface PodLogSearchProps {
|
||||||
|
onSearch: (query: string) => void
|
||||||
|
toPrevOverlay: () => void
|
||||||
|
toNextOverlay: () => void
|
||||||
|
logs: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PodLogSearch = observer((props: PodLogSearchProps) => {
|
||||||
|
const { logs, onSearch, toPrevOverlay, toNextOverlay } = props;
|
||||||
|
const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = searchStore;
|
||||||
|
const jumpDisabled = !searchQuery || !occurrences.length;
|
||||||
|
const findCounts = (
|
||||||
|
<div className="find-count">
|
||||||
|
{activeFind}/{totalFinds}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const setSearch = (query: string) => {
|
||||||
|
searchStore.onSearch(logs, query);
|
||||||
|
onSearch(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPrevOverlay = () => {
|
||||||
|
setPrevOverlayActive();
|
||||||
|
toPrevOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onNextOverlay = () => {
|
||||||
|
setNextOverlayActive();
|
||||||
|
toNextOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClear = () => {
|
||||||
|
setSearch("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = (evt: React.KeyboardEvent<any>) => {
|
||||||
|
if (evt.key === "Enter") {
|
||||||
|
onNextOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Refresh search when logs changed
|
||||||
|
searchStore.onSearch(logs);
|
||||||
|
}, [logs]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="PodLogsSearch flex box grow justify-flex-end gaps align-center">
|
||||||
|
<SearchInput
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={setSearch}
|
||||||
|
closeIcon={false}
|
||||||
|
contentRight={totalFinds > 0 && findCounts}
|
||||||
|
onClear={onClear}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
material="keyboard_arrow_up"
|
||||||
|
tooltip={_i18n._(t`Previous`)}
|
||||||
|
onClick={onPrevOverlay}
|
||||||
|
disabled={jumpDisabled}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
material="keyboard_arrow_down"
|
||||||
|
tooltip={_i18n._(t`Next`)}
|
||||||
|
onClick={onNextOverlay}
|
||||||
|
disabled={jumpDisabled}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
material="close"
|
||||||
|
tooltip={_i18n._(t`Clear`)}
|
||||||
|
onClick={onClear}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -6,19 +6,46 @@
|
|||||||
// `overflow: overlay` don't allow scroll to the last line
|
// `overflow: overlay` don't allow scroll to the last line
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
color: $textColorAccent;
|
color: $textColorAccent;
|
||||||
background: $logsBackground;
|
background: $logsBackground;
|
||||||
line-height: var(--log-line-height);
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.find-overlay {
|
||||||
|
position: absolute;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: $padding * 2;
|
background-color: #8cc474;
|
||||||
|
margin-top: 4px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VirtualList {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.list {
|
||||||
|
.LogRow {
|
||||||
|
padding: 2px 16px;
|
||||||
|
height: 18px; // Must be equal to lineHeight variable in pod-logs.scss
|
||||||
font-family: $font-monospace;
|
font-family: $font-monospace;
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
flex-grow: 1;
|
-webkit-font-smoothing: auto; // Better readability on non-retina screens
|
||||||
|
|
||||||
> div {
|
&:hover {
|
||||||
// Provides font better readability on large screens
|
background: $logRowHoverBackground;
|
||||||
-webkit-font-smoothing: subpixel-antialiased;
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: #8cc474b8;
|
||||||
|
-webkit-font-smoothing: auto;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +74,8 @@
|
|||||||
padding: $unit / 2 $unit * 1.5;
|
padding: $unit / 2 $unit * 1.5;
|
||||||
border-radius: $unit * 2;
|
border-radius: $unit * 2;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s;
|
z-index: 2;
|
||||||
|
top: 20px;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@ -57,4 +85,20 @@
|
|||||||
--size: $unit * 2;
|
--size: $unit * 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.PodLogControls {
|
||||||
|
.Select {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs .VirtualList .list {
|
||||||
|
overflow-x: scroll!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.noscroll {
|
||||||
|
.logs .VirtualList .list {
|
||||||
|
overflow-x: hidden!important; // fixing scroll to bottom issues in PodLogs
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -5,7 +5,7 @@ import { DockTabStore } from "./dock-tab.store";
|
|||||||
import { dockStore, IDockTab, TabKind } from "./dock.store";
|
import { dockStore, IDockTab, TabKind } from "./dock.store";
|
||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
import { _i18n } from "../../i18n";
|
import { _i18n } from "../../i18n";
|
||||||
import { isDevelopment } from "../../../common/vars";
|
import { searchStore } from "../../../common/search-store";
|
||||||
|
|
||||||
export interface IPodLogsData {
|
export interface IPodLogsData {
|
||||||
pod: Pod;
|
pod: Pod;
|
||||||
@ -20,7 +20,7 @@ type TabId = string;
|
|||||||
type PodLogLine = string;
|
type PodLogLine = string;
|
||||||
|
|
||||||
// Number for log lines to load
|
// Number for log lines to load
|
||||||
export const logRange = isDevelopment ? 100 : 1000;
|
export const logRange = 500;
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class PodLogsStore extends DockTabStore<IPodLogsData> {
|
export class PodLogsStore extends DockTabStore<IPodLogsData> {
|
||||||
@ -49,6 +49,11 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
|
|||||||
reaction(() => this.logs.get(dockStore.selectedTabId), () => {
|
reaction(() => this.logs.get(dockStore.selectedTabId), () => {
|
||||||
this.setNewLogSince(dockStore.selectedTabId);
|
this.setNewLogSince(dockStore.selectedTabId);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
reaction(() => dockStore.selectedTabId, () => {
|
||||||
|
// Clear search query on tab change
|
||||||
|
searchStore.reset();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -82,6 +87,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
|
|||||||
* @param tabId
|
* @param tabId
|
||||||
*/
|
*/
|
||||||
loadMore = async (tabId: TabId) => {
|
loadMore = async (tabId: TabId) => {
|
||||||
|
if (!this.logs.get(tabId).length) return;
|
||||||
const oldLogs = this.logs.get(tabId);
|
const oldLogs = this.logs.get(tabId);
|
||||||
const logs = await this.loadLogs(tabId, {
|
const logs = await this.loadLogs(tabId, {
|
||||||
sinceTime: this.getLastSinceTime(tabId)
|
sinceTime: this.getLastSinceTime(tabId)
|
||||||
@ -120,7 +126,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
|
|||||||
* @param tabId
|
* @param tabId
|
||||||
*/
|
*/
|
||||||
setNewLogSince(tabId: TabId) {
|
setNewLogSince(tabId: TabId) {
|
||||||
if (!this.logs.has(tabId) || this.newLogSince.has(tabId)) return;
|
if (!this.logs.has(tabId) || !this.logs.get(tabId).length || this.newLogSince.has(tabId)) return;
|
||||||
const timestamp = this.getLastSinceTime(tabId);
|
const timestamp = this.getLastSinceTime(tabId);
|
||||||
this.newLogSince.set(tabId, timestamp.split(".")[0]); // Removing milliseconds from string
|
this.newLogSince.set(tabId, timestamp.split(".")[0]); // Removing milliseconds from string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import "./pod-logs.scss";
|
import "./pod-logs.scss";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import AnsiUp from "ansi_up";
|
import { Trans } from "@lingui/macro";
|
||||||
import DOMPurify from "dompurify";
|
import { action, computed, observable, reaction } from "mobx";
|
||||||
import { t, Trans } from "@lingui/macro";
|
|
||||||
import { computed, observable, reaction } from "mobx";
|
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { _i18n } from "../../i18n";
|
import { _i18n } from "../../i18n";
|
||||||
import { autobind, cssNames } from "../../utils";
|
import { autobind, cssNames } from "../../utils";
|
||||||
@ -14,30 +12,33 @@ import { InfoPanel } from "./info-panel";
|
|||||||
import { IPodLogsData, logRange, podLogsStore } from "./pod-logs.store";
|
import { IPodLogsData, logRange, podLogsStore } from "./pod-logs.store";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { PodLogControls } from "./pod-log-controls";
|
import { PodLogControls } from "./pod-log-controls";
|
||||||
|
import { VirtualList } from "../virtual-list";
|
||||||
|
import { searchStore } from "../../../common/search-store";
|
||||||
|
import { ListOnScrollProps } from "react-window";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
tab: IDockTab
|
tab: IDockTab
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lineHeight = 18; // Height of a log line. Should correlate with styles in pod-logs.scss
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class PodLogs extends React.Component<Props> {
|
export class PodLogs extends React.Component<Props> {
|
||||||
@observable ready = false;
|
@observable ready = false;
|
||||||
@observable preloading = false; // Indicator for setting Spinner (loader) at the top of the logs
|
@observable preloading = false; // Indicator for setting Spinner (loader) at the top of the logs
|
||||||
@observable showJumpToBottom = false;
|
@observable showJumpToBottom = false;
|
||||||
|
@observable hideHorizontalScroll = true; // Hiding scrollbar allows to scroll logs down to last element
|
||||||
|
|
||||||
private logsElement: HTMLDivElement;
|
private logsElement = React.createRef<HTMLDivElement>(); // A reference for outer container in VirtualList
|
||||||
|
private virtualListRef = React.createRef<VirtualList>(); // A reference for VirtualList component
|
||||||
private lastLineIsShown = true; // used for proper auto-scroll content after refresh
|
private lastLineIsShown = true; // used for proper auto-scroll content after refresh
|
||||||
private colorConverter = new AnsiUp();
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => this.props.tab.id, async () => {
|
reaction(() => this.props.tab.id, async () => {
|
||||||
if (podLogsStore.logs.has(this.tabId)) {
|
|
||||||
this.ready = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.load();
|
await this.load();
|
||||||
|
this.scrollToBottom();
|
||||||
}, { fireImmediately: true }),
|
}, { fireImmediately: true }),
|
||||||
|
|
||||||
// Check if need to show JumpToBottom if new log amount is less than previous one
|
// Check if need to show JumpToBottom if new log amount is less than previous one
|
||||||
@ -53,8 +54,8 @@ export class PodLogs extends React.Component<Props> {
|
|||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
// scroll logs only when it's already in the end,
|
// scroll logs only when it's already in the end,
|
||||||
// otherwise it can interrupt reading by jumping after loading new logs update
|
// otherwise it can interrupt reading by jumping after loading new logs update
|
||||||
if (this.logsElement && this.lastLineIsShown) {
|
if (this.logsElement.current && this.lastLineIsShown) {
|
||||||
this.logsElement.scrollTop = this.logsElement.scrollHeight;
|
this.logsElement.current.scrollTop = this.logsElement.current.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,60 +87,130 @@ export class PodLogs extends React.Component<Props> {
|
|||||||
/**
|
/**
|
||||||
* Function loads more logs (usually after user scrolls to top) and sets proper
|
* Function loads more logs (usually after user scrolls to top) and sets proper
|
||||||
* scrolling position
|
* scrolling position
|
||||||
* @param scrollHeight previous scrollHeight position before adding new lines
|
|
||||||
*/
|
*/
|
||||||
loadMore = async (scrollHeight: number) => {
|
loadMore = async () => {
|
||||||
if (podLogsStore.lines < logRange) return;
|
const lines = podLogsStore.lines;
|
||||||
|
if (lines < logRange) return;
|
||||||
this.preloading = true;
|
this.preloading = true;
|
||||||
await podLogsStore.load(this.tabId).then(() => this.preloading = false);
|
await podLogsStore.load(this.tabId);
|
||||||
if (this.logsElement.scrollHeight > scrollHeight) {
|
this.preloading = false;
|
||||||
|
if (podLogsStore.lines > lines) {
|
||||||
// Set scroll position back to place where preloading started
|
// Set scroll position back to place where preloading started
|
||||||
this.logsElement.scrollTop = this.logsElement.scrollHeight - scrollHeight - 48;
|
this.logsElement.current.scrollTop = (podLogsStore.lines - lines) * lineHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed prop which returns logs with or without timestamps added to each line and
|
* A function for various actions after search is happened
|
||||||
* does separation between new and old logs
|
* @param query {string} A text from search field
|
||||||
* @returns {Array} An array with 2 items - [oldLogs, newLogs]
|
|
||||||
*/
|
*/
|
||||||
@computed
|
@autobind()
|
||||||
get logs() {
|
onSearch(query: string) {
|
||||||
if (!podLogsStore.logs.has(this.tabId)) return [];
|
this.toOverlay();
|
||||||
const logs = podLogsStore.logs.get(this.tabId);
|
|
||||||
const { getData, removeTimestamps, newLogSince } = podLogsStore;
|
|
||||||
const { showTimestamps } = getData(this.tabId);
|
|
||||||
let oldLogs: string[] = logs;
|
|
||||||
let newLogs: string[] = [];
|
|
||||||
if (newLogSince.has(this.tabId)) {
|
|
||||||
// Finding separator timestamp in logs
|
|
||||||
const index = logs.findIndex(item => item.includes(newLogSince.get(this.tabId)));
|
|
||||||
if (index !== -1) {
|
|
||||||
// Splitting logs to old and new ones
|
|
||||||
oldLogs = logs.slice(0, index);
|
|
||||||
newLogs = logs.slice(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!showTimestamps) {
|
|
||||||
return [oldLogs, newLogs].map(logs => logs.map(item => removeTimestamps(item)))
|
|
||||||
}
|
|
||||||
return [oldLogs, newLogs];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onScroll = (evt: React.UIEvent<HTMLDivElement>) => {
|
/**
|
||||||
const logsArea = evt.currentTarget;
|
* Scrolling to active overlay (search word highlight)
|
||||||
const toBottomOffset = 100 * 16; // 100 lines * 16px (height of each line)
|
*/
|
||||||
const { scrollHeight, clientHeight, scrollTop } = logsArea;
|
@autobind()
|
||||||
if (scrollTop === 0) {
|
toOverlay() {
|
||||||
this.loadMore(scrollHeight);
|
const { activeOverlayLine } = searchStore;
|
||||||
|
if (!this.virtualListRef.current || activeOverlayLine === undefined) return;
|
||||||
|
// Scroll vertically
|
||||||
|
this.virtualListRef.current.scrollToItem(activeOverlayLine, "center");
|
||||||
|
// Scroll horizontally in timeout since virtual list need some time to prepare its contents
|
||||||
|
setTimeout(() => {
|
||||||
|
const overlay = document.querySelector(".PodLogs .list span.active");
|
||||||
|
if (!overlay) return;
|
||||||
|
overlay.scrollIntoViewIfNeeded();
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
if (scrollHeight - scrollTop > toBottomOffset) {
|
|
||||||
this.showJumpToBottom = true;
|
/**
|
||||||
} else {
|
* Computed prop which returns logs with or without timestamps added to each line
|
||||||
|
* @returns {Array} An array log items
|
||||||
|
*/
|
||||||
|
@computed
|
||||||
|
get logs(): string[] {
|
||||||
|
if (!podLogsStore.logs.has(this.tabId)) return [];
|
||||||
|
const logs = podLogsStore.logs.get(this.tabId);
|
||||||
|
const { getData, removeTimestamps } = podLogsStore;
|
||||||
|
const { showTimestamps } = getData(this.tabId);
|
||||||
|
if (!showTimestamps) {
|
||||||
|
return logs.map(item => removeTimestamps(item));
|
||||||
|
}
|
||||||
|
return logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
onScroll = (props: ListOnScrollProps) => {
|
||||||
|
if (!this.logsElement.current) return;
|
||||||
|
const toBottomOffset = 100 * lineHeight; // 100 lines * 18px (height of each line)
|
||||||
|
const { scrollHeight, clientHeight } = this.logsElement.current;
|
||||||
|
const { scrollDirection, scrollOffset, scrollUpdateWasRequested } = props;
|
||||||
|
if (scrollDirection == "forward") {
|
||||||
|
if (scrollHeight - scrollOffset < toBottomOffset) {
|
||||||
this.showJumpToBottom = false;
|
this.showJumpToBottom = false;
|
||||||
}
|
}
|
||||||
this.lastLineIsShown = clientHeight + scrollTop === scrollHeight;
|
if (clientHeight + scrollOffset === scrollHeight) {
|
||||||
};
|
this.lastLineIsShown = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.lastLineIsShown = false;
|
||||||
|
// Trigger loading only if scrolled by user
|
||||||
|
if (scrollOffset === 0 && !scrollUpdateWasRequested) {
|
||||||
|
this.loadMore();
|
||||||
|
}
|
||||||
|
if (scrollHeight - scrollOffset > toBottomOffset) {
|
||||||
|
this.showJumpToBottom = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
scrollToBottom = () => {
|
||||||
|
if (!this.virtualListRef.current) return;
|
||||||
|
this.hideHorizontalScroll = true;
|
||||||
|
this.virtualListRef.current.scrollToItem(this.logs.length, "end");
|
||||||
|
this.showJumpToBottom = false;
|
||||||
|
// Showing horizontal scrollbar after VirtualList settles down
|
||||||
|
setTimeout(() => this.hideHorizontalScroll = false, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function is called by VirtualList for rendering each of the row
|
||||||
|
* @param rowIndex {Number} index of the log element in logs array
|
||||||
|
* @returns A react element with a row itself
|
||||||
|
*/
|
||||||
|
getLogRow = (rowIndex: number) => {
|
||||||
|
const { searchQuery, isActiveOverlay } = searchStore;
|
||||||
|
const item = this.logs[rowIndex];
|
||||||
|
const contents: React.ReactElement[] = [];
|
||||||
|
if (searchQuery) { // If search is enabled, replace keyword with backgrounded <span>
|
||||||
|
// Case-insensitive search (lowercasing query and keywords in line)
|
||||||
|
const regex = new RegExp(searchStore.escapeRegex(searchQuery), "gi");
|
||||||
|
const matches = item.matchAll(regex);
|
||||||
|
const modified = item.replace(regex, match => match.toLowerCase());
|
||||||
|
// Splitting text line by keyword
|
||||||
|
const pieces = modified.split(searchQuery.toLowerCase());
|
||||||
|
pieces.forEach((piece, index) => {
|
||||||
|
const active = isActiveOverlay(rowIndex, index);
|
||||||
|
const lastItem = index === pieces.length - 1;
|
||||||
|
const overlay = !lastItem ?
|
||||||
|
<span className={cssNames({ active })}>{matches.next().value}</span> :
|
||||||
|
null
|
||||||
|
contents.push(
|
||||||
|
<React.Fragment key={piece + index}>
|
||||||
|
{piece}{overlay}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={cssNames("LogRow")}>
|
||||||
|
{contents.length > 1 ? contents : item}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderJumpToBottom() {
|
renderJumpToBottom() {
|
||||||
if (!this.logsElement) return null;
|
if (!this.logsElement) return null;
|
||||||
@ -149,10 +220,7 @@ export class PodLogs extends React.Component<Props> {
|
|||||||
className={cssNames("jump-to-bottom flex gaps", {active: this.showJumpToBottom})}
|
className={cssNames("jump-to-bottom flex gaps", {active: this.showJumpToBottom})}
|
||||||
onClick={evt => {
|
onClick={evt => {
|
||||||
evt.currentTarget.blur();
|
evt.currentTarget.blur();
|
||||||
this.logsElement.scrollTo({
|
this.scrollToBottom();
|
||||||
top: this.logsElement.scrollHeight,
|
|
||||||
behavior: "auto"
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trans>Jump to bottom</Trans>
|
<Trans>Jump to bottom</Trans>
|
||||||
@ -162,13 +230,15 @@ export class PodLogs extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderLogs() {
|
renderLogs() {
|
||||||
const [oldLogs, newLogs] = this.logs;
|
// Generating equal heights for each row with ability to do multyrow logs in future
|
||||||
|
// e. g. for wrapping logs feature
|
||||||
|
const rowHeights = new Array(this.logs.length).fill(lineHeight);
|
||||||
if (!this.ready) {
|
if (!this.ready) {
|
||||||
return <Spinner center/>;
|
return <Spinner center/>;
|
||||||
}
|
}
|
||||||
if (!oldLogs.length && !newLogs.length) {
|
if (!this.logs.length) {
|
||||||
return (
|
return (
|
||||||
<div className="flex align-center justify-center">
|
<div className="flex box grow align-center justify-center">
|
||||||
<Trans>There are no logs available for container.</Trans>
|
<Trans>There are no logs available for container.</Trans>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -177,16 +247,18 @@ export class PodLogs extends React.Component<Props> {
|
|||||||
<>
|
<>
|
||||||
{this.preloading && (
|
{this.preloading && (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Spinner />
|
<Spinner center />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(this.colorConverter.ansi_to_html(oldLogs.join("\n"))) }} />
|
<VirtualList
|
||||||
{newLogs.length > 0 && (
|
items={this.logs}
|
||||||
<>
|
rowHeights={rowHeights}
|
||||||
<p className="new-logs-sep" title={_i18n._(t`New logs since opening logs tab`)}/>
|
getRow={this.getLogRow}
|
||||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(this.colorConverter.ansi_to_html(newLogs.join("\n"))) }} />
|
onScroll={this.onScroll}
|
||||||
</>
|
outerRef={this.logsElement}
|
||||||
)}
|
ref={this.virtualListRef}
|
||||||
|
className="box grow"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -201,17 +273,20 @@ export class PodLogs extends React.Component<Props> {
|
|||||||
logs={this.logs}
|
logs={this.logs}
|
||||||
save={this.save}
|
save={this.save}
|
||||||
reload={this.reload}
|
reload={this.reload}
|
||||||
|
onSearch={this.onSearch}
|
||||||
|
toPrevOverlay={this.toOverlay}
|
||||||
|
toNextOverlay={this.toOverlay}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("PodLogs flex column", className)}>
|
<div className={cssNames("PodLogs flex column", className, { noscroll: this.hideHorizontalScroll })}>
|
||||||
<InfoPanel
|
<InfoPanel
|
||||||
tabId={this.props.tab.id}
|
tabId={this.props.tab.id}
|
||||||
controls={controls}
|
controls={controls}
|
||||||
showSubmitClose={false}
|
showSubmitClose={false}
|
||||||
showButtons={false}
|
showButtons={false}
|
||||||
/>
|
/>
|
||||||
<div className="logs" onScroll={this.onScroll} ref={e => this.logsElement = e}>
|
<div className="logs flex">
|
||||||
{this.renderJumpToBottom()}
|
{this.renderJumpToBottom()}
|
||||||
{this.renderLogs()}
|
{this.renderLogs()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,11 +2,11 @@ import React from "react";
|
|||||||
import { DrawerItem, DrawerItemProps } from "./drawer-item";
|
import { DrawerItem, DrawerItemProps } from "./drawer-item";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
|
|
||||||
interface Props extends DrawerItemProps {
|
export interface DrawerItemLabelsProps extends DrawerItemProps {
|
||||||
labels: string[];
|
labels: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DrawerItemLabels(props: Props) {
|
export function DrawerItemLabels(props: DrawerItemLabelsProps) {
|
||||||
const { labels, ...itemProps } = props;
|
const { labels, ...itemProps } = props;
|
||||||
if (!labels || !labels.length) {
|
if (!labels || !labels.length) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -5,14 +5,14 @@ import { Icon } from "../icon";
|
|||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { _i18n } from "../../i18n";
|
import { _i18n } from "../../i18n";
|
||||||
|
|
||||||
interface Props {
|
export interface DrawerParamTogglerProps {
|
||||||
label: string | number;
|
label: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
}
|
}
|
||||||
export class DrawerParamToggler extends React.Component<Props, State> {
|
export class DrawerParamToggler extends React.Component<DrawerParamTogglerProps, State> {
|
||||||
public state: State = {}
|
public state: State = {}
|
||||||
|
|
||||||
toggle = () => {
|
toggle = () => {
|
||||||
|
|||||||
@ -2,12 +2,12 @@ import "./drawer-title.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
|
|
||||||
interface Props {
|
export interface DrawerTitleProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DrawerTitle extends React.Component<Props> {
|
export class DrawerTitle extends React.Component<DrawerTitleProps> {
|
||||||
render() {
|
render() {
|
||||||
const { title, children, className } = this.props
|
const { title, children, className } = this.props
|
||||||
return (
|
return (
|
||||||
|
|||||||
28
src/renderer/components/editable-list/editable-list.scss
Normal file
28
src/renderer/components/editable-list/editable-list.scss
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
.EditableList {
|
||||||
|
.el-contents {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: $padding * 2;
|
||||||
|
|
||||||
|
.el-value-remove {
|
||||||
|
.Icon {
|
||||||
|
justify-content: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
padding: $padding $padding * 2;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
|
||||||
|
:last-child {
|
||||||
|
margin-bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
:first-child {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/renderer/components/editable-list/editable-list.tsx
Normal file
71
src/renderer/components/editable-list/editable-list.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import "./editable-list.scss"
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { Input } from "../input";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { autobind } from "../../utils";
|
||||||
|
import { _i18n } from "../../i18n";
|
||||||
|
|
||||||
|
export interface Props<T> {
|
||||||
|
items: T[],
|
||||||
|
add: (newItem: string) => void,
|
||||||
|
remove: (info: { oldItem: T, index: number }) => void,
|
||||||
|
placeholder?: string,
|
||||||
|
|
||||||
|
// An optional prop used to convert T to a displayable string
|
||||||
|
// defaults to `String`
|
||||||
|
renderItem?: (item: T, index: number) => React.ReactNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProps: Partial<Props<any>> = {
|
||||||
|
placeholder: _i18n._("Add new item..."),
|
||||||
|
renderItem: (item: any, index: number) => <React.Fragment key={index}>{item}</React.Fragment>
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class EditableList<T> extends React.Component<Props<T>> {
|
||||||
|
static defaultProps = defaultProps as Props<any>;
|
||||||
|
@observable currentNewItem = "";
|
||||||
|
|
||||||
|
@autobind()
|
||||||
|
onSubmit(val: string) {
|
||||||
|
const { add } = this.props
|
||||||
|
|
||||||
|
if (val) {
|
||||||
|
add(val)
|
||||||
|
this.currentNewItem = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { items, remove, renderItem, placeholder } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="EditableList">
|
||||||
|
<div className="el-header">
|
||||||
|
<Input
|
||||||
|
theme="round-black"
|
||||||
|
value={this.currentNewItem}
|
||||||
|
onSubmit={this.onSubmit}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={val => this.currentNewItem = val}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="el-contents">
|
||||||
|
{
|
||||||
|
items.map((item, index) => (
|
||||||
|
<div key={item + `${index}`} className="el-item Badge">
|
||||||
|
<div>{renderItem(item, index)}</div>
|
||||||
|
<div className="el-value-remove">
|
||||||
|
<Icon material="delete_outline" onClick={() => remove(({ index, oldItem: item }))} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/renderer/components/editable-list/index.ts
Normal file
1
src/renderer/components/editable-list/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./editable-list"
|
||||||
@ -1,3 +1,4 @@
|
|||||||
export * from './input'
|
export * from './input'
|
||||||
export * from './search-input'
|
export * from './search-input'
|
||||||
|
export * from './search-input-url'
|
||||||
export * from './file-input'
|
export * from './file-input'
|
||||||
|
|||||||
@ -3,13 +3,16 @@ import "./input.scss";
|
|||||||
import React, { DOMAttributes, InputHTMLAttributes, TextareaHTMLAttributes } from "react";
|
import React, { DOMAttributes, InputHTMLAttributes, TextareaHTMLAttributes } from "react";
|
||||||
import { autobind, cssNames, debouncePromise } from "../../utils";
|
import { autobind, cssNames, debouncePromise } from "../../utils";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { conditionalValidators, Validator } from "./input_validators";
|
import * as Validators from "./input_validators";
|
||||||
|
import { InputValidator } from "./input_validators";
|
||||||
import isString from "lodash/isString"
|
import isString from "lodash/isString"
|
||||||
import isFunction from "lodash/isFunction"
|
import isFunction from "lodash/isFunction"
|
||||||
import isBoolean from "lodash/isBoolean"
|
import isBoolean from "lodash/isBoolean"
|
||||||
import uniqueId from "lodash/uniqueId"
|
import uniqueId from "lodash/uniqueId"
|
||||||
|
|
||||||
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
|
const { conditionalValidators, ...InputValidators } = Validators;
|
||||||
|
export { InputValidators, InputValidator }
|
||||||
|
|
||||||
type InputElement = HTMLInputElement | HTMLTextAreaElement;
|
type InputElement = HTMLInputElement | HTMLTextAreaElement;
|
||||||
type InputElementProps = InputHTMLAttributes<InputElement> & TextareaHTMLAttributes<InputElement> & DOMAttributes<InputElement>;
|
type InputElementProps = InputHTMLAttributes<InputElement> & TextareaHTMLAttributes<InputElement> & DOMAttributes<InputElement>;
|
||||||
|
|
||||||
@ -24,7 +27,8 @@ export type InputProps<T = string> = Omit<InputElementProps, "onChange" | "onSub
|
|||||||
showValidationLine?: boolean; // show animated validation line for async validators
|
showValidationLine?: boolean; // show animated validation line for async validators
|
||||||
iconLeft?: string | React.ReactNode; // material-icon name in case of string-type
|
iconLeft?: string | React.ReactNode; // material-icon name in case of string-type
|
||||||
iconRight?: string | React.ReactNode;
|
iconRight?: string | React.ReactNode;
|
||||||
validators?: Validator | Validator[];
|
contentRight?: string | React.ReactNode; // Any component of string goes after iconRight
|
||||||
|
validators?: InputValidator | InputValidator[];
|
||||||
onChange?(value: T, evt: React.ChangeEvent<InputElement>): void;
|
onChange?(value: T, evt: React.ChangeEvent<InputElement>): void;
|
||||||
onSubmit?(value: T): void;
|
onSubmit?(value: T): void;
|
||||||
}
|
}
|
||||||
@ -49,7 +53,7 @@ export class Input extends React.Component<InputProps, State> {
|
|||||||
static defaultProps = defaultProps as object;
|
static defaultProps = defaultProps as object;
|
||||||
|
|
||||||
public input: InputElement;
|
public input: InputElement;
|
||||||
public validators: Validator[] = [];
|
public validators: InputValidator[] = [];
|
||||||
|
|
||||||
public state: State = {
|
public state: State = {
|
||||||
dirty: !!this.props.dirty,
|
dirty: !!this.props.dirty,
|
||||||
@ -149,7 +153,7 @@ export class Input extends React.Component<InputProps, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getValidatorError(value: string, { message }: Validator) {
|
private getValidatorError(value: string, { message }: InputValidator) {
|
||||||
if (isFunction(message)) return message(value, this.props)
|
if (isFunction(message)) return message(value, this.props)
|
||||||
return message || "";
|
return message || "";
|
||||||
}
|
}
|
||||||
@ -213,6 +217,10 @@ export class Input extends React.Component<InputProps, State> {
|
|||||||
onKeyDown(evt: React.KeyboardEvent<any>) {
|
onKeyDown(evt: React.KeyboardEvent<any>) {
|
||||||
const modified = evt.shiftKey || evt.metaKey || evt.altKey || evt.ctrlKey;
|
const modified = evt.shiftKey || evt.metaKey || evt.altKey || evt.ctrlKey;
|
||||||
|
|
||||||
|
if (this.props.onKeyDown) {
|
||||||
|
this.props.onKeyDown(evt);
|
||||||
|
}
|
||||||
|
|
||||||
switch (evt.key) {
|
switch (evt.key) {
|
||||||
case "Enter":
|
case "Enter":
|
||||||
if (this.props.onSubmit && !modified && !evt.repeat) {
|
if (this.props.onSubmit && !modified && !evt.repeat) {
|
||||||
@ -258,7 +266,7 @@ export class Input extends React.Component<InputProps, State> {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
multiLine, showValidationLine, validators, theme, maxRows, children,
|
multiLine, showValidationLine, validators, theme, maxRows, children,
|
||||||
maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight,
|
maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight, contentRight,
|
||||||
...inputProps
|
...inputProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { focused, dirty, valid, validating, errors } = this.state;
|
const { focused, dirty, valid, validating, errors } = this.state;
|
||||||
@ -288,9 +296,10 @@ export class Input extends React.Component<InputProps, State> {
|
|||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<label className="input-area flex gaps align-center">
|
<label className="input-area flex gaps align-center">
|
||||||
{isString(iconLeft) ? <Icon material={iconLeft} /> : iconLeft}
|
{isString(iconLeft) ? <Icon material={iconLeft}/> : iconLeft}
|
||||||
{multiLine ? <textarea {...inputProps as any} /> : <input {...inputProps as any} />}
|
{multiLine ? <textarea {...inputProps as any} /> : <input {...inputProps as any} />}
|
||||||
{isString(iconRight) ? <Icon material={iconRight} /> : iconRight}
|
{isString(iconRight) ? <Icon material={iconRight} /> : iconRight}
|
||||||
|
{contentRight}
|
||||||
</label>
|
</label>
|
||||||
<div className="input-info flex gaps">
|
<div className="input-info flex gaps">
|
||||||
{!valid && dirty && (
|
{!valid && dirty && (
|
||||||
|
|||||||
@ -4,26 +4,26 @@ import { t } from "@lingui/macro";
|
|||||||
import { _i18n } from '../../i18n';
|
import { _i18n } from '../../i18n';
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
|
|
||||||
export interface Validator {
|
export interface InputValidator {
|
||||||
debounce?: number; // debounce for async validators in ms
|
debounce?: number; // debounce for async validators in ms
|
||||||
condition?(props: InputProps): boolean; // auto-bind condition depending on input props
|
condition?(props: InputProps): boolean; // auto-bind condition depending on input props
|
||||||
message?: ReactNode | ((value: string, props?: InputProps) => ReactNode | string);
|
message?: ReactNode | ((value: string, props?: InputProps) => ReactNode | string);
|
||||||
validate(value: string, props?: InputProps): boolean | Promise<any>; // promise can throw error message
|
validate(value: string, props?: InputProps): boolean | Promise<any>; // promise can throw error message
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isRequired: Validator = {
|
export const isRequired: InputValidator = {
|
||||||
condition: ({ required }) => required,
|
condition: ({ required }) => required,
|
||||||
message: () => _i18n._(t`This field is required`),
|
message: () => _i18n._(t`This field is required`),
|
||||||
validate: value => !!value.trim(),
|
validate: value => !!value.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isEmail: Validator = {
|
export const isEmail: InputValidator = {
|
||||||
condition: ({ type }) => type === "email",
|
condition: ({ type }) => type === "email",
|
||||||
message: () => _i18n._(t`Wrong email format`),
|
message: () => _i18n._(t`Wrong email format`),
|
||||||
validate: value => !!value.match(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/),
|
validate: value => !!value.match(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isNumber: Validator = {
|
export const isNumber: InputValidator = {
|
||||||
condition: ({ type }) => type === "number",
|
condition: ({ type }) => type === "number",
|
||||||
message: () => _i18n._(t`Invalid number`),
|
message: () => _i18n._(t`Invalid number`),
|
||||||
validate: (value, { min, max }) => {
|
validate: (value, { min, max }) => {
|
||||||
@ -36,37 +36,37 @@ export const isNumber: Validator = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isUrl: Validator = {
|
export const isUrl: InputValidator = {
|
||||||
condition: ({ type }) => type === "url",
|
condition: ({ type }) => type === "url",
|
||||||
message: () => _i18n._(t`Wrong url format`),
|
message: () => _i18n._(t`Wrong url format`),
|
||||||
validate: value => !!value.match(/^$|^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/),
|
validate: value => !!value.match(/^$|^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isPath: Validator = {
|
export const isPath: InputValidator = {
|
||||||
condition: ({ type }) => type === "text",
|
condition: ({ type }) => type === "text",
|
||||||
message: () => _i18n._(t`This field must be a valid path`),
|
message: () => _i18n._(t`This field must be a valid path`),
|
||||||
validate: value => !value || fse.pathExistsSync(value),
|
validate: value => !value || fse.pathExistsSync(value),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const minLength: Validator = {
|
export const minLength: InputValidator = {
|
||||||
condition: ({ minLength }) => !!minLength,
|
condition: ({ minLength }) => !!minLength,
|
||||||
message: (value, { minLength }) => _i18n._(t`Minimum length is ${minLength}`),
|
message: (value, { minLength }) => _i18n._(t`Minimum length is ${minLength}`),
|
||||||
validate: (value, { minLength }) => value.length >= minLength,
|
validate: (value, { minLength }) => value.length >= minLength,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const maxLength: Validator = {
|
export const maxLength: InputValidator = {
|
||||||
condition: ({ maxLength }) => !!maxLength,
|
condition: ({ maxLength }) => !!maxLength,
|
||||||
message: (value, { maxLength }) => _i18n._(t`Maximum length is ${maxLength}`),
|
message: (value, { maxLength }) => _i18n._(t`Maximum length is ${maxLength}`),
|
||||||
validate: (value, { maxLength }) => value.length <= maxLength,
|
validate: (value, { maxLength }) => value.length <= maxLength,
|
||||||
};
|
};
|
||||||
|
|
||||||
const systemNameMatcher = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/;
|
const systemNameMatcher = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/;
|
||||||
export const systemName: Validator = {
|
export const systemName: InputValidator = {
|
||||||
message: () => _i18n._(t`A System Name must be lowercase DNS labels separated by dots. DNS labels are alphanumerics and dashes enclosed by alphanumerics.`),
|
message: () => _i18n._(t`A System Name must be lowercase DNS labels separated by dots. DNS labels are alphanumerics and dashes enclosed by alphanumerics.`),
|
||||||
validate: value => !!value.match(systemNameMatcher),
|
validate: value => !!value.match(systemNameMatcher),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const accountId: Validator = {
|
export const accountId: InputValidator = {
|
||||||
message: () => _i18n._(t`Invalid account ID`),
|
message: () => _i18n._(t`Invalid account ID`),
|
||||||
validate: value => (isEmail.validate(value) || systemName.validate(value))
|
validate: value => (isEmail.validate(value) || systemName.validate(value))
|
||||||
};
|
};
|
||||||
|
|||||||
49
src/renderer/components/input/search-input-url.tsx
Normal file
49
src/renderer/components/input/search-input-url.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React from "react";
|
||||||
|
import debounce from "lodash/debounce";
|
||||||
|
import { autorun, observable } from "mobx";
|
||||||
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
|
import { getSearch, setSearch } from "../../navigation";
|
||||||
|
import { InputProps } from "./input";
|
||||||
|
import { SearchInput } from "./search-input";
|
||||||
|
|
||||||
|
interface Props extends InputProps {
|
||||||
|
compact?: boolean; // show only search-icon when not focused
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class SearchInputUrl extends React.Component<Props> {
|
||||||
|
@observable inputVal = ""; // fix: use empty string to avoid react warnings
|
||||||
|
|
||||||
|
@disposeOnUnmount
|
||||||
|
updateInput = autorun(() => this.inputVal = getSearch())
|
||||||
|
updateUrl = debounce((val: string) => setSearch(val), 250)
|
||||||
|
|
||||||
|
setValue = (value: string) => {
|
||||||
|
this.inputVal = value;
|
||||||
|
this.updateUrl(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear = () => {
|
||||||
|
this.setValue("");
|
||||||
|
this.updateUrl.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = (val: string, evt: React.ChangeEvent<any>) => {
|
||||||
|
this.setValue(val);
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(val, evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { inputVal } = this;
|
||||||
|
return (
|
||||||
|
<SearchInput
|
||||||
|
value={inputVal}
|
||||||
|
onChange={this.onChange}
|
||||||
|
onClear={this.clear}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,10 @@
|
|||||||
|
|
||||||
> label {
|
> label {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
|
box-shadow: 0 0 0 1px $halfGray;
|
||||||
padding: 6px 6px 6px 10px;
|
padding: 6px 6px 6px 10px;
|
||||||
|
|
||||||
.Icon {
|
.Icon {
|
||||||
|
|||||||
@ -1,22 +1,22 @@
|
|||||||
import "./search-input.scss";
|
import "./search-input.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React, { createRef } from "react";
|
||||||
import debounce from "lodash/debounce"
|
|
||||||
import { autorun, observable } from "mobx";
|
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
|
||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { _i18n } from "../../i18n";
|
||||||
|
import { autobind, cssNames } from "../../utils";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { cssNames } from "../../utils";
|
|
||||||
import { Input, InputProps } from "./input";
|
import { Input, InputProps } from "./input";
|
||||||
import { getSearch, setSearch } from "../../navigation";
|
|
||||||
import { _i18n } from '../../i18n';
|
|
||||||
|
|
||||||
interface Props extends InputProps {
|
interface Props extends InputProps {
|
||||||
compact?: boolean; // show only search-icon when not focused
|
compact?: boolean; // show only search-icon when not focused
|
||||||
|
closeIcon?: boolean;
|
||||||
|
onClear?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultProps: Partial<Props> = {
|
const defaultProps: Partial<Props> = {
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
|
closeIcon: true,
|
||||||
get placeholder() {
|
get placeholder() {
|
||||||
return _i18n._(t`Search...`)
|
return _i18n._(t`Search...`)
|
||||||
},
|
},
|
||||||
@ -26,28 +26,25 @@ const defaultProps: Partial<Props> = {
|
|||||||
export class SearchInput extends React.Component<Props> {
|
export class SearchInput extends React.Component<Props> {
|
||||||
static defaultProps = defaultProps as object;
|
static defaultProps = defaultProps as object;
|
||||||
|
|
||||||
@observable inputVal = ""; // fix: use empty string to avoid react warnings
|
private input = createRef<Input>();
|
||||||
|
|
||||||
@disposeOnUnmount
|
componentDidMount() {
|
||||||
updateInput = autorun(() => this.inputVal = getSearch())
|
addEventListener("keydown", this.focus);
|
||||||
updateUrl = debounce((val: string) => setSearch(val), 250)
|
}
|
||||||
|
|
||||||
setValue = (value: string) => {
|
componentWillUnmount() {
|
||||||
this.inputVal = value;
|
removeEventListener("keydown", this.focus);
|
||||||
this.updateUrl(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clear = () => {
|
clear = () => {
|
||||||
this.setValue("");
|
if (this.props.onClear) {
|
||||||
this.updateUrl.flush();
|
this.props.onClear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange = (val: string, evt: React.ChangeEvent<any>) => {
|
onChange = (val: string, evt: React.ChangeEvent<any>) => {
|
||||||
this.setValue(val);
|
|
||||||
if (this.props.onChange) {
|
|
||||||
this.props.onChange(val, evt);
|
this.props.onChange(val, evt);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onKeyDown = (evt: React.KeyboardEvent<any>) => {
|
onKeyDown = (evt: React.KeyboardEvent<any>) => {
|
||||||
if (this.props.onKeyDown) {
|
if (this.props.onKeyDown) {
|
||||||
@ -61,20 +58,27 @@ export class SearchInput extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@autobind()
|
||||||
|
focus(evt: KeyboardEvent) {
|
||||||
|
const meta = evt.metaKey || evt.ctrlKey;
|
||||||
|
if (meta && evt.key == "f") {
|
||||||
|
this.input.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { inputVal } = this;
|
const { className, compact, closeIcon, onClear, ...inputProps } = this.props;
|
||||||
const { className, compact, ...inputProps } = this.props;
|
const icon = this.props.value
|
||||||
const icon = inputVal
|
? closeIcon ? <Icon small material="close" onClick={this.clear}/> : null
|
||||||
? <Icon small material="close" onClick={this.clear}/>
|
|
||||||
: <Icon small material="search"/>
|
: <Icon small material="search"/>
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
className={cssNames("SearchInput", className, { compact })}
|
className={cssNames("SearchInput", className, { compact })}
|
||||||
value={inputVal}
|
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
iconRight={icon}
|
iconRight={icon}
|
||||||
|
ref={this.input}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,15 +22,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.SearchInput {
|
|
||||||
label {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: $radius;
|
|
||||||
box-shadow: 0 0 0 1px $halfGray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .items {
|
> .items {
|
||||||
|
|||||||
@ -5,14 +5,14 @@ import React, { ReactNode } from "react";
|
|||||||
import { computed, observable, reaction, toJS, when } from "mobx";
|
import { computed, observable, reaction, toJS, when } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { Plural, Trans } from "@lingui/macro";
|
import { Plural, Trans } from "@lingui/macro";
|
||||||
import { ConfirmDialog, IConfirmDialogParams } from "../confirm-dialog";
|
import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog";
|
||||||
import { SortingCallback, Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps } from "../table";
|
import { TableSortCallback, Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps } from "../table";
|
||||||
import { autobind, createStorage, cssNames, IClassName, isReactNode, noop, prevDefault, stopPropagation } from "../../utils";
|
import { autobind, createStorage, cssNames, IClassName, isReactNode, noop, prevDefault, stopPropagation } from "../../utils";
|
||||||
import { AddRemoveButtons, AddRemoveButtonsProps } from "../add-remove-buttons";
|
import { AddRemoveButtons, AddRemoveButtonsProps } from "../add-remove-buttons";
|
||||||
import { NoItems } from "../no-items";
|
import { NoItems } from "../no-items";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import { ItemObject, ItemStore } from "../../item.store";
|
import { ItemObject, ItemStore } from "../../item.store";
|
||||||
import { SearchInput } from "../input";
|
import { SearchInputUrl } from "../input";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||||
import { Filter, FilterType, pageFilters } from "./page-filters.store";
|
import { Filter, FilterType, pageFilters } from "./page-filters.store";
|
||||||
import { PageFiltersList } from "./page-filters-list";
|
import { PageFiltersList } from "./page-filters-list";
|
||||||
@ -52,7 +52,7 @@ export interface ItemListLayoutProps<T extends ItemObject = ItemObject> {
|
|||||||
isSelectable?: boolean; // show checkbox in rows for selecting items
|
isSelectable?: boolean; // show checkbox in rows for selecting items
|
||||||
isSearchable?: boolean; // apply search-filter & add search-input
|
isSearchable?: boolean; // apply search-filter & add search-input
|
||||||
copyClassNameFromHeadCells?: boolean;
|
copyClassNameFromHeadCells?: boolean;
|
||||||
sortingCallbacks?: { [sortBy: string]: SortingCallback };
|
sortingCallbacks?: { [sortBy: string]: TableSortCallback };
|
||||||
tableProps?: Partial<TableProps>; // low-level table configuration
|
tableProps?: Partial<TableProps>; // low-level table configuration
|
||||||
renderTableHeader: TableCellProps[] | null;
|
renderTableHeader: TableCellProps[] | null;
|
||||||
renderTableContents: (item: T) => (ReactNode | TableCellProps)[];
|
renderTableContents: (item: T) => (ReactNode | TableCellProps)[];
|
||||||
@ -67,7 +67,7 @@ export interface ItemListLayoutProps<T extends ItemObject = ItemObject> {
|
|||||||
onDetails?: (item: T) => void;
|
onDetails?: (item: T) => void;
|
||||||
|
|
||||||
// other
|
// other
|
||||||
customizeRemoveDialog?: (selectedItems: T[]) => Partial<IConfirmDialogParams>;
|
customizeRemoveDialog?: (selectedItems: T[]) => Partial<ConfirmDialogParams>;
|
||||||
renderFooter?: (parent: ItemListLayout) => React.ReactNode;
|
renderFooter?: (parent: ItemListLayout) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,7 +349,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
[FilterType.NAMESPACE]: true, // namespace-select used instead
|
[FilterType.NAMESPACE]: true, // namespace-select used instead
|
||||||
}}/>
|
}}/>
|
||||||
</>,
|
</>,
|
||||||
search: <SearchInput/>,
|
search: <SearchInputUrl/>,
|
||||||
}
|
}
|
||||||
let header = this.renderHeaderContent(placeholders);
|
let header = this.renderHeaderContent(placeholders);
|
||||||
if (customizeHeader) {
|
if (customizeHeader) {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from "./kube-object-details"
|
export * from "./kube-object-details"
|
||||||
export * from "./kube-object-list-layout"
|
export * from "./kube-object-list-layout"
|
||||||
export * from "./kube-object-menu"
|
export * from "./kube-object-menu"
|
||||||
|
export * from "./kube-object-meta"
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { Spinner } from "../spinner";
|
|||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
import { crdStore } from "../+custom-resources/crd.store";
|
import { crdStore } from "../+custom-resources/crd.store";
|
||||||
import { CrdResourceDetails } from "../+custom-resources";
|
import { CrdResourceDetails } from "../+custom-resources";
|
||||||
import { KubeObjectMenu } from "./kube-object-menu"
|
import { KubeObjectMenu } from "./kube-object-menu"
|
||||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||||
|
|
||||||
export interface KubeObjectDetailsProps<T = KubeObject> {
|
export interface KubeObjectDetailsProps<T = KubeObject> {
|
||||||
|
|||||||
@ -2,17 +2,16 @@ import React from "react";
|
|||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { IKubeMetaField, KubeObject } from "../../api/kube-object";
|
import { IKubeMetaField, KubeObject } from "../../api/kube-object";
|
||||||
import { DrawerItem, DrawerItemLabels } from "../drawer";
|
import { DrawerItem, DrawerItemLabels } from "../drawer";
|
||||||
import { WorkloadKubeObject } from "../../api/workload-kube-object";
|
|
||||||
import { getDetailsUrl } from "../../navigation";
|
import { getDetailsUrl } from "../../navigation";
|
||||||
import { lookupApiLink } from "../../api/kube-api";
|
import { lookupApiLink } from "../../api/kube-api";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export interface Props {
|
export interface KubeObjectMetaProps {
|
||||||
object: KubeObject;
|
object: KubeObject;
|
||||||
hideFields?: IKubeMetaField[];
|
hideFields?: IKubeMetaField[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KubeObjectMeta extends React.Component<Props> {
|
export class KubeObjectMeta extends React.Component<KubeObjectMetaProps> {
|
||||||
static defaultHiddenFields: IKubeMetaField[] = [
|
static defaultHiddenFields: IKubeMetaField[] = [
|
||||||
"uid", "resourceVersion", "selfLink"
|
"uid", "resourceVersion", "selfLink"
|
||||||
];
|
];
|
||||||
|
|||||||
@ -12,14 +12,14 @@ export interface TabRoute extends RouteProps {
|
|||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
export interface TabLayoutProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: any;
|
className?: any;
|
||||||
tabs?: TabRoute[];
|
tabs?: TabRoute[];
|
||||||
contentClass?: string;
|
contentClass?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TabLayout = observer(({ className, contentClass, tabs, children }: Props) => {
|
export const TabLayout = observer(({ className, contentClass, tabs, children }: TabLayoutProps) => {
|
||||||
const routePath = navigation.location.pathname;
|
const routePath = navigation.location.pathname;
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("TabLayout", className)}>
|
<div className={cssNames("TabLayout", className)}>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { cssNames, IClassName } from "../../utils";
|
import { cssNames, IClassName } from "../../utils";
|
||||||
|
|
||||||
interface Props extends React.DOMAttributes<any> {
|
export interface WizardLayoutProps extends React.DOMAttributes<any> {
|
||||||
className?: IClassName;
|
className?: IClassName;
|
||||||
header?: React.ReactNode;
|
header?: React.ReactNode;
|
||||||
headerClass?: IClassName;
|
headerClass?: IClassName;
|
||||||
@ -15,7 +15,7 @@ interface Props extends React.DOMAttributes<any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class WizardLayout extends React.Component<Props> {
|
export class WizardLayout extends React.Component<WizardLayoutProps> {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
className, contentClass, infoPanelClass, infoPanel, header, headerClass, centered,
|
className, contentClass, infoPanelClass, infoPanel, header, headerClass, centered,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React from "react";
|
|||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { TooltipDecoratorProps, withTooltip } from "../tooltip";
|
import { TooltipDecoratorProps, withTooltip } from "../tooltip";
|
||||||
|
|
||||||
interface Props extends React.HTMLProps<any>, TooltipDecoratorProps {
|
export interface LineProgressProps extends React.HTMLProps<any>, TooltipDecoratorProps {
|
||||||
value: number;
|
value: number;
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
@ -12,8 +12,8 @@ interface Props extends React.HTMLProps<any>, TooltipDecoratorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@withTooltip
|
@withTooltip
|
||||||
export class LineProgress extends React.PureComponent<Props> {
|
export class LineProgress extends React.PureComponent<LineProgressProps> {
|
||||||
static defaultProps: Props = {
|
static defaultProps: LineProgressProps = {
|
||||||
value: 0,
|
value: 0,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import debounce from "lodash/debounce"
|
|||||||
export const MenuContext = React.createContext<MenuContextValue>(null);
|
export const MenuContext = React.createContext<MenuContextValue>(null);
|
||||||
export type MenuContextValue = Menu;
|
export type MenuContextValue = Menu;
|
||||||
|
|
||||||
interface MenuPosition {
|
export interface MenuPosition {
|
||||||
left?: boolean;
|
left?: boolean;
|
||||||
top?: boolean;
|
top?: boolean;
|
||||||
right?: boolean;
|
right?: boolean;
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export * from './notifications'
|
export * from './notifications'
|
||||||
|
export * from './notifications.store'
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import isObject from "lodash/isObject"
|
|||||||
import uniqueId from "lodash/uniqueId";
|
import uniqueId from "lodash/uniqueId";
|
||||||
import { JsonApiErrorParsed } from "../../api/json-api";
|
import { JsonApiErrorParsed } from "../../api/json-api";
|
||||||
|
|
||||||
export type IMessageId = string | number;
|
export type NotificationId = string | number;
|
||||||
export type IMessage = React.ReactNode | React.ReactNode[] | JsonApiErrorParsed;
|
export type NotificationMessage = React.ReactNode | React.ReactNode[] | JsonApiErrorParsed;
|
||||||
|
|
||||||
export enum NotificationStatus {
|
export enum NotificationStatus {
|
||||||
OK = "ok",
|
OK = "ok",
|
||||||
@ -14,20 +14,20 @@ export enum NotificationStatus {
|
|||||||
INFO = "info",
|
INFO = "info",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INotification {
|
export interface Notification {
|
||||||
id?: IMessageId;
|
id?: NotificationId;
|
||||||
message: IMessage;
|
message: NotificationMessage;
|
||||||
status?: NotificationStatus;
|
status?: NotificationStatus;
|
||||||
timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide
|
timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class NotificationsStore {
|
export class NotificationsStore {
|
||||||
public notifications = observable<INotification>([], { deep: false });
|
public notifications = observable<Notification>([], { deep: false });
|
||||||
|
|
||||||
protected autoHideTimers = new Map<IMessageId, number>();
|
protected autoHideTimers = new Map<NotificationId, number>();
|
||||||
|
|
||||||
addAutoHideTimer(notification: INotification) {
|
addAutoHideTimer(notification: Notification) {
|
||||||
this.removeAutoHideTimer(notification);
|
this.removeAutoHideTimer(notification);
|
||||||
const { id, timeout } = notification;
|
const { id, timeout } = notification;
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
@ -36,7 +36,7 @@ export class NotificationsStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAutoHideTimer(notification: INotification) {
|
removeAutoHideTimer(notification: Notification) {
|
||||||
const { id } = notification;
|
const { id } = notification;
|
||||||
if (this.autoHideTimers.has(id)) {
|
if (this.autoHideTimers.has(id)) {
|
||||||
clearTimeout(this.autoHideTimers.get(id));
|
clearTimeout(this.autoHideTimers.get(id));
|
||||||
@ -45,7 +45,7 @@ export class NotificationsStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
add(notification: INotification) {
|
add(notification: Notification) {
|
||||||
if (!notification.id) {
|
if (!notification.id) {
|
||||||
notification.id = uniqueId("notification_");
|
notification.id = uniqueId("notification_");
|
||||||
}
|
}
|
||||||
@ -56,11 +56,11 @@ export class NotificationsStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
remove(itemOrId: IMessageId | INotification) {
|
remove(itemOrId: NotificationId | Notification) {
|
||||||
if (!isObject(itemOrId)) {
|
if (!isObject(itemOrId)) {
|
||||||
itemOrId = this.notifications.find(item => item.id === itemOrId);
|
itemOrId = this.notifications.find(item => item.id === itemOrId);
|
||||||
}
|
}
|
||||||
return this.notifications.remove(itemOrId as INotification);
|
return this.notifications.remove(itemOrId as Notification);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { reaction } from "mobx";
|
|||||||
import { disposeOnUnmount, observer } from "mobx-react"
|
import { disposeOnUnmount, observer } from "mobx-react"
|
||||||
import { JsonApiErrorParsed } from "../../api/json-api";
|
import { JsonApiErrorParsed } from "../../api/json-api";
|
||||||
import { cssNames, prevDefault } from "../../utils";
|
import { cssNames, prevDefault } from "../../utils";
|
||||||
import { IMessage, INotification, notificationsStore, NotificationStatus } from "./notifications.store";
|
import { NotificationMessage, Notification, notificationsStore, NotificationStatus } from "./notifications.store";
|
||||||
import { Animate } from "../animate";
|
import { Animate } from "../animate";
|
||||||
import { Icon } from "../icon"
|
import { Icon } from "../icon"
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ import { Icon } from "../icon"
|
|||||||
export class Notifications extends React.Component {
|
export class Notifications extends React.Component {
|
||||||
public elem: HTMLElement;
|
public elem: HTMLElement;
|
||||||
|
|
||||||
static ok(message: IMessage) {
|
static ok(message: NotificationMessage) {
|
||||||
notificationsStore.add({
|
notificationsStore.add({
|
||||||
message: message,
|
message: message,
|
||||||
timeout: 2500,
|
timeout: 2500,
|
||||||
@ -21,7 +21,7 @@ export class Notifications extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static error(message: IMessage) {
|
static error(message: NotificationMessage) {
|
||||||
notificationsStore.add({
|
notificationsStore.add({
|
||||||
message: message,
|
message: message,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
@ -29,7 +29,7 @@ export class Notifications extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static info(message: IMessage, customOpts: Partial<INotification> = {}) {
|
static info(message: NotificationMessage, customOpts: Partial<Notification> = {}) {
|
||||||
return notificationsStore.add({
|
return notificationsStore.add({
|
||||||
status: NotificationStatus.INFO,
|
status: NotificationStatus.INFO,
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
@ -56,7 +56,7 @@ export class Notifications extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getMessage(notification: INotification) {
|
getMessage(notification: Notification) {
|
||||||
let { message } = notification;
|
let { message } = notification;
|
||||||
if (message instanceof JsonApiErrorParsed) {
|
if (message instanceof JsonApiErrorParsed) {
|
||||||
message = message.toString();
|
message = message.toString();
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import uniqueId from "lodash/uniqueId";
|
|||||||
|
|
||||||
// todo: refactor with contexts
|
// todo: refactor with contexts
|
||||||
|
|
||||||
interface RadioGroupProps {
|
export interface RadioGroupProps {
|
||||||
className?: any;
|
className?: any;
|
||||||
value?: any;
|
value?: any;
|
||||||
asButtons?: boolean;
|
asButtons?: boolean;
|
||||||
@ -35,7 +35,7 @@ export class RadioGroup extends React.Component<RadioGroupProps, {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RadioProps = React.HTMLProps<any> & {
|
export type RadioProps = React.HTMLProps<any> & {
|
||||||
name?: string;
|
name?: string;
|
||||||
label?: React.ReactNode | any;
|
label?: React.ReactNode | any;
|
||||||
value?: any;
|
value?: any;
|
||||||
|
|||||||
@ -4,20 +4,20 @@ import "./slider.scss";
|
|||||||
|
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import MaterialSlider, { SliderClassKey, SliderProps } from "@material-ui/core/Slider";
|
import MaterialSlider, { SliderClassKey, SliderProps as MaterialSliderProps } from "@material-ui/core/Slider";
|
||||||
|
|
||||||
interface Props extends Omit<SliderProps, "onChange"> {
|
export interface SliderProps extends Omit<MaterialSliderProps, "onChange"> {
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange?(evt: React.FormEvent<any>, value: number): void;
|
onChange?(evt: React.FormEvent<any>, value: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultProps: Partial<Props> = {
|
const defaultProps: Partial<SliderProps> = {
|
||||||
step: 1,
|
step: 1,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Slider extends Component<Props> {
|
export class Slider extends Component<SliderProps> {
|
||||||
static defaultProps = defaultProps as object;
|
static defaultProps = defaultProps as object;
|
||||||
|
|
||||||
private classNames: Partial<{ [P in SliderClassKey]: string }> = {
|
private classNames: Partial<{ [P in SliderClassKey]: string }> = {
|
||||||
|
|||||||
@ -2,12 +2,12 @@ import './cube-spinner.scss'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
|
|
||||||
interface Props {
|
export interface CubeSpinnerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
center?: boolean;
|
center?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CubeSpinner extends React.Component<Props> {
|
export class CubeSpinner extends React.Component<CubeSpinnerProps> {
|
||||||
render() {
|
render() {
|
||||||
const { className, center } = this.props;
|
const { className, center } = this.props;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -3,12 +3,12 @@ import './spinner.scss'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
|
|
||||||
interface Props extends React.HTMLProps<any> {
|
export interface SpinnerProps extends React.HTMLProps<any> {
|
||||||
singleColor?: boolean;
|
singleColor?: boolean;
|
||||||
center?: boolean;
|
center?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Spinner extends React.Component<Props, {}> {
|
export class Spinner extends React.Component<SpinnerProps, {}> {
|
||||||
private elem: HTMLElement;
|
private elem: HTMLElement;
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import React from "react";
|
|||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { TooltipDecoratorProps, withTooltip } from "../tooltip";
|
import { TooltipDecoratorProps, withTooltip } from "../tooltip";
|
||||||
|
|
||||||
interface Props extends React.HTMLAttributes<any>, TooltipDecoratorProps {
|
export interface StatusBrickProps extends React.HTMLAttributes<any>, TooltipDecoratorProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
@withTooltip
|
@withTooltip
|
||||||
export class StatusBrick extends React.Component<Props> {
|
export class StatusBrick extends React.Component<StatusBrickProps> {
|
||||||
render() {
|
render() {
|
||||||
const { className, ...elemProps } = this.props
|
const { className, ...elemProps } = this.props
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import "./stepper.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
|
|
||||||
interface Props extends React.HTMLProps<any> {
|
export interface StepperProps extends React.HTMLProps<any> {
|
||||||
step: number;
|
step: number;
|
||||||
steps: Step[];
|
steps: Step[];
|
||||||
}
|
}
|
||||||
@ -11,7 +11,7 @@ interface Step {
|
|||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Stepper extends React.Component<Props, {}> {
|
export class Stepper extends React.Component<StepperProps, {}> {
|
||||||
render() {
|
render() {
|
||||||
const { className, steps, ...props } = this.props;
|
const { className, steps, ...props } = this.props;
|
||||||
const stepsCount = steps.length;
|
const stepsCount = steps.length;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import "./table-cell.scss";
|
import "./table-cell.scss";
|
||||||
import type { SortBy, SortParams } from "./table";
|
import type { TableSortBy, TableSortParams } from "./table";
|
||||||
|
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { autobind, cssNames } from "../../utils";
|
import { autobind, cssNames } from "../../utils";
|
||||||
@ -13,9 +13,9 @@ export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
|
|||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
checkbox?: boolean; // render cell with a checkbox
|
checkbox?: boolean; // render cell with a checkbox
|
||||||
isChecked?: boolean; // mark checkbox as checked or not
|
isChecked?: boolean; // mark checkbox as checked or not
|
||||||
sortBy?: SortBy; // column name, must be same as key in sortable object <Table sortable={}/>
|
sortBy?: TableSortBy; // column name, must be same as key in sortable object <Table sortable={}/>
|
||||||
_sorting?: Partial<SortParams>; // <Table> sorting state, don't use this prop outside (!)
|
_sorting?: Partial<TableSortParams>; // <Table> sorting state, don't use this prop outside (!)
|
||||||
_sort?(sortBy: SortBy): void; // <Table> sort function, don't use this prop outside (!)
|
_sort?(sortBy: TableSortBy): void; // <Table> sort function, don't use this prop outside (!)
|
||||||
_nowrap?: boolean; // indicator, might come from parent <TableHead>, don't use this prop outside (!)
|
_nowrap?: boolean; // indicator, might come from parent <TableHead>, don't use this prop outside (!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,10 +14,10 @@ import { ItemObject } from "../../item.store";
|
|||||||
|
|
||||||
// todo: refactor + decouple search from location
|
// todo: refactor + decouple search from location
|
||||||
|
|
||||||
export type SortBy = string;
|
export type TableSortBy = string;
|
||||||
export type OrderBy = "asc" | "desc" | string;
|
export type TableOrderBy = "asc" | "desc" | string;
|
||||||
export type SortParams = { sortBy: SortBy; orderBy: OrderBy }
|
export type TableSortParams = { sortBy: TableSortBy; orderBy: TableOrderBy }
|
||||||
export type SortingCallback<D = any> = (data: D) => string | number | (string | number)[];
|
export type TableSortCallback<D = any> = (data: D) => string | number | (string | number)[];
|
||||||
|
|
||||||
export interface TableProps extends React.DOMAttributes<HTMLDivElement> {
|
export interface TableProps extends React.DOMAttributes<HTMLDivElement> {
|
||||||
items?: ItemObject[]; // Raw items data
|
items?: ItemObject[]; // Raw items data
|
||||||
@ -29,11 +29,11 @@ export interface TableProps extends React.DOMAttributes<HTMLDivElement> {
|
|||||||
sortable?: {
|
sortable?: {
|
||||||
// Define sortable callbacks for every column in <TableHead><TableCell sortBy="someCol"><TableHead>
|
// Define sortable callbacks for every column in <TableHead><TableCell sortBy="someCol"><TableHead>
|
||||||
// @sortItem argument in the callback is an object, provided in <TableRow sortItem={someColDataItem}/>
|
// @sortItem argument in the callback is an object, provided in <TableRow sortItem={someColDataItem}/>
|
||||||
[sortBy: string]: SortingCallback;
|
[sortBy: string]: TableSortCallback;
|
||||||
};
|
};
|
||||||
sortSyncWithUrl?: boolean; // sorting state is managed globally from url params
|
sortSyncWithUrl?: boolean; // sorting state is managed globally from url params
|
||||||
sortByDefault?: Partial<SortParams>; // default sorting params
|
sortByDefault?: Partial<TableSortParams>; // default sorting params
|
||||||
onSort?: (params: SortParams) => void; // callback on sort change, default: global sync with url
|
onSort?: (params: TableSortParams) => void; // callback on sort change, default: global sync with url
|
||||||
noItems?: React.ReactNode; // Show no items state table list is empty
|
noItems?: React.ReactNode; // Show no items state table list is empty
|
||||||
selectedItemId?: string; // Allows to scroll list to selected item
|
selectedItemId?: string; // Allows to scroll list to selected item
|
||||||
virtual?: boolean; // Use virtual list component to render only visible rows
|
virtual?: boolean; // Use virtual list component to render only visible rows
|
||||||
@ -55,7 +55,7 @@ export class Table extends React.Component<TableProps> {
|
|||||||
|
|
||||||
@observable sortParamsLocal = this.props.sortByDefault;
|
@observable sortParamsLocal = this.props.sortByDefault;
|
||||||
|
|
||||||
@computed get sortParams(): Partial<SortParams> {
|
@computed get sortParams(): Partial<TableSortParams> {
|
||||||
if (this.props.sortSyncWithUrl) {
|
if (this.props.sortSyncWithUrl) {
|
||||||
const sortBy = navigation.searchParams.get("sortBy")
|
const sortBy = navigation.searchParams.get("sortBy")
|
||||||
const orderBy = navigation.searchParams.get("orderBy")
|
const orderBy = navigation.searchParams.get("orderBy")
|
||||||
@ -105,7 +105,7 @@ export class Table extends React.Component<TableProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
protected onSort(params: SortParams) {
|
protected onSort(params: TableSortParams) {
|
||||||
const { sortSyncWithUrl, onSort } = this.props;
|
const { sortSyncWithUrl, onSort } = this.props;
|
||||||
if (sortSyncWithUrl) {
|
if (sortSyncWithUrl) {
|
||||||
setQueryParams(params)
|
setQueryParams(params)
|
||||||
@ -119,11 +119,11 @@ export class Table extends React.Component<TableProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
sort(colName: SortBy) {
|
sort(colName: TableSortBy) {
|
||||||
const { sortBy, orderBy } = this.sortParams;
|
const { sortBy, orderBy } = this.sortParams;
|
||||||
const sameColumn = sortBy == colName;
|
const sameColumn = sortBy == colName;
|
||||||
const newSortBy: SortBy = colName;
|
const newSortBy: TableSortBy = colName;
|
||||||
const newOrderBy: OrderBy = (!orderBy || !sameColumn || orderBy === "desc") ? "asc" : "desc";
|
const newOrderBy: TableOrderBy = (!orderBy || !sameColumn || orderBy === "desc") ? "asc" : "desc";
|
||||||
this.onSort({
|
this.onSort({
|
||||||
sortBy: String(newSortBy),
|
sortBy: String(newSortBy),
|
||||||
orderBy: newOrderBy,
|
orderBy: newOrderBy,
|
||||||
@ -159,7 +159,7 @@ export class Table extends React.Component<TableProps> {
|
|||||||
<VirtualList
|
<VirtualList
|
||||||
items={sortedItems}
|
items={sortedItems}
|
||||||
rowHeights={rowHeights}
|
rowHeights={rowHeights}
|
||||||
getTableRow={getTableRow}
|
getRow={getTableRow}
|
||||||
selectedItemId={selectedItemId}
|
selectedItemId={selectedItemId}
|
||||||
className={className}
|
className={className}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -9,6 +9,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
overflow-y: overlay !important;
|
overflow-y: overlay !important;
|
||||||
overflow-x: hidden !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,8 +4,8 @@ import "./virtual-list.scss";
|
|||||||
|
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { ListChildComponentProps, VariableSizeList } from "react-window";
|
import { Align, ListChildComponentProps, ListOnScrollProps, VariableSizeList } from "react-window";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames, noop } from "../../utils";
|
||||||
import { TableRowProps } from "../table/table-row";
|
import { TableRowProps } from "../table/table-row";
|
||||||
import { ItemObject } from "../../item.store";
|
import { ItemObject } from "../../item.store";
|
||||||
import throttle from "lodash/throttle";
|
import throttle from "lodash/throttle";
|
||||||
@ -13,15 +13,17 @@ import debounce from "lodash/debounce";
|
|||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
import ResizeSensor from "css-element-queries/src/ResizeSensor";
|
import ResizeSensor from "css-element-queries/src/ResizeSensor";
|
||||||
|
|
||||||
interface Props {
|
interface Props<T extends ItemObject = any> {
|
||||||
items: ItemObject[];
|
items: T[];
|
||||||
rowHeights: number[];
|
rowHeights: number[];
|
||||||
className?: string;
|
className?: string;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
initialOffset?: number;
|
initialOffset?: number;
|
||||||
readyOffset?: number;
|
readyOffset?: number;
|
||||||
selectedItemId?: string;
|
selectedItemId?: string;
|
||||||
getTableRow?: (uid: string) => React.ReactElement<TableRowProps>;
|
getRow?: (uid: string | number) => React.ReactElement<any>;
|
||||||
|
onScroll?: (props: ListOnScrollProps) => void;
|
||||||
|
outerRef?: React.Ref<any>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -33,6 +35,7 @@ const defaultProps: Partial<Props> = {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
initialOffset: 1,
|
initialOffset: 1,
|
||||||
readyOffset: 10,
|
readyOffset: 10,
|
||||||
|
onScroll: noop
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VirtualList extends Component<Props, State> {
|
export class VirtualList extends Component<Props, State> {
|
||||||
@ -56,7 +59,7 @@ export class VirtualList extends Component<Props, State> {
|
|||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
const { items, rowHeights } = this.props;
|
const { items, rowHeights } = this.props;
|
||||||
if (prevProps.items.length !== items.length || !isEqual(prevProps.rowHeights, rowHeights)) {
|
if (prevProps.items.length !== items.length || !isEqual(prevProps.rowHeights, rowHeights)) {
|
||||||
this.listRef.current.resetAfterIndex(0, true);
|
this.listRef.current.resetAfterIndex(0, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,18 +76,23 @@ export class VirtualList extends Component<Props, State> {
|
|||||||
getItemSize = (index: number) => this.props.rowHeights[index];
|
getItemSize = (index: number) => this.props.rowHeights[index];
|
||||||
|
|
||||||
scrollToSelectedItem = debounce(() => {
|
scrollToSelectedItem = debounce(() => {
|
||||||
|
if (!this.props.selectedItemId) return;
|
||||||
const { items, selectedItemId } = this.props;
|
const { items, selectedItemId } = this.props;
|
||||||
const index = items.findIndex(item => item.getId() == selectedItemId);
|
const index = items.findIndex(item => item.getId() == selectedItemId);
|
||||||
if (index === -1) return;
|
if (index === -1) return;
|
||||||
this.listRef.current.scrollToItem(index, "start");
|
this.listRef.current.scrollToItem(index, "start");
|
||||||
})
|
})
|
||||||
|
|
||||||
|
scrollToItem = (index: number, align: Align) => {
|
||||||
|
this.listRef.current.scrollToItem(index, align)
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { width, className, items, getTableRow } = this.props;
|
const { width, className, items, getRow, onScroll, outerRef } = this.props;
|
||||||
const { height, overscanCount } = this.state;
|
const { height, overscanCount } = this.state;
|
||||||
const rowData: RowData = {
|
const rowData: RowData = {
|
||||||
items,
|
items,
|
||||||
getTableRow
|
getRow
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("VirtualList", className)} ref={this.parentRef}>
|
<div className={cssNames("VirtualList", className)} ref={this.parentRef}>
|
||||||
@ -97,7 +105,9 @@ export class VirtualList extends Component<Props, State> {
|
|||||||
itemData={rowData}
|
itemData={rowData}
|
||||||
overscanCount={overscanCount}
|
overscanCount={overscanCount}
|
||||||
ref={this.listRef}
|
ref={this.listRef}
|
||||||
|
outerRef={outerRef}
|
||||||
children={Row}
|
children={Row}
|
||||||
|
onScroll={onScroll}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -106,7 +116,7 @@ export class VirtualList extends Component<Props, State> {
|
|||||||
|
|
||||||
interface RowData {
|
interface RowData {
|
||||||
items: ItemObject[];
|
items: ItemObject[];
|
||||||
getTableRow?: (uid: string) => React.ReactElement<TableRowProps>;
|
getRow?: (uid: string | number) => React.ReactElement<TableRowProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RowProps extends ListChildComponentProps {
|
interface RowProps extends ListChildComponentProps {
|
||||||
@ -115,9 +125,10 @@ interface RowProps extends ListChildComponentProps {
|
|||||||
|
|
||||||
const Row = observer((props: RowProps) => {
|
const Row = observer((props: RowProps) => {
|
||||||
const { index, style, data } = props;
|
const { index, style, data } = props;
|
||||||
const { items, getTableRow } = data;
|
const { items, getRow } = data;
|
||||||
const uid = items[index].getId();
|
const item = items[index];
|
||||||
const row = getTableRow(uid);
|
const uid = typeof item == "string" ? index : items[index].getId();
|
||||||
|
const row = getRow(uid);
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
return React.cloneElement(row, {
|
return React.cloneElement(row, {
|
||||||
style: Object.assign({}, row.props.style, style)
|
style: Object.assign({}, row.props.style, style)
|
||||||
|
|||||||
@ -61,6 +61,7 @@
|
|||||||
"dockInfoBackground": "#1e2125",
|
"dockInfoBackground": "#1e2125",
|
||||||
"dockInfoBorderColor": "#303136",
|
"dockInfoBorderColor": "#303136",
|
||||||
"logsBackground": "#000000",
|
"logsBackground": "#000000",
|
||||||
|
"logRowHoverBackground": "#35373a",
|
||||||
"terminalBackground": "#000000",
|
"terminalBackground": "#000000",
|
||||||
"terminalForeground": "#ffffff",
|
"terminalForeground": "#ffffff",
|
||||||
"terminalCursor": "#ffffff",
|
"terminalCursor": "#ffffff",
|
||||||
|
|||||||
@ -62,6 +62,7 @@
|
|||||||
"dockInfoBackground": "#e8e8e8",
|
"dockInfoBackground": "#e8e8e8",
|
||||||
"dockInfoBorderColor": "#c9cfd3",
|
"dockInfoBorderColor": "#c9cfd3",
|
||||||
"logsBackground": "#ffffff",
|
"logsBackground": "#ffffff",
|
||||||
|
"logRowHoverBackground": "#eeeeee",
|
||||||
"terminalBackground": "#ffffff",
|
"terminalBackground": "#ffffff",
|
||||||
"terminalForeground": "#2d2d2d",
|
"terminalForeground": "#2d2d2d",
|
||||||
"terminalCursor": "#2d2d2d",
|
"terminalCursor": "#2d2d2d",
|
||||||
|
|||||||
@ -93,6 +93,7 @@ $terminalBrightWhite: var(--terminalBrightWhite);
|
|||||||
|
|
||||||
// Logs
|
// Logs
|
||||||
$logsBackground: var(--logsBackground);
|
$logsBackground: var(--logsBackground);
|
||||||
|
$logRowHoverBackground: var(--logRowHoverBackground);
|
||||||
|
|
||||||
// Dialogs
|
// Dialogs
|
||||||
$dialogTextColor: var(--dialogTextColor);
|
$dialogTextColor: var(--dialogTextColor);
|
||||||
|
|||||||
7
types/dom.d.ts
vendored
Normal file
7
types/dom.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export {}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Element {
|
||||||
|
scrollIntoViewIfNeeded(opt_center?: boolean): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user