mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into electron-12
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
commit
2596ba4a95
67
.github/workflows/mkdocs-manual.yml
vendored
Normal file
67
.github/workflows/mkdocs-manual.yml
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
name: Manual documentation update to sync a deployed Version with Master branch
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version string to use (e.g."v0.0.1")'
|
||||
required: true
|
||||
jobs:
|
||||
build:
|
||||
name: Manual documentation update to sync a deployed Version with Master branch
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
steps:
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install git+https://${{ secrets.GH_TOKEN }}@github.com/lensapp/mkdocs-material-insiders.git
|
||||
pip install mike
|
||||
|
||||
- name: Checkout Version from lens
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: '${{ github.event.inputs.version }}'
|
||||
|
||||
- name: Using Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Generate Extensions API Reference using typedocs
|
||||
run: |
|
||||
yarn install
|
||||
yarn typedocs-extensions-api
|
||||
|
||||
- name: Checkout master branch from lens
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: 'master'
|
||||
ref: 'master'
|
||||
|
||||
- name: Bring in latest mkdocs.yml from master
|
||||
run: |
|
||||
cp -p ./master/mkdocs.yml .
|
||||
cp -p ./master/docs/stylesheets/extra.css ./docs/stylesheets/extra.css
|
||||
rm -fr ./docs/clusters ./docs/contributing ./docs/faq ./docs/getting-started ./docs/helm ./docs/support ./docs/supporting
|
||||
sed -i '/Protocol Handlers/d' ./mkdocs.yml
|
||||
sed -i '/IPC/d' ./mkdocs.yml
|
||||
sed -i 's#../../clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/extensions/get-started/your-first-extension.md
|
||||
sed -i 's#clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/README.md
|
||||
sed -i 's#../../contributing/README.md#https://docs.k8slens.dev/latest/contributing/#g' ./docs/extensions/guides/generator.md
|
||||
|
||||
- name: git config
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
|
||||
- name: mkdocs deploy new release
|
||||
run: |
|
||||
mike deploy --push --force ${{ github.event.inputs.version }}
|
||||
3
Makefile
3
Makefile
@ -68,8 +68,9 @@ integration-win: binaries/client build-extension-types build-extensions
|
||||
yarn integration
|
||||
|
||||
.PHONY: build
|
||||
build: node_modules binaries/client build-extensions
|
||||
build: node_modules binaries/client
|
||||
yarn run npm:fix-build-version
|
||||
$(MAKE) build-extensions
|
||||
yarn run compile
|
||||
ifeq "$(DETECTED_OS)" "Windows"
|
||||
yarn run electron-builder --publish onTag --x64 --ia32
|
||||
|
||||
47
mkdocs.yml
47
mkdocs.yml
@ -1,5 +1,5 @@
|
||||
site_name: Lens Extension API
|
||||
site_description: Documentation for Lens Extension API.
|
||||
site_name: Lens Extension Development
|
||||
site_description: Documentation for Lens Extension Development and API.
|
||||
site_author: Mirantis, Inc.
|
||||
site_url: https://api-docs.k8slens.dev
|
||||
docs_dir: docs/
|
||||
@ -12,28 +12,27 @@ google_analytics:
|
||||
- auto
|
||||
nav:
|
||||
- Overview: README.md
|
||||
- Extension Development:
|
||||
- Getting Started:
|
||||
- Overview: extensions/get-started/overview.md
|
||||
- Your First Extension: extensions/get-started/your-first-extension.md
|
||||
- Extension Anatomy: extensions/get-started/anatomy.md
|
||||
- Wrapping Up: extensions/get-started/wrapping-up.md
|
||||
- Extension Capabilities:
|
||||
- Common Capabilities: extensions/capabilities/common-capabilities.md
|
||||
- Styling: extensions/capabilities/styling.md
|
||||
- Extension Guides:
|
||||
- Overview: extensions/guides/README.md
|
||||
- Generator: extensions/guides/generator.md
|
||||
- Main Extension: extensions/guides/main-extension.md
|
||||
- Renderer Extension: extensions/guides/renderer-extension.md
|
||||
- Stores: extensions/guides/stores.md
|
||||
- Working with MobX: extensions/guides/working-with-mobx.md
|
||||
- Protocol Handlers: extensions/guides/protocol-handlers.md
|
||||
- IPC: extensions/guides/ipc.md
|
||||
- Testing and Publishing:
|
||||
- Testing Extensions: extensions/testing-and-publishing/testing.md
|
||||
- Publishing Extensions: extensions/testing-and-publishing/publishing.md
|
||||
- API Reference: extensions/api/README.md
|
||||
- Getting Started:
|
||||
- Overview: extensions/get-started/overview.md
|
||||
- Your First Extension: extensions/get-started/your-first-extension.md
|
||||
- Extension Anatomy: extensions/get-started/anatomy.md
|
||||
- Wrapping Up: extensions/get-started/wrapping-up.md
|
||||
- Extension Capabilities:
|
||||
- Common Capabilities: extensions/capabilities/common-capabilities.md
|
||||
- Styling: extensions/capabilities/styling.md
|
||||
- Extension Guides:
|
||||
- Overview: extensions/guides/README.md
|
||||
- Generator: extensions/guides/generator.md
|
||||
- Main Extension: extensions/guides/main-extension.md
|
||||
- Renderer Extension: extensions/guides/renderer-extension.md
|
||||
- Stores: extensions/guides/stores.md
|
||||
- Working with MobX: extensions/guides/working-with-mobx.md
|
||||
- Protocol Handlers: extensions/guides/protocol-handlers.md
|
||||
- IPC: extensions/guides/ipc.md
|
||||
- Testing and Publishing:
|
||||
- Testing Extensions: extensions/testing-and-publishing/testing.md
|
||||
- Publishing Extensions: extensions/testing-and-publishing/publishing.md
|
||||
- API Reference: extensions/api/README.md
|
||||
theme:
|
||||
name: 'material'
|
||||
highlightjs: true
|
||||
|
||||
@ -250,7 +250,7 @@
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||
"@testing-library/jest-dom": "^5.11.10",
|
||||
"@testing-library/jest-dom": "^5.13.0",
|
||||
"@testing-library/react": "^11.2.6",
|
||||
"@types/byline": "^4.2.32",
|
||||
"@types/chart.js": "^2.9.21",
|
||||
@ -312,7 +312,7 @@
|
||||
"color": "^3.1.2",
|
||||
"concurrently": "^5.2.0",
|
||||
"css-element-queries": "^1.2.3",
|
||||
"css-loader": "^3.5.3",
|
||||
"css-loader": "^5.2.6",
|
||||
"deepdash": "^5.3.5",
|
||||
"dompurify": "^2.0.11",
|
||||
"electron": "^12.0.10",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ ${git branch --show-current} =~ ^release/v ]]
|
||||
if [[ `git branch --show-current` =~ ^release/v ]]
|
||||
then
|
||||
VERSION_STRING=$(cat package.json | jq '.version' -r | xargs printf "v%s")
|
||||
git tag ${VERSION_STRING}
|
||||
|
||||
@ -101,20 +101,18 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
||||
}
|
||||
|
||||
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
||||
context.menuItems = [
|
||||
{
|
||||
if (!this.metadata.source || this.metadata.source === "local") {
|
||||
context.menuItems.push({
|
||||
title: "Settings",
|
||||
icon: "edit",
|
||||
onlyVisibleForSource: "local",
|
||||
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
if (this.metadata.labels["file"]?.startsWith(ClusterStore.storedKubeConfigFolder)) {
|
||||
context.menuItems.push({
|
||||
title: "Delete",
|
||||
icon: "delete",
|
||||
onlyVisibleForSource: "local",
|
||||
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
|
||||
confirm: {
|
||||
message: `Remove Kubernetes Cluster "${this.metadata.name} from ${productName}?`
|
||||
|
||||
@ -104,10 +104,6 @@ export interface CatalogEntityContextMenu {
|
||||
* Menu icon
|
||||
*/
|
||||
icon?: string;
|
||||
/**
|
||||
* Show only if empty or if value matches with entity.metadata.source
|
||||
*/
|
||||
onlyVisibleForSource?: string;
|
||||
/**
|
||||
* OnClick handler
|
||||
*/
|
||||
|
||||
528
src/common/utils/__tests__/hash-set.test.ts
Normal file
528
src/common/utils/__tests__/hash-set.test.ts
Normal file
@ -0,0 +1,528 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { HashSet, ObservableHashSet } from "../hash-set";
|
||||
|
||||
describe("ObservableHashSet<T>", () => {
|
||||
it("should not throw on creation", () => {
|
||||
expect(() => new ObservableHashSet<{ a: number }>([], item => item.a.toString())).not.toThrowError();
|
||||
});
|
||||
|
||||
it("should be initializable", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.size).toBe(4);
|
||||
});
|
||||
|
||||
it("has should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
expect(res.has({ a: 5 })).toBe(false);
|
||||
});
|
||||
|
||||
it("forEach should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
let a = 1;
|
||||
|
||||
res.forEach((item) => {
|
||||
expect(item.a).toEqual(a++);
|
||||
});
|
||||
});
|
||||
|
||||
it("delete should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
expect(res.delete({ a: 1 })).toBe(true);
|
||||
expect(res.has({ a: 1 })).toBe(false);
|
||||
|
||||
expect(res.has({ a: 5 })).toBe(false);
|
||||
expect(res.delete({ a: 5 })).toBe(false);
|
||||
expect(res.has({ a: 5 })).toBe(false);
|
||||
});
|
||||
|
||||
it("toggle should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
res.toggle({ a: 1 });
|
||||
expect(res.has({ a: 1 })).toBe(false);
|
||||
|
||||
expect(res.has({ a: 6 })).toBe(false);
|
||||
res.toggle({ a: 6 });
|
||||
expect(res.has({ a: 6 })).toBe(true);
|
||||
});
|
||||
|
||||
it("add should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 6 })).toBe(false);
|
||||
res.add({ a: 6 });
|
||||
expect(res.has({ a: 6 })).toBe(true);
|
||||
});
|
||||
|
||||
it("add should treat the hash to be the same as equality", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1, foobar: "hello" },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
res.add({ a: 1, foobar: "goodbye" });
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
it("clear should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.size).toBe(4);
|
||||
res.clear();
|
||||
expect(res.size).toBe(0);
|
||||
});
|
||||
|
||||
it("replace should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.size).toBe(4);
|
||||
res.replace([{ a: 13 }]);
|
||||
expect(res.size).toBe(1);
|
||||
expect(res.has({ a: 1 })).toBe(false);
|
||||
expect(res.has({ a: 13 })).toBe(true);
|
||||
});
|
||||
|
||||
it("toJSON should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.toJSON()).toStrictEqual([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("values should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
const iter = res.values();
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 1 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 2 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 3 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 4 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: undefined,
|
||||
done: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keys should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
const iter = res.keys();
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 1 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 2 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 3 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 4 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: undefined,
|
||||
done: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("entries should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
const iter = res.entries();
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 1 }, { a: 1 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 2 }, { a: 2 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 3 }, { a: 3 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 4 }, { a: 4 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: undefined,
|
||||
done: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("HashSet<T>", () => {
|
||||
it("should not throw on creation", () => {
|
||||
expect(() => new HashSet<{ a: number }>([], item => item.a.toString())).not.toThrowError();
|
||||
});
|
||||
|
||||
it("should be initializable", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.size).toBe(4);
|
||||
});
|
||||
|
||||
it("has should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
expect(res.has({ a: 5 })).toBe(false);
|
||||
});
|
||||
|
||||
it("forEach should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
let a = 1;
|
||||
|
||||
res.forEach((item) => {
|
||||
expect(item.a).toEqual(a++);
|
||||
});
|
||||
});
|
||||
|
||||
it("delete should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
expect(res.delete({ a: 1 })).toBe(true);
|
||||
expect(res.has({ a: 1 })).toBe(false);
|
||||
|
||||
expect(res.has({ a: 5 })).toBe(false);
|
||||
expect(res.delete({ a: 5 })).toBe(false);
|
||||
expect(res.has({ a: 5 })).toBe(false);
|
||||
});
|
||||
|
||||
it("toggle should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
res.toggle({ a: 1 });
|
||||
expect(res.has({ a: 1 })).toBe(false);
|
||||
|
||||
expect(res.has({ a: 6 })).toBe(false);
|
||||
res.toggle({ a: 6 });
|
||||
expect(res.has({ a: 6 })).toBe(true);
|
||||
});
|
||||
|
||||
it("add should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 6 })).toBe(false);
|
||||
res.add({ a: 6 });
|
||||
expect(res.has({ a: 6 })).toBe(true);
|
||||
});
|
||||
|
||||
it("add should treat the hash to be the same as equality", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1, foobar: "hello" },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
res.add({ a: 1, foobar: "goodbye" });
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
it("clear should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.size).toBe(4);
|
||||
res.clear();
|
||||
expect(res.size).toBe(0);
|
||||
});
|
||||
|
||||
it("replace should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.size).toBe(4);
|
||||
res.replace([{ a: 13 }]);
|
||||
expect(res.size).toBe(1);
|
||||
expect(res.has({ a: 1 })).toBe(false);
|
||||
expect(res.has({ a: 13 })).toBe(true);
|
||||
});
|
||||
|
||||
it("toJSON should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.toJSON()).toStrictEqual([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("values should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
const iter = res.values();
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 1 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 2 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 3 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 4 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: undefined,
|
||||
done: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keys should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
const iter = res.keys();
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 1 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 2 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 3 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 4 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: undefined,
|
||||
done: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("entries should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
const iter = res.entries();
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 1 }, { a: 1 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 2 }, { a: 2 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 3 }, { a: 3 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 4 }, { a: 4 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: undefined,
|
||||
done: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
55
src/common/utils/__tests__/n-fircate.test.ts
Normal file
55
src/common/utils/__tests__/n-fircate.test.ts
Normal file
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { nFircate } from "../n-fircate";
|
||||
|
||||
describe("nFircate", () => {
|
||||
it("should produce an empty array if no parts are provided", () => {
|
||||
expect(nFircate([{ a: 1 }, { a: 2 }], "a", []).length).toBe(0);
|
||||
});
|
||||
|
||||
it("should ignore non-matching parts", () => {
|
||||
const res = nFircate([{ a: 1 }, { a: 2 }], "a", [1]);
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0].length).toBe(1);
|
||||
});
|
||||
|
||||
it("should include all matching parts in each type", () => {
|
||||
const res = nFircate([{ a: 1, b: "a" }, { a: 2, b: "b" }, { a: 1, b: "c" }], "a", [1, 2]);
|
||||
|
||||
expect(res.length).toBe(2);
|
||||
expect(res[0].length).toBe(2);
|
||||
expect(res[0][0].b).toBe("a");
|
||||
expect(res[0][1].b).toBe("c");
|
||||
expect(res[1].length).toBe(1);
|
||||
expect(res[1][0].b).toBe("b");
|
||||
});
|
||||
|
||||
it("should throw a type error if the same part is provided more than once", () => {
|
||||
try {
|
||||
nFircate([{ a: 1, b: "a" }, { a: 2, b: "b" }, { a: 1, b: "c" }], "a", [1, 2, 1]);
|
||||
fail("Expected error");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TypeError);
|
||||
}
|
||||
});
|
||||
});
|
||||
260
src/common/utils/hash-set.ts
Normal file
260
src/common/utils/hash-set.ts
Normal file
@ -0,0 +1,260 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { action, IInterceptable, IInterceptor, IListenable, ISetWillChange, observable, ObservableMap, ObservableSet } from "mobx";
|
||||
|
||||
export function makeIterableIterator<T>(iterator: Iterator<T>): IterableIterator<T> {
|
||||
(iterator as IterableIterator<T>)[Symbol.iterator] = () => iterator as IterableIterator<T>;
|
||||
|
||||
return iterator as IterableIterator<T>;
|
||||
}
|
||||
|
||||
export class HashSet<T> implements Set<T> {
|
||||
#hashmap: Map<string, T>;
|
||||
|
||||
constructor(initialValues: Iterable<T>, protected hasher: (item: T) => string) {
|
||||
this.#hashmap = new Map<string, T>(Array.from(initialValues, value => [this.hasher(value), value]));
|
||||
}
|
||||
|
||||
replace(other: ObservableHashSet<T> | ObservableSet<T> | Set<T> | readonly T[]): this {
|
||||
if (other === null || other === undefined) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!(Array.isArray(other) || other instanceof Set || other instanceof ObservableHashSet || other instanceof ObservableSet)) {
|
||||
throw new Error(`ObservableHashSet: Cannot initialize set from ${other}`);
|
||||
}
|
||||
|
||||
this.clear();
|
||||
|
||||
for (const value of other) {
|
||||
this.add(value);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.#hashmap.clear();
|
||||
}
|
||||
|
||||
add(value: T): this {
|
||||
this.#hashmap.set(this.hasher(value), value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
toggle(value: T): void {
|
||||
const hash = this.hasher(value);
|
||||
|
||||
if (this.#hashmap.has(hash)) {
|
||||
this.#hashmap.delete(hash);
|
||||
} else {
|
||||
this.#hashmap.set(hash, value);
|
||||
}
|
||||
}
|
||||
|
||||
delete(value: T): boolean {
|
||||
return this.#hashmap.delete(this.hasher(value));
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: T, key: T, set: Set<T>) => void, thisArg?: any): void {
|
||||
this.#hashmap.forEach(value => callbackfn(value, value, thisArg ?? this));
|
||||
}
|
||||
|
||||
has(value: T): boolean {
|
||||
return this.#hashmap.has(this.hasher(value));
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.#hashmap.size;
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[T, T]> {
|
||||
let nextIndex = 0;
|
||||
const keys = Array.from(this.keys());
|
||||
const values = Array.from(this.values());
|
||||
|
||||
return makeIterableIterator<[T, T]>({
|
||||
next() {
|
||||
const index = nextIndex++;
|
||||
|
||||
return index < values.length
|
||||
? { value: [keys[index], values[index]], done: false }
|
||||
: { done: true, value: undefined };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
keys(): IterableIterator<T> {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
values(): IterableIterator<T> {
|
||||
let nextIndex = 0;
|
||||
const observableValues = Array.from(this.#hashmap.values());
|
||||
|
||||
return makeIterableIterator<T>({
|
||||
next: () => {
|
||||
return nextIndex < observableValues.length
|
||||
? { value: observableValues[nextIndex++], done: false }
|
||||
: { done: true, value: undefined };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<T> {
|
||||
return this.#hashmap.values();
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag](): string {
|
||||
return "Set";
|
||||
}
|
||||
|
||||
toJSON(): T[] {
|
||||
return Array.from(this);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return "[object Set]";
|
||||
}
|
||||
}
|
||||
|
||||
export class ObservableHashSet<T> implements Set<T>, IInterceptable<ISetWillChange>, IListenable {
|
||||
#hashmap: ObservableMap<string, T>;
|
||||
|
||||
get interceptors_(): IInterceptor<ISetWillChange<T>>[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
get changeListeners_(): Function[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
constructor(initialValues: Iterable<T>, protected hasher: (item: T) => string) {
|
||||
this.#hashmap = observable.map<string, T>(Array.from(initialValues, value => [this.hasher(value), value]), undefined);
|
||||
}
|
||||
|
||||
@action
|
||||
replace(other: ObservableHashSet<T> | ObservableSet<T> | Set<T> | readonly T[]): this {
|
||||
if (other === null || other === undefined) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!(Array.isArray(other) || other instanceof Set || other instanceof ObservableHashSet || other instanceof ObservableSet)) {
|
||||
throw new Error(`ObservableHashSet: Cannot initialize set from ${other}`);
|
||||
}
|
||||
|
||||
this.clear();
|
||||
|
||||
for (const value of other) {
|
||||
this.add(value);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.#hashmap.clear();
|
||||
}
|
||||
|
||||
add(value: T): this {
|
||||
this.#hashmap.set(this.hasher(value), value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@action
|
||||
toggle(value: T): void {
|
||||
const hash = this.hasher(value);
|
||||
|
||||
if (this.#hashmap.has(hash)) {
|
||||
this.#hashmap.delete(hash);
|
||||
} else {
|
||||
this.#hashmap.set(hash, value);
|
||||
}
|
||||
}
|
||||
|
||||
delete(value: T): boolean {
|
||||
return this.#hashmap.delete(this.hasher(value));
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: T, key: T, set: Set<T>) => void, thisArg?: any): void {
|
||||
this.#hashmap.forEach(value => callbackfn(value, value, thisArg ?? this));
|
||||
}
|
||||
|
||||
has(value: T): boolean {
|
||||
return this.#hashmap.has(this.hasher(value));
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.#hashmap.size;
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[T, T]> {
|
||||
let nextIndex = 0;
|
||||
const keys = Array.from(this.keys());
|
||||
const values = Array.from(this.values());
|
||||
|
||||
return makeIterableIterator<[T, T]>({
|
||||
next() {
|
||||
const index = nextIndex++;
|
||||
|
||||
return index < values.length
|
||||
? { value: [keys[index], values[index]], done: false }
|
||||
: { done: true, value: undefined };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
keys(): IterableIterator<T> {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
values(): IterableIterator<T> {
|
||||
let nextIndex = 0;
|
||||
const observableValues = Array.from(this.#hashmap.values());
|
||||
|
||||
return makeIterableIterator<T>({
|
||||
next: () => {
|
||||
return nextIndex < observableValues.length
|
||||
? { value: observableValues[nextIndex++], done: false }
|
||||
: { done: true, value: undefined };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<T> {
|
||||
return this.#hashmap.values();
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag](): string {
|
||||
return "Set";
|
||||
}
|
||||
|
||||
toJSON(): T[] {
|
||||
return Array.from(this);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return "[object ObservableSet]";
|
||||
}
|
||||
}
|
||||
@ -29,17 +29,17 @@ export * from "./app-version";
|
||||
export * from "./autobind";
|
||||
export * from "./base64";
|
||||
export * from "./camelCase";
|
||||
export * from "./toJS";
|
||||
export * from "./cloneJson";
|
||||
export * from "./debouncePromise";
|
||||
export * from "./defineGlobal";
|
||||
export * from "./delay";
|
||||
export * from "./disposer";
|
||||
export * from "./disposer";
|
||||
export * from "./downloadFile";
|
||||
export * from "./escapeRegExp";
|
||||
export * from "./extended-map";
|
||||
export * from "./getRandId";
|
||||
export * from "./hash-set";
|
||||
export * from "./n-fircate";
|
||||
export * from "./openExternal";
|
||||
export * from "./paths";
|
||||
export * from "./reject-promise";
|
||||
@ -47,6 +47,7 @@ export * from "./singleton";
|
||||
export * from "./splitArray";
|
||||
export * from "./tar";
|
||||
export * from "./toggle-set";
|
||||
export * from "./toJS";
|
||||
export * from "./type-narrowing";
|
||||
|
||||
import * as iter from "./iter";
|
||||
|
||||
52
src/common/utils/n-fircate.ts
Normal file
52
src/common/utils/n-fircate.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Split an iterable into several arrays with matching fields
|
||||
* @param from The iterable of items to split up
|
||||
* @param field The field of each item to split over
|
||||
* @param parts What each array will be filtered to
|
||||
* @returns A `parts.length` tuple of `T[]` where each array has matching `field` values
|
||||
*/
|
||||
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: []): [];
|
||||
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: [T[typeof field]]): [T[]];
|
||||
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: [T[typeof field], T[typeof field]]): [T[], T[]];
|
||||
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: [T[typeof field], T[typeof field], T[typeof field]]): [T[], T[], T[]];
|
||||
|
||||
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: T[typeof field][]): T[][] {
|
||||
if (new Set(parts).size !== parts.length) {
|
||||
throw new TypeError("Duplicate parts entries");
|
||||
}
|
||||
|
||||
const res = Array.from(parts, () => [] as T[]);
|
||||
|
||||
for (const item of from) {
|
||||
const index = parts.indexOf(item[field]);
|
||||
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
res[index].push(item);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
@ -21,6 +21,7 @@
|
||||
|
||||
// App's common configuration for any process (main, renderer, build pipeline, etc.)
|
||||
import path from "path";
|
||||
import { SemVer } from "semver";
|
||||
import packageInfo from "../../package.json";
|
||||
import { defineGlobal } from "./utils/defineGlobal";
|
||||
|
||||
@ -66,4 +67,6 @@ export const apiKubePrefix = "/api-kube" as string; // k8s cluster apis
|
||||
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string;
|
||||
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI" as string;
|
||||
export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string;
|
||||
|
||||
export const appSemVer = new SemVer(packageInfo.version);
|
||||
export const docsUrl = `https://docs.k8slens.dev/main/` as string;
|
||||
|
||||
@ -93,6 +93,7 @@ describe("ExtensionDiscovery", () => {
|
||||
id: path.normalize("node_modules/my-extension/package.json"),
|
||||
isBundled: false,
|
||||
isEnabled: false,
|
||||
isCompatible: false,
|
||||
manifest: {
|
||||
name: "my-extension",
|
||||
},
|
||||
|
||||
@ -38,7 +38,8 @@ describe("lens extension", () => {
|
||||
absolutePath: "/absolute/fake/",
|
||||
manifestPath: "/this/is/fake/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: true
|
||||
isEnabled: true,
|
||||
isCompatible: true
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -30,10 +30,13 @@ import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } fr
|
||||
import { Singleton, toJS } from "../common/utils";
|
||||
import logger from "../main/logger";
|
||||
import { ExtensionInstallationStateStore } from "../renderer/components/+extensions/extension-install.store";
|
||||
import { extensionInstaller, PackageJson } from "./extension-installer";
|
||||
import { extensionInstaller } from "./extension-installer";
|
||||
import { ExtensionsStore } from "./extensions-store";
|
||||
import { ExtensionLoader } from "./extension-loader";
|
||||
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
||||
import type { PackageJson } from "type-fest";
|
||||
import semver from "semver";
|
||||
import { appSemVer } from "../common/vars";
|
||||
import { isProduction } from "../common/vars";
|
||||
|
||||
export interface InstalledExtension {
|
||||
@ -48,6 +51,7 @@ export interface InstalledExtension {
|
||||
// Absolute to the symlinked package.json file
|
||||
readonly manifestPath: string;
|
||||
readonly isBundled: boolean; // defined in project root's package.json
|
||||
readonly isCompatible: boolean;
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
@ -349,12 +353,17 @@ export class ExtensionDiscovery extends Singleton {
|
||||
*/
|
||||
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension | null> {
|
||||
try {
|
||||
const manifest = await fse.readJson(manifestPath);
|
||||
const manifest = await fse.readJson(manifestPath) as LensExtensionManifest;
|
||||
const installedManifestPath = this.getInstalledManifestPath(manifest.name);
|
||||
const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath);
|
||||
const extensionDir = path.dirname(manifestPath);
|
||||
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
||||
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
||||
let isCompatible = isBundled;
|
||||
|
||||
if (manifest.engines?.lens) {
|
||||
isCompatible = semver.satisfies(appSemVer, manifest.engines.lens);
|
||||
}
|
||||
|
||||
return {
|
||||
id: installedManifestPath,
|
||||
@ -362,7 +371,8 @@ export class ExtensionDiscovery extends Singleton {
|
||||
manifestPath: installedManifestPath,
|
||||
manifest,
|
||||
isBundled,
|
||||
isEnabled
|
||||
isEnabled,
|
||||
isCompatible
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.code === "ENOTDIR") {
|
||||
|
||||
@ -25,17 +25,10 @@ import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import logger from "../main/logger";
|
||||
import { extensionPackagesRoot } from "./extension-loader";
|
||||
import type { PackageJson } from "type-fest";
|
||||
|
||||
const logModule = "[EXTENSION-INSTALLER]";
|
||||
|
||||
type Dependencies = {
|
||||
[name: string]: string;
|
||||
};
|
||||
|
||||
// Type for the package.json file that is written by ExtensionInstaller
|
||||
export type PackageJson = {
|
||||
dependencies: Dependencies;
|
||||
};
|
||||
|
||||
/**
|
||||
* Installs dependencies for extensions
|
||||
|
||||
@ -303,7 +303,7 @@ export class ExtensionLoader extends Singleton {
|
||||
for (const [extId, extension] of installedExtensions) {
|
||||
const alreadyInit = this.instances.has(extId);
|
||||
|
||||
if (extension.isEnabled && !alreadyInit) {
|
||||
if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
|
||||
try {
|
||||
const LensExtensionClass = this.requireExtension(extension);
|
||||
|
||||
|
||||
@ -24,18 +24,17 @@ import { action, observable, makeObservable } from "mobx";
|
||||
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
||||
import logger from "../main/logger";
|
||||
import type { ProtocolHandlerRegistration } from "./registries";
|
||||
import type { PackageJson } from "type-fest";
|
||||
import { Disposer, disposer } from "../common/utils";
|
||||
|
||||
export type LensExtensionId = string; // path to manifest (package.json)
|
||||
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
|
||||
|
||||
export interface LensExtensionManifest {
|
||||
export interface LensExtensionManifest extends PackageJson {
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
main?: string; // path to %ext/dist/main.js
|
||||
renderer?: string; // path to %ext/dist/renderer.js
|
||||
lens?: object; // fixme: add more required fields for validation
|
||||
}
|
||||
|
||||
export const Disposers = Symbol();
|
||||
@ -91,7 +90,7 @@ export class LensExtension {
|
||||
try {
|
||||
await this.onActivate();
|
||||
this.isEnabled = true;
|
||||
|
||||
|
||||
this[Disposers].push(...await register(this));
|
||||
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
|
||||
} catch (error) {
|
||||
|
||||
@ -40,7 +40,8 @@ describe("getPageUrl", () => {
|
||||
absolutePath: "/absolute/fake/",
|
||||
manifestPath: "/this/is/fake/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: true
|
||||
isEnabled: true,
|
||||
isCompatible: true
|
||||
});
|
||||
globalPageRegistry.add({
|
||||
id: "page-with-params",
|
||||
@ -107,7 +108,8 @@ describe("globalPageRegistry", () => {
|
||||
absolutePath: "/absolute/fake/",
|
||||
manifestPath: "/this/is/fake/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: true
|
||||
isEnabled: true,
|
||||
isCompatible: true
|
||||
});
|
||||
globalPageRegistry.add([
|
||||
{
|
||||
|
||||
@ -86,8 +86,8 @@ export type { PersistentVolumesStore } from "../../renderer/components/+storage-
|
||||
export type { VolumeClaimStore } from "../../renderer/components/+storage-volume-claims/volume-claim.store";
|
||||
export type { StorageClassStore } from "../../renderer/components/+storage-classes/storage-class.store";
|
||||
export type { NamespaceStore } from "../../renderer/components/+namespaces/namespace.store";
|
||||
export type { ServiceAccountsStore } from "../../renderer/components/+user-management-service-accounts/service-accounts.store";
|
||||
export type { RolesStore } from "../../renderer/components/+user-management-roles/roles.store";
|
||||
export type { RoleBindingsStore } from "../../renderer/components/+user-management-roles-bindings/role-bindings.store";
|
||||
export type { ServiceAccountsStore } from "../../renderer/components/+user-management/+service-accounts/store";
|
||||
export type { RolesStore } from "../../renderer/components/+user-management/+roles/store";
|
||||
export type { RoleBindingsStore } from "../../renderer/components/+user-management/+role-bindings/store";
|
||||
export type { CRDStore } from "../../renderer/components/+custom-resources/crd.store";
|
||||
export type { CRDResourceStore } from "../../renderer/components/+custom-resources/crd-resource.store";
|
||||
|
||||
@ -87,6 +87,7 @@ describe("protocol router tests", () => {
|
||||
},
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
isCompatible: true,
|
||||
absolutePath: "/foo/bar",
|
||||
});
|
||||
const lpr = LensProtocolRouterMain.getInstance();
|
||||
@ -165,6 +166,7 @@ describe("protocol router tests", () => {
|
||||
},
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
isCompatible: true,
|
||||
absolutePath: "/foo/bar",
|
||||
});
|
||||
|
||||
@ -206,6 +208,7 @@ describe("protocol router tests", () => {
|
||||
},
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
isCompatible: true,
|
||||
absolutePath: "/foo/bar",
|
||||
});
|
||||
|
||||
@ -230,6 +233,7 @@ describe("protocol router tests", () => {
|
||||
},
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
isCompatible: true,
|
||||
absolutePath: "/foo/bar",
|
||||
});
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
import type { Cluster } from "../cluster";
|
||||
import { Kubectl } from "../kubectl";
|
||||
import type * as WebSocket from "ws";
|
||||
import shellEnv from "shell-env";
|
||||
import { shellEnv } from "../utils/shell-env";
|
||||
import { app } from "electron";
|
||||
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
|
||||
import path from "path";
|
||||
|
||||
@ -19,10 +19,9 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import shellEnv from "shell-env";
|
||||
import { shellEnv } from "./utils/shell-env";
|
||||
import os from "os";
|
||||
import { app } from "electron";
|
||||
import logger from "./logger";
|
||||
|
||||
interface Env {
|
||||
[key: string]: string;
|
||||
@ -37,16 +36,7 @@ export async function shellSync() {
|
||||
const { shell } = os.userInfo();
|
||||
let envVars = {};
|
||||
|
||||
try {
|
||||
envVars = await Promise.race([
|
||||
shellEnv(shell),
|
||||
new Promise((_resolve, reject) => setTimeout(() => {
|
||||
reject(new Error("Resolving shell environment is taking very long. Please review your shell configuration."));
|
||||
}, 5_000))
|
||||
]);
|
||||
} catch (error) {
|
||||
logger.error(`shellEnv: ${error}`);
|
||||
}
|
||||
envVars = await shellEnv(shell);
|
||||
|
||||
const env: Env = JSON.parse(JSON.stringify(envVars));
|
||||
|
||||
|
||||
66
src/main/utils/shell-env.ts
Normal file
66
src/main/utils/shell-env.ts
Normal file
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import shellEnvironment from "shell-env";
|
||||
import logger from "../logger";
|
||||
|
||||
export interface EnvironmentVariables {
|
||||
readonly [key: string]: string;
|
||||
}
|
||||
|
||||
let shellSyncFailed = false;
|
||||
|
||||
/**
|
||||
* Attempts to get the shell environment per the user's existing startup scripts.
|
||||
* If the environment can't be retrieved after 5 seconds an error message is logged.
|
||||
* Subsequent calls after such a timeout simply log an error message without trying
|
||||
* to get the environment, unless forceRetry is true.
|
||||
* @param shell the shell to get the environment from
|
||||
* @param forceRetry if true will always try to get the environment, otherwise if
|
||||
* a previous call to this function failed then this call will fail too.
|
||||
* @returns object containing the shell's environment variables. An empty object is
|
||||
* returned if the call fails.
|
||||
*/
|
||||
export async function shellEnv(shell?: string, forceRetry = false) : Promise<EnvironmentVariables> {
|
||||
let envVars = {};
|
||||
|
||||
if (forceRetry) {
|
||||
shellSyncFailed = false;
|
||||
}
|
||||
|
||||
if (!shellSyncFailed) {
|
||||
try {
|
||||
envVars = await Promise.race([
|
||||
shellEnvironment(shell),
|
||||
new Promise((_resolve, reject) => setTimeout(() => {
|
||||
reject(new Error("Resolving shell environment is taking very long. Please review your shell configuration."));
|
||||
}, 5_000))
|
||||
]);
|
||||
} catch (error) {
|
||||
logger.error(`shellEnv: ${error}`);
|
||||
shellSyncFailed = true;
|
||||
}
|
||||
} else {
|
||||
logger.error("shellSync(): Resolving shell environment took too long. Please review your shell configuration.");
|
||||
}
|
||||
|
||||
return envVars;
|
||||
}
|
||||
@ -18,14 +18,39 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { RoleBinding } from "./role-binding.api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
export class ClusterRoleBinding extends RoleBinding {
|
||||
export type ClusterRoleBindingSubjectKind = "Group" | "ServiceAccount" | "User";
|
||||
|
||||
export interface ClusterRoleBindingSubject {
|
||||
kind: ClusterRoleBindingSubjectKind;
|
||||
name: string;
|
||||
apiGroup?: string;
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export interface ClusterRoleBinding {
|
||||
subjects?: ClusterRoleBindingSubject[];
|
||||
roleRef: {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiGroup?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class ClusterRoleBinding extends KubeObject {
|
||||
static kind = "ClusterRoleBinding";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings";
|
||||
|
||||
getSubjects() {
|
||||
return this.subjects || [];
|
||||
}
|
||||
|
||||
getSubjectNames(): string {
|
||||
return this.getSubjects().map(subject => subject.name).join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
export const clusterRoleBindingApi = new KubeApi({
|
||||
|
||||
@ -19,13 +19,26 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Role } from "./role.api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
export class ClusterRole extends Role {
|
||||
export interface ClusterRole {
|
||||
rules: {
|
||||
verbs: string[];
|
||||
apiGroups: string[];
|
||||
resources: string[];
|
||||
resourceNames?: string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export class ClusterRole extends KubeObject {
|
||||
static kind = "ClusterRole";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterroles";
|
||||
|
||||
getRules() {
|
||||
return this.rules || [];
|
||||
}
|
||||
}
|
||||
|
||||
export const clusterRoleApi = new KubeApi({
|
||||
|
||||
@ -24,15 +24,17 @@ import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
|
||||
export interface IRoleBindingSubject {
|
||||
kind: string;
|
||||
export type RoleBindingSubjectKind = "Group" | "ServiceAccount" | "User";
|
||||
|
||||
export interface RoleBindingSubject {
|
||||
kind: RoleBindingSubjectKind;
|
||||
name: string;
|
||||
namespace?: string;
|
||||
apiGroup?: string;
|
||||
}
|
||||
|
||||
export interface RoleBinding {
|
||||
subjects?: IRoleBindingSubject[];
|
||||
subjects?: RoleBindingSubject[];
|
||||
roleRef: {
|
||||
kind: string;
|
||||
name: string;
|
||||
|
||||
@ -78,32 +78,31 @@ export class CatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Comp
|
||||
return [];
|
||||
}
|
||||
|
||||
const menuItems = this.contextMenu.menuItems.filter((menuItem) => {
|
||||
return menuItem.icon && !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === entity.metadata.source;
|
||||
});
|
||||
const items: React.ReactChild[] = [];
|
||||
|
||||
const items = menuItems.map((menuItem, index) => {
|
||||
const props = menuItem.icon.includes("<svg") ? { svg: menuItem.icon } : { material: menuItem.icon };
|
||||
for (const menuItem of this.contextMenu.menuItems) {
|
||||
if (!menuItem.icon) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem key={index} onClick={() => this.onMenuItemClick(menuItem)}>
|
||||
const key = menuItem.icon.includes("<svg") ? "svg" : "material";
|
||||
|
||||
items.push(
|
||||
<MenuItem key={menuItem.title} onClick={() => this.onMenuItemClick(menuItem)}>
|
||||
<Icon
|
||||
title={menuItem.title}
|
||||
{...props}
|
||||
{...{ [key]: menuItem.icon }}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
items.unshift(
|
||||
items.push(
|
||||
<MenuItem key="add-to-hotbar" onClick={() => this.addToHotbar(entity) }>
|
||||
<Icon material="playlist_add" small title="Add to Hotbar" />
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
items.reverse();
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
@ -171,12 +171,10 @@ export class Catalog extends React.Component<Props> {
|
||||
}
|
||||
|
||||
renderItemMenu = (item: CatalogEntityItem) => {
|
||||
const menuItems = this.contextMenu.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === item.entity.metadata.source);
|
||||
|
||||
return (
|
||||
<MenuActions onOpen={() => item.onContextMenuOpen(this.contextMenu)}>
|
||||
{
|
||||
menuItems.map((menuItem, index) => (
|
||||
this.contextMenu.menuItems.map((menuItem, index) => (
|
||||
<MenuItem key={index} onClick={() => this.onMenuItemClick(menuItem)}>
|
||||
{menuItem.title}
|
||||
</MenuItem>
|
||||
|
||||
@ -76,7 +76,8 @@ describe("Extensions", () => {
|
||||
absolutePath: "/absolute/path",
|
||||
manifestPath: "/symlinked/path/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: true
|
||||
isEnabled: true,
|
||||
isCompatible: true
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -11,6 +11,10 @@
|
||||
color: var(--colorOk);
|
||||
}
|
||||
|
||||
.invalid {
|
||||
color: var(--colorWarning);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 0!important;
|
||||
}
|
||||
@ -22,4 +26,4 @@
|
||||
|
||||
.frozenRow {
|
||||
@apply opacity-30 pointer-events-none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,14 +39,18 @@ interface Props {
|
||||
uninstall: (extension: InstalledExtension) => void;
|
||||
}
|
||||
|
||||
function getStatus(isEnabled: boolean) {
|
||||
return isEnabled ? "Enabled" : "Disabled";
|
||||
function getStatus(extension: InstalledExtension) {
|
||||
if (!extension.isCompatible) {
|
||||
return "Incompatible";
|
||||
}
|
||||
|
||||
return extension.isEnabled ? "Enabled" : "Disabled";
|
||||
}
|
||||
|
||||
export const InstalledExtensions = observer(({ extensions, uninstall, enable, disable }: Props) => {
|
||||
const filters = [
|
||||
(extension: InstalledExtension) => extension.manifest.name,
|
||||
(extension: InstalledExtension) => getStatus(extension.isEnabled),
|
||||
(extension: InstalledExtension) => getStatus(extension),
|
||||
(extension: InstalledExtension) => extension.manifest.version,
|
||||
];
|
||||
|
||||
@ -87,7 +91,7 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di
|
||||
const data = useMemo(
|
||||
() => {
|
||||
return extensions.map(extension => {
|
||||
const { id, isEnabled, manifest } = extension;
|
||||
const { id, isEnabled, isCompatible, manifest } = extension;
|
||||
const { name, description, version } = manifest;
|
||||
const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id);
|
||||
|
||||
@ -102,29 +106,34 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di
|
||||
),
|
||||
version,
|
||||
status: (
|
||||
<div className={cssNames({[styles.enabled]: getStatus(isEnabled) == "Enabled"})}>
|
||||
{getStatus(isEnabled)}
|
||||
<div className={cssNames({[styles.enabled]: isEnabled, [styles.invalid]: !isCompatible})}>
|
||||
{getStatus(extension)}
|
||||
</div>
|
||||
),
|
||||
actions: (
|
||||
<MenuActions usePortal toolbar={false}>
|
||||
{isEnabled ? (
|
||||
<MenuItem
|
||||
disabled={isUninstalling}
|
||||
onClick={() => disable(id)}
|
||||
>
|
||||
<Icon material="unpublished"/>
|
||||
<span className="title" aria-disabled={isUninstalling}>Disable</span>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem
|
||||
disabled={isUninstalling}
|
||||
onClick={() => enable(id)}
|
||||
>
|
||||
<Icon material="check_circle"/>
|
||||
<span className="title" aria-disabled={isUninstalling}>Enable</span>
|
||||
</MenuItem>
|
||||
{ isCompatible && (
|
||||
<>
|
||||
{isEnabled ? (
|
||||
<MenuItem
|
||||
disabled={isUninstalling}
|
||||
onClick={() => disable(id)}
|
||||
>
|
||||
<Icon material="unpublished"/>
|
||||
<span className="title" aria-disabled={isUninstalling}>Disable</span>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem
|
||||
disabled={isUninstalling}
|
||||
onClick={() => enable(id)}
|
||||
>
|
||||
<Icon material="check_circle"/>
|
||||
<span className="title" aria-disabled={isUninstalling}>Enable</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
disabled={isUninstalling}
|
||||
onClick={() => uninstall(extension)}
|
||||
|
||||
@ -32,14 +32,12 @@ import { kubeWatchApi } from "../../api/kube-watch-api";
|
||||
|
||||
interface Props extends SelectProps {
|
||||
showIcons?: boolean;
|
||||
showClusterOption?: boolean; // show "Cluster" option on the top (default: false)
|
||||
showAllNamespacesOption?: boolean; // show "All namespaces" option on the top (default: false)
|
||||
customizeOptions?(options: SelectOption[]): SelectOption[];
|
||||
}
|
||||
|
||||
const defaultProps: Partial<Props> = {
|
||||
showIcons: true,
|
||||
showClusterOption: false,
|
||||
};
|
||||
|
||||
@observer
|
||||
@ -61,13 +59,11 @@ export class NamespaceSelect extends React.Component<Props> {
|
||||
}
|
||||
|
||||
@computed.struct get options(): SelectOption[] {
|
||||
const { customizeOptions, showClusterOption, showAllNamespacesOption } = this.props;
|
||||
const { customizeOptions, showAllNamespacesOption } = this.props;
|
||||
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() }));
|
||||
|
||||
if (showAllNamespacesOption) {
|
||||
options.unshift({ label: "All Namespaces", value: "" });
|
||||
} else if (showClusterOption) {
|
||||
options.unshift({ label: "Cluster", value: "" });
|
||||
}
|
||||
|
||||
if (customizeOptions) {
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
import { action, comparer, computed, IReactionDisposer, IReactionOptions, makeObservable, reaction, } from "mobx";
|
||||
import { autoBind, createStorage } from "../../utils";
|
||||
import { autoBind, createStorage, noop } from "../../utils";
|
||||
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
||||
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
@ -97,13 +97,16 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
return this.selectedNamespaces;
|
||||
}
|
||||
|
||||
getSubscribeApis() {
|
||||
// if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
|
||||
subscribe() {
|
||||
/**
|
||||
* if user has given static list of namespaces let's not start watches
|
||||
* because watch adds stuff that's not wanted or will just fail
|
||||
*/
|
||||
if (this.context?.cluster.accessibleNamespaces.length > 0) {
|
||||
return [];
|
||||
return noop;
|
||||
}
|
||||
|
||||
return super.getSubscribeApis();
|
||||
return super.subscribe();
|
||||
}
|
||||
|
||||
protected async loadItems(params: KubeObjectStoreLoadingParams) {
|
||||
|
||||
@ -1,318 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./add-role-binding-dialog.scss";
|
||||
|
||||
import React from "react";
|
||||
import { computed, observable, makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import type { IRoleBindingSubject, Role, RoleBinding, ServiceAccount } from "../../api/endpoints";
|
||||
import { Icon } from "../icon";
|
||||
import { Input } from "../input";
|
||||
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
import { Notifications } from "../notifications";
|
||||
import { rolesStore } from "../+user-management-roles/roles.store";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { serviceAccountsStore } from "../+user-management-service-accounts/service-accounts.store";
|
||||
import { roleBindingsStore } from "./role-bindings.store";
|
||||
import { showDetails } from "../kube-object";
|
||||
import type { KubeObjectStore } from "../../kube-object.store";
|
||||
|
||||
interface BindingSelectOption extends SelectOption {
|
||||
value: string; // binding name
|
||||
item?: ServiceAccount | any;
|
||||
subject?: IRoleBindingSubject; // used for new user/group when users-management-api not available
|
||||
}
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
const dialogState = observable.object({
|
||||
isOpen: false,
|
||||
data: null as RoleBinding,
|
||||
});
|
||||
|
||||
@observer
|
||||
export class AddRoleBindingDialog extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
static open(roleBinding?: RoleBinding) {
|
||||
dialogState.isOpen = true;
|
||||
dialogState.data = roleBinding;
|
||||
}
|
||||
|
||||
static close() {
|
||||
dialogState.isOpen = false;
|
||||
}
|
||||
|
||||
get roleBinding(): RoleBinding {
|
||||
return dialogState.data;
|
||||
}
|
||||
|
||||
@observable isLoading = false;
|
||||
@observable selectedRoleId = "";
|
||||
@observable useRoleForBindingName = true;
|
||||
@observable bindingName = ""; // new role-binding name
|
||||
@observable bindContext = ""; // empty value means "cluster-wide", otherwise bind to namespace
|
||||
@observable selectedAccounts = observable.array<ServiceAccount>([], { deep: false });
|
||||
|
||||
@computed get isEditing() {
|
||||
return !!this.roleBinding;
|
||||
}
|
||||
|
||||
@computed get selectedRole() {
|
||||
return rolesStore.items.find(role => role.getId() === this.selectedRoleId);
|
||||
}
|
||||
|
||||
@computed get selectedBindings() {
|
||||
return [
|
||||
...this.selectedAccounts,
|
||||
];
|
||||
}
|
||||
|
||||
close = () => {
|
||||
AddRoleBindingDialog.close();
|
||||
};
|
||||
|
||||
async loadData() {
|
||||
const stores: KubeObjectStore[] = [
|
||||
namespaceStore,
|
||||
rolesStore,
|
||||
serviceAccountsStore,
|
||||
];
|
||||
|
||||
this.isLoading = true;
|
||||
await Promise.all(stores.map(store => store.reloadAll()));
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
onOpen = async () => {
|
||||
await this.loadData();
|
||||
|
||||
if (this.roleBinding) {
|
||||
const { name, kind } = this.roleBinding.roleRef;
|
||||
const role = rolesStore.items.find(role => role.kind === kind && role.getName() === name);
|
||||
|
||||
if (role) {
|
||||
this.selectedRoleId = role.getId();
|
||||
this.bindContext = role.getNs() || "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
reset = () => {
|
||||
this.selectedRoleId = "";
|
||||
this.bindContext = "";
|
||||
this.selectedAccounts.clear();
|
||||
};
|
||||
|
||||
onBindContextChange = (namespace: string) => {
|
||||
this.bindContext = namespace;
|
||||
const roleContext = this.selectedRole && this.selectedRole.getNs() || "";
|
||||
|
||||
if (this.bindContext && this.bindContext !== roleContext) {
|
||||
this.selectedRoleId = ""; // reset previously selected role for specific context
|
||||
}
|
||||
};
|
||||
|
||||
createBindings = async () => {
|
||||
const { selectedRole, bindContext: namespace, selectedBindings, bindingName, useRoleForBindingName } = this;
|
||||
|
||||
const subjects = selectedBindings.map((item: KubeObject | IRoleBindingSubject) => {
|
||||
if (item instanceof KubeObject) {
|
||||
return {
|
||||
name: item.getName(),
|
||||
kind: item.kind,
|
||||
namespace: item.getNs(),
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
try {
|
||||
let roleBinding: RoleBinding;
|
||||
|
||||
if (this.isEditing) {
|
||||
roleBinding = await roleBindingsStore.updateSubjects({
|
||||
roleBinding: this.roleBinding,
|
||||
addSubjects: subjects,
|
||||
});
|
||||
} else {
|
||||
const name = useRoleForBindingName ? selectedRole.getName() : bindingName;
|
||||
|
||||
roleBinding = await roleBindingsStore.create({ name, namespace }, {
|
||||
subjects,
|
||||
roleRef: {
|
||||
name: selectedRole.getName(),
|
||||
kind: selectedRole.kind,
|
||||
}
|
||||
});
|
||||
}
|
||||
showDetails(roleBinding.selfLink);
|
||||
this.close();
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
@computed get roleOptions(): BindingSelectOption[] {
|
||||
let roles = rolesStore.items as Role[];
|
||||
|
||||
if (this.bindContext) {
|
||||
// show only cluster-roles or roles for selected context namespace
|
||||
roles = roles.filter(role => !role.getNs() || role.getNs() === this.bindContext);
|
||||
}
|
||||
|
||||
return roles.map(role => {
|
||||
const name = role.getName();
|
||||
const namespace = role.getNs();
|
||||
|
||||
return {
|
||||
value: role.getId(),
|
||||
label: name + (namespace ? ` (${namespace})` : "")
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@computed get serviceAccountOptions(): BindingSelectOption[] {
|
||||
return serviceAccountsStore.items.map(account => {
|
||||
const name = account.getName();
|
||||
const namespace = account.getNs();
|
||||
|
||||
return {
|
||||
item: account,
|
||||
value: name,
|
||||
label: <><Icon small material="account_box"/> {name} ({namespace})</>
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
renderContents() {
|
||||
const unwrapBindings = (options: BindingSelectOption[]) => options.map(option => option.item || option.subject);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Context"/>
|
||||
<NamespaceSelect
|
||||
showClusterOption
|
||||
themeName="light"
|
||||
isDisabled={this.isEditing}
|
||||
value={this.bindContext}
|
||||
onChange={({ value }) => this.onBindContextChange(value)}
|
||||
/>
|
||||
|
||||
<SubTitle title="Role"/>
|
||||
<Select
|
||||
key={this.selectedRoleId}
|
||||
themeName="light"
|
||||
placeholder="Select role.."
|
||||
isDisabled={this.isEditing}
|
||||
options={this.roleOptions}
|
||||
value={this.selectedRoleId}
|
||||
onChange={({ value }) => this.selectedRoleId = value}
|
||||
/>
|
||||
{
|
||||
!this.isEditing && (
|
||||
<>
|
||||
<Checkbox
|
||||
theme="light"
|
||||
label="Use same name for RoleBinding"
|
||||
value={this.useRoleForBindingName}
|
||||
onChange={v => this.useRoleForBindingName = v}
|
||||
/>
|
||||
{
|
||||
!this.useRoleForBindingName && (
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder="Name"
|
||||
disabled={this.isEditing}
|
||||
value={this.bindingName}
|
||||
onChange={v => this.bindingName = v}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<SubTitle title="Binding targets"/>
|
||||
<Select
|
||||
isMulti
|
||||
themeName="light"
|
||||
placeholder="Select service accounts"
|
||||
autoConvertOptions={false}
|
||||
options={this.serviceAccountOptions}
|
||||
onChange={(opts: BindingSelectOption[]) => {
|
||||
if (!opts) opts = [];
|
||||
this.selectedAccounts.replace(unwrapBindings(opts));
|
||||
}}
|
||||
maxMenuHeight={200}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ...dialogProps } = this.props;
|
||||
const { isEditing, roleBinding, selectedRole, selectedBindings } = this;
|
||||
const roleBindingName = roleBinding ? roleBinding.getName() : "";
|
||||
const header = (
|
||||
<h5>
|
||||
{roleBindingName
|
||||
? <>Edit RoleBinding <span className="name">{roleBindingName}</span></>
|
||||
: "Add RoleBinding"
|
||||
}
|
||||
</h5>
|
||||
);
|
||||
const disableNext = this.isLoading || !selectedRole || !selectedBindings.length;
|
||||
const nextLabel = isEditing ? "Update" : "Create";
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
className="AddRoleBindingDialog"
|
||||
isOpen={dialogState.isOpen}
|
||||
onOpen={this.onOpen}
|
||||
close={this.close}
|
||||
>
|
||||
<Wizard header={header} done={this.close}>
|
||||
<WizardStep
|
||||
nextLabel={nextLabel} next={this.createBindings}
|
||||
disabledNext={disableNext}
|
||||
loading={this.isLoading}
|
||||
>
|
||||
{this.renderContents()}
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,100 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import difference from "lodash/difference";
|
||||
import uniqBy from "lodash/uniqBy";
|
||||
import { clusterRoleBindingApi, IRoleBindingSubject, RoleBinding, roleBindingApi } from "../../api/endpoints";
|
||||
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
||||
import { autoBind } from "../../utils";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
|
||||
export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
||||
api = clusterRoleBindingApi;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getSubscribeApis() {
|
||||
return [clusterRoleBindingApi, roleBindingApi];
|
||||
}
|
||||
|
||||
protected sortItems(items: RoleBinding[]) {
|
||||
return super.sortItems(items, [
|
||||
roleBinding => roleBinding.kind,
|
||||
roleBinding => roleBinding.getName()
|
||||
]);
|
||||
}
|
||||
|
||||
protected loadItem(params: { name: string; namespace?: string }) {
|
||||
if (params.namespace) return roleBindingApi.get(params);
|
||||
|
||||
return clusterRoleBindingApi.get(params);
|
||||
}
|
||||
|
||||
protected async loadItems(params: KubeObjectStoreLoadingParams): Promise<RoleBinding[]> {
|
||||
const items = await Promise.all([
|
||||
super.loadItems({ ...params, api: clusterRoleBindingApi }),
|
||||
super.loadItems({ ...params, api: roleBindingApi }),
|
||||
]);
|
||||
|
||||
return items.flat();
|
||||
}
|
||||
|
||||
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<RoleBinding>) {
|
||||
if (params.namespace) {
|
||||
return roleBindingApi.create(params, data);
|
||||
} else {
|
||||
return clusterRoleBindingApi.create(params, data);
|
||||
}
|
||||
}
|
||||
|
||||
async updateSubjects(params: {
|
||||
roleBinding: RoleBinding;
|
||||
addSubjects?: IRoleBindingSubject[];
|
||||
removeSubjects?: IRoleBindingSubject[];
|
||||
}) {
|
||||
const { roleBinding, addSubjects, removeSubjects } = params;
|
||||
const currentSubjects = roleBinding.getSubjects();
|
||||
let newSubjects = currentSubjects;
|
||||
|
||||
if (addSubjects) {
|
||||
newSubjects = uniqBy(currentSubjects.concat(addSubjects), ({ kind, name, namespace }) => {
|
||||
return [kind, name, namespace].join("-");
|
||||
});
|
||||
} else if (removeSubjects) {
|
||||
newSubjects = difference(currentSubjects, removeSubjects);
|
||||
}
|
||||
|
||||
return this.update(roleBinding, {
|
||||
roleRef: roleBinding.roleRef,
|
||||
subjects: newSubjects
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const roleBindingsStore = new RoleBindingsStore();
|
||||
|
||||
apiManager.registerStore(roleBindingsStore, [
|
||||
roleBindingApi,
|
||||
clusterRoleBindingApi,
|
||||
]);
|
||||
@ -1,85 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./role-bindings.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import type { IRoleBindingsRouteParams } from "../+user-management/user-management.route";
|
||||
import type { RoleBinding } from "../../api/endpoints";
|
||||
import { roleBindingsStore } from "./role-bindings.store";
|
||||
import { KubeObjectListLayout } from "../kube-object";
|
||||
import { AddRoleBindingDialog } from "./add-role-binding-dialog";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
namespace = "namespace",
|
||||
bindings = "bindings",
|
||||
age = "age",
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps<IRoleBindingsRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class RoleBindings extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<KubeObjectListLayout
|
||||
isConfigurable
|
||||
tableId="access_role_bindings"
|
||||
className="RoleBindings"
|
||||
store={roleBindingsStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: (binding: RoleBinding) => binding.getName(),
|
||||
[columnId.namespace]: (binding: RoleBinding) => binding.getNs(),
|
||||
[columnId.bindings]: (binding: RoleBinding) => binding.getSubjectNames(),
|
||||
[columnId.age]: (binding: RoleBinding) => binding.getTimeDiffFromNow(),
|
||||
}}
|
||||
searchFilters={[
|
||||
(binding: RoleBinding) => binding.getSearchFields(),
|
||||
(binding: RoleBinding) => binding.getSubjectNames(),
|
||||
]}
|
||||
renderHeaderTitle="Role Bindings"
|
||||
renderTableHeader={[
|
||||
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||
{ className: "warning", showWithColumn: columnId.name },
|
||||
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||
{ title: "Bindings", className: "bindings", sortBy: columnId.bindings, id: columnId.bindings },
|
||||
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||
]}
|
||||
renderTableContents={(binding: RoleBinding) => [
|
||||
binding.getName(),
|
||||
<KubeObjectStatusIcon key="icon" object={binding} />,
|
||||
binding.getNs() || "-",
|
||||
binding.getSubjectNames(),
|
||||
binding.getAge(),
|
||||
]}
|
||||
addRemoveButtons={{
|
||||
onAdd: () => AddRoleBindingDialog.open(),
|
||||
addTooltip: "Create new RoleBinding",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { clusterRoleApi, Role, roleApi } from "../../api/endpoints";
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
|
||||
export class RolesStore extends KubeObjectStore<Role> {
|
||||
api = clusterRoleApi;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getSubscribeApis() {
|
||||
return [roleApi, clusterRoleApi];
|
||||
}
|
||||
|
||||
protected sortItems(items: Role[]) {
|
||||
return super.sortItems(items, [
|
||||
role => role.kind,
|
||||
role => role.getName(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected loadItem(params: { name: string; namespace?: string }) {
|
||||
if (params.namespace) return roleApi.get(params);
|
||||
|
||||
return clusterRoleApi.get(params);
|
||||
}
|
||||
|
||||
protected async loadItems(params: KubeObjectStoreLoadingParams): Promise<Role[]> {
|
||||
const items = await Promise.all([
|
||||
super.loadItems({ ...params, api: clusterRoleApi }),
|
||||
super.loadItems({ ...params, api: roleApi }),
|
||||
]);
|
||||
|
||||
return items.flat();
|
||||
}
|
||||
|
||||
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<Role>) {
|
||||
if (params.namespace) {
|
||||
return roleApi.create(params, data);
|
||||
} else {
|
||||
return clusterRoleApi.create(params, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const rolesStore = new RolesStore();
|
||||
|
||||
apiManager.registerStore(rolesStore, [
|
||||
roleApi,
|
||||
clusterRoleApi,
|
||||
]);
|
||||
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./details.scss";
|
||||
|
||||
import { reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import React from "react";
|
||||
|
||||
import { KubeEventDetails } from "../../+events/kube-event-details";
|
||||
import type { ClusterRoleBinding, ClusterRoleBindingSubject } from "../../../api/endpoints";
|
||||
import { kubeObjectDetailRegistry } from "../../../api/kube-object-detail-registry";
|
||||
import { autoBind, ObservableHashSet, prevDefault } from "../../../utils";
|
||||
import { AddRemoveButtons } from "../../add-remove-buttons";
|
||||
import { ConfirmDialog } from "../../confirm-dialog";
|
||||
import { DrawerTitle } from "../../drawer";
|
||||
import type { KubeObjectDetailsProps } from "../../kube-object";
|
||||
import { KubeObjectMeta } from "../../kube-object/kube-object-meta";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../../table";
|
||||
import { ClusterRoleBindingDialog } from "./dialog";
|
||||
import { clusterRoleBindingsStore } from "./store";
|
||||
import { hashClusterRoleBindingSubject } from "./hashers";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<ClusterRoleBinding> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterRoleBindingDetails extends React.Component<Props> {
|
||||
selectedSubjects = new ObservableHashSet<ClusterRoleBindingSubject>([], hashClusterRoleBindingSubject);
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.props.object, () => {
|
||||
this.selectedSubjects.clear();
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
removeSelectedSubjects() {
|
||||
const { object: clusterRoleBinding } = this.props;
|
||||
const { selectedSubjects } = this;
|
||||
|
||||
ConfirmDialog.open({
|
||||
ok: () => clusterRoleBindingsStore.removeSubjects(clusterRoleBinding, selectedSubjects),
|
||||
labelOk: `Remove`,
|
||||
message: (
|
||||
<p>Remove selected bindings for <b>{clusterRoleBinding.getName()}</b>?</p>
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { selectedSubjects } = this;
|
||||
const { object: clusterRoleBinding } = this.props;
|
||||
|
||||
if (!clusterRoleBinding) {
|
||||
return null;
|
||||
}
|
||||
const { roleRef } = clusterRoleBinding;
|
||||
const subjects = clusterRoleBinding.getSubjects();
|
||||
|
||||
return (
|
||||
<div className="RoleBindingDetails">
|
||||
<KubeObjectMeta object={clusterRoleBinding} />
|
||||
|
||||
<DrawerTitle title="Reference" />
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableCell>Kind</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>API Group</TableCell>
|
||||
</TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{roleRef.kind}</TableCell>
|
||||
<TableCell>{roleRef.name}</TableCell>
|
||||
<TableCell>{roleRef.apiGroup}</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
|
||||
<DrawerTitle title="Bindings" />
|
||||
{subjects.length > 0 && (
|
||||
<Table selectable className="bindings box grow">
|
||||
<TableHead>
|
||||
<TableCell checkbox />
|
||||
<TableCell className="binding">Name</TableCell>
|
||||
<TableCell className="type">Type</TableCell>
|
||||
</TableHead>
|
||||
{
|
||||
subjects.map((subject, i) => {
|
||||
const { kind, name } = subject;
|
||||
const isSelected = selectedSubjects.has(subject);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={i}
|
||||
selected={isSelected}
|
||||
onClick={prevDefault(() => this.selectedSubjects.toggle(subject))}
|
||||
>
|
||||
<TableCell checkbox isChecked={isSelected} />
|
||||
<TableCell className="binding">{name}</TableCell>
|
||||
<TableCell className="type">{kind}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Table>
|
||||
)}
|
||||
|
||||
<AddRemoveButtons
|
||||
onAdd={() => ClusterRoleBindingDialog.open(clusterRoleBinding)}
|
||||
onRemove={selectedSubjects.size ? this.removeSelectedSubjects : null}
|
||||
addTooltip={`Add bindings to ${roleRef.name}`}
|
||||
removeTooltip={`Remove selected bindings from ${roleRef.name}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "ClusterRoleBinding",
|
||||
apiVersions: ["rbac.authorization.k8s.io/v1"],
|
||||
components: {
|
||||
Details: (props) => <ClusterRoleBindingDetails {...props} />
|
||||
}
|
||||
});
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "ClusterRoleBinding",
|
||||
apiVersions: ["rbac.authorization.k8s.io/v1"],
|
||||
priority: 5,
|
||||
components: {
|
||||
Details: (props) => <KubeEventDetails {...props} />
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
.AddClusterRoleBindingDialog {
|
||||
.Select + .Select {
|
||||
margin-top: $margin /2;
|
||||
}
|
||||
.Checkbox {
|
||||
margin-top: $margin;
|
||||
}
|
||||
.name {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./dialog.scss";
|
||||
|
||||
import { action, computed, makeObservable, observable, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import React from "react";
|
||||
|
||||
import { serviceAccountsStore } from "../+service-accounts/store";
|
||||
import { ClusterRole, ClusterRoleBinding, ClusterRoleBindingSubject, ServiceAccount } from "../../../api/endpoints";
|
||||
import { Dialog, DialogProps } from "../../dialog";
|
||||
import { EditableList } from "../../editable-list";
|
||||
import { Icon } from "../../icon";
|
||||
import { showDetails } from "../../kube-object";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { Notifications } from "../../notifications";
|
||||
import { Select, SelectOption } from "../../select";
|
||||
import { Wizard, WizardStep } from "../../wizard";
|
||||
import { clusterRoleBindingsStore } from "./store";
|
||||
import { clusterRolesStore } from "../+cluster-roles/store";
|
||||
import { getRoleRefSelectOption, ServiceAccountOption } from "../select-options";
|
||||
import { ObservableHashSet, nFircate } from "../../../utils";
|
||||
import { Input } from "../../input";
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
interface DialogState {
|
||||
isOpen: boolean;
|
||||
data?: ClusterRoleBinding;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterRoleBindingDialog extends React.Component<Props> {
|
||||
static state = observable.object<DialogState>({
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.isEditing, () => {
|
||||
this.bindingName = ClusterRoleBindingDialog.state.data?.getName();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
static open(roleBinding?: ClusterRoleBinding) {
|
||||
ClusterRoleBindingDialog.state.isOpen = true;
|
||||
ClusterRoleBindingDialog.state.data = roleBinding;
|
||||
}
|
||||
|
||||
static close() {
|
||||
ClusterRoleBindingDialog.state.isOpen = false;
|
||||
}
|
||||
|
||||
get clusterRoleBinding(): ClusterRoleBinding {
|
||||
return ClusterRoleBindingDialog.state.data;
|
||||
}
|
||||
|
||||
@computed get isEditing() {
|
||||
return !!this.clusterRoleBinding;
|
||||
}
|
||||
|
||||
@observable selectedRoleRef: ClusterRole | undefined = undefined;
|
||||
@observable bindingName = "";
|
||||
selectedAccounts = new ObservableHashSet<ServiceAccount>([], sa => sa.metadata.uid);
|
||||
selectedUsers = observable.set<string>([]);
|
||||
selectedGroups = observable.set<string>([]);
|
||||
|
||||
@computed get selectedBindings(): ClusterRoleBindingSubject[] {
|
||||
const serviceAccounts = Array.from(this.selectedAccounts, sa => ({
|
||||
name: sa.getName(),
|
||||
kind: "ServiceAccount" as const,
|
||||
namespace: sa.getNs(),
|
||||
}));
|
||||
const users = Array.from(this.selectedUsers, user => ({
|
||||
name: user,
|
||||
kind: "User" as const,
|
||||
}));
|
||||
const groups = Array.from(this.selectedGroups, group => ({
|
||||
name: group,
|
||||
kind: "Group" as const,
|
||||
}));
|
||||
|
||||
return [
|
||||
...serviceAccounts,
|
||||
...users,
|
||||
...groups,
|
||||
];
|
||||
}
|
||||
|
||||
@computed get clusterRoleRefoptions(): SelectOption<ClusterRole>[] {
|
||||
return clusterRolesStore.items.map(getRoleRefSelectOption);
|
||||
}
|
||||
|
||||
@computed get serviceAccountOptions(): ServiceAccountOption[] {
|
||||
return serviceAccountsStore.items.map(account => {
|
||||
const name = account.getName();
|
||||
const namespace = account.getNs();
|
||||
|
||||
return {
|
||||
value: `${account.getName()}%${account.getNs()}`,
|
||||
account,
|
||||
label: <><Icon small material="account_box" /> {name} ({namespace})</>
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@computed get selectedServiceAccountOptions(): ServiceAccountOption[] {
|
||||
return this.serviceAccountOptions.filter(({ account }) => this.selectedAccounts.has(account));
|
||||
}
|
||||
|
||||
@action
|
||||
onOpen = () => {
|
||||
const binding = this.clusterRoleBinding;
|
||||
|
||||
if (!binding) {
|
||||
return this.reset();
|
||||
}
|
||||
|
||||
this.selectedRoleRef = clusterRolesStore
|
||||
.items
|
||||
.find(item => item.getName() === binding.roleRef.name);
|
||||
this.bindingName = this.clusterRoleBinding.getName();
|
||||
|
||||
const [saSubjects, uSubjects, gSubjects] = nFircate(binding.getSubjects(), "kind", ["ServiceAccount", "User", "Group"]);
|
||||
const accountNames = new Set(saSubjects.map(acc => acc.name));
|
||||
|
||||
this.selectedAccounts.replace(
|
||||
serviceAccountsStore.items
|
||||
.filter(sa => accountNames.has(sa.getName()))
|
||||
);
|
||||
this.selectedUsers.replace(uSubjects.map(user => user.name));
|
||||
this.selectedGroups.replace(gSubjects.map(group => group.name));
|
||||
};
|
||||
|
||||
@action
|
||||
reset = () => {
|
||||
this.selectedRoleRef = undefined;
|
||||
this.bindingName = "";
|
||||
this.selectedAccounts.clear();
|
||||
this.selectedUsers.clear();
|
||||
this.selectedGroups.clear();
|
||||
};
|
||||
|
||||
createBindings = async () => {
|
||||
const { selectedRoleRef, selectedBindings, bindingName } = this;
|
||||
|
||||
try {
|
||||
const { selfLink } = this.isEditing
|
||||
? await clusterRoleBindingsStore.updateSubjects(this.clusterRoleBinding, selectedBindings)
|
||||
: await clusterRoleBindingsStore.create({ name: bindingName }, {
|
||||
subjects: selectedBindings,
|
||||
roleRef: {
|
||||
name: selectedRoleRef.getName(),
|
||||
kind: selectedRoleRef.kind,
|
||||
}
|
||||
});
|
||||
|
||||
showDetails(selfLink);
|
||||
ClusterRoleBindingDialog.close();
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
renderContents() {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Cluster Role Reference" />
|
||||
<Select
|
||||
themeName="light"
|
||||
placeholder="Select cluster role ..."
|
||||
isDisabled={this.isEditing}
|
||||
options={this.clusterRoleRefoptions}
|
||||
value={this.selectedRoleRef}
|
||||
onChange={({ value }: SelectOption<ClusterRole> ) => {
|
||||
if (!this.selectedRoleRef || this.bindingName === this.selectedRoleRef.getName()) {
|
||||
this.bindingName = value.getName();
|
||||
}
|
||||
|
||||
this.selectedRoleRef = value;
|
||||
}}
|
||||
/>
|
||||
|
||||
<SubTitle title="Binding Name" />
|
||||
<Input
|
||||
placeholder="Name of ClusterRoleBinding ..."
|
||||
disabled={this.isEditing}
|
||||
value={this.bindingName}
|
||||
onChange={val => this.bindingName = val}
|
||||
/>
|
||||
|
||||
<SubTitle title="Binding targets" />
|
||||
|
||||
<b>Users</b>
|
||||
<EditableList
|
||||
placeholder="Bind to User Account ..."
|
||||
add={(newUser) => this.selectedUsers.add(newUser)}
|
||||
items={Array.from(this.selectedUsers)}
|
||||
remove={({ oldItem }) => this.selectedUsers.delete(oldItem)}
|
||||
/>
|
||||
|
||||
<b>Groups</b>
|
||||
<EditableList
|
||||
placeholder="Bind to User Group ..."
|
||||
add={(newGroup) => this.selectedGroups.add(newGroup)}
|
||||
items={Array.from(this.selectedGroups)}
|
||||
remove={({ oldItem }) => this.selectedGroups.delete(oldItem)}
|
||||
/>
|
||||
|
||||
<b>Service Accounts</b>
|
||||
<Select
|
||||
isMulti
|
||||
themeName="light"
|
||||
placeholder="Select service accounts ..."
|
||||
autoConvertOptions={false}
|
||||
options={this.serviceAccountOptions}
|
||||
value={this.selectedServiceAccountOptions}
|
||||
onChange={(selected: ServiceAccountOption[] | null) => {
|
||||
if (selected) {
|
||||
this.selectedAccounts.replace(selected.map(opt => opt.account));
|
||||
} else {
|
||||
this.selectedAccounts.clear();
|
||||
}
|
||||
}}
|
||||
maxMenuHeight={200}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ...dialogProps } = this.props;
|
||||
const [action, nextLabel] = this.isEditing ? ["Edit", "Update"] : ["Add", "Create"];
|
||||
const disableNext = !this.selectedRoleRef || !this.selectedBindings.length || !this.bindingName;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
className="AddClusterRoleBindingDialog"
|
||||
isOpen={ClusterRoleBindingDialog.state.isOpen}
|
||||
close={ClusterRoleBindingDialog.close}
|
||||
onClose={this.reset}
|
||||
onOpen={this.onOpen}
|
||||
>
|
||||
<Wizard
|
||||
header={<h5>{action} ClusterRoleBinding</h5>}
|
||||
done={ClusterRoleBindingDialog.close}
|
||||
>
|
||||
<WizardStep
|
||||
nextLabel={nextLabel}
|
||||
next={this.createBindings}
|
||||
disabledNext={disableNext}
|
||||
>
|
||||
{this.renderContents()}
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,13 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "./service-accounts";
|
||||
export * from "./service-accounts-details";
|
||||
export * from "./create-service-account-dialog";
|
||||
import { MD5 } from "crypto-js";
|
||||
import type { ClusterRoleBindingSubject } from "../../../api/endpoints";
|
||||
|
||||
export function hashClusterRoleBindingSubject(subject: ClusterRoleBindingSubject): string {
|
||||
return MD5(JSON.stringify([
|
||||
["kind", subject.kind],
|
||||
["name", subject.name],
|
||||
["apiGroup", subject.apiGroup],
|
||||
])).toString();
|
||||
}
|
||||
@ -18,7 +18,6 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "./roles";
|
||||
export * from "./role-details";
|
||||
export * from "./add-role-dialog";
|
||||
export * from "./view";
|
||||
export * from "./details";
|
||||
export * from "./dialog";
|
||||
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { apiManager } from "../../../api/api-manager";
|
||||
import { ClusterRoleBinding, clusterRoleBindingApi, ClusterRoleBindingSubject } from "../../../api/endpoints";
|
||||
import { KubeObjectStore } from "../../../kube-object.store";
|
||||
import { autoBind, HashSet } from "../../../utils";
|
||||
import { hashClusterRoleBindingSubject } from "./hashers";
|
||||
|
||||
export class ClusterRoleBindingsStore extends KubeObjectStore<ClusterRoleBinding> {
|
||||
api = clusterRoleBindingApi;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
protected sortItems(items: ClusterRoleBinding[]) {
|
||||
return super.sortItems(items, [
|
||||
clusterRoleBinding => clusterRoleBinding.kind,
|
||||
clusterRoleBinding => clusterRoleBinding.getName()
|
||||
]);
|
||||
}
|
||||
|
||||
async updateSubjects(clusterRoleBinding: ClusterRoleBinding, subjects: ClusterRoleBindingSubject[]) {
|
||||
return this.update(clusterRoleBinding, {
|
||||
roleRef: clusterRoleBinding.roleRef,
|
||||
subjects,
|
||||
});
|
||||
}
|
||||
|
||||
async removeSubjects(clusterRoleBinding: ClusterRoleBinding, subjectsToRemove: Iterable<ClusterRoleBindingSubject>) {
|
||||
const currentSubjects = new HashSet(clusterRoleBinding.getSubjects(), hashClusterRoleBindingSubject);
|
||||
|
||||
for (const subject of subjectsToRemove) {
|
||||
currentSubjects.delete(subject);
|
||||
}
|
||||
|
||||
return this.updateSubjects(clusterRoleBinding, currentSubjects.toJSON());
|
||||
}
|
||||
}
|
||||
|
||||
export const clusterRoleBindingsStore = new ClusterRoleBindingsStore();
|
||||
|
||||
apiManager.registerStore(clusterRoleBindingsStore);
|
||||
@ -0,0 +1,11 @@
|
||||
.ClusterRoleBindings {
|
||||
.help-icon {
|
||||
margin-left: $margin / 2;
|
||||
}
|
||||
|
||||
.TableCell {
|
||||
&.warning {
|
||||
@include table-cell-warning;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./view.scss";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import type { ClusterRoleBinding } from "../../../api/endpoints";
|
||||
import { KubeObjectListLayout } from "../../kube-object";
|
||||
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
|
||||
import type { ClusterRoleBindingsRouteParams } from "../user-management.route";
|
||||
import { ClusterRoleBindingDialog } from "./dialog";
|
||||
import { clusterRoleBindingsStore } from "./store";
|
||||
import { clusterRolesStore } from "../+cluster-roles/store";
|
||||
import { serviceAccountsStore } from "../+service-accounts/store";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
namespace = "namespace",
|
||||
bindings = "bindings",
|
||||
age = "age",
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps<ClusterRoleBindingsRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterRoleBindings extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<KubeObjectListLayout
|
||||
isConfigurable
|
||||
tableId="access_cluster_role_bindings"
|
||||
className="ClusterRoleBindings"
|
||||
store={clusterRoleBindingsStore}
|
||||
dependentStores={[clusterRolesStore, serviceAccountsStore]}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: (binding: ClusterRoleBinding) => binding.getName(),
|
||||
[columnId.bindings]: (binding: ClusterRoleBinding) => binding.getSubjectNames(),
|
||||
[columnId.age]: (binding: ClusterRoleBinding) => binding.getTimeDiffFromNow(),
|
||||
}}
|
||||
searchFilters={[
|
||||
(binding: ClusterRoleBinding) => binding.getSearchFields(),
|
||||
(binding: ClusterRoleBinding) => binding.getSubjectNames(),
|
||||
]}
|
||||
renderHeaderTitle="Cluster Role Bindings"
|
||||
renderTableHeader={[
|
||||
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||
{ className: "warning", showWithColumn: columnId.name },
|
||||
{ title: "Bindings", className: "bindings", sortBy: columnId.bindings, id: columnId.bindings },
|
||||
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||
]}
|
||||
renderTableContents={(binding: ClusterRoleBinding) => [
|
||||
binding.getName(),
|
||||
<KubeObjectStatusIcon key="icon" object={binding} />,
|
||||
binding.getSubjectNames(),
|
||||
binding.getAge(),
|
||||
]}
|
||||
addRemoveButtons={{
|
||||
onAdd: () => ClusterRoleBindingDialog.open(),
|
||||
addTooltip: "Create new ClusterRoleBinding",
|
||||
}}
|
||||
/>
|
||||
<ClusterRoleBindingDialog />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import "./add-dialog.scss";
|
||||
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
|
||||
import { Dialog, DialogProps } from "../../dialog";
|
||||
import { Input } from "../../input";
|
||||
import { showDetails } from "../../kube-object";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { Notifications } from "../../notifications";
|
||||
import { Wizard, WizardStep } from "../../wizard";
|
||||
import { clusterRolesStore } from "./store";
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class AddClusterRoleDialog extends React.Component<Props> {
|
||||
static isOpen = observable.box(false);
|
||||
|
||||
@observable clusterRoleName = "";
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
static open() {
|
||||
AddClusterRoleDialog.isOpen.set(true);
|
||||
}
|
||||
|
||||
static close() {
|
||||
AddClusterRoleDialog.isOpen.set(false);
|
||||
}
|
||||
|
||||
reset = () => {
|
||||
this.clusterRoleName = "";
|
||||
};
|
||||
|
||||
createRole = async () => {
|
||||
try {
|
||||
const role = await clusterRolesStore.create({ name: this.clusterRoleName });
|
||||
|
||||
showDetails(role.selfLink);
|
||||
this.reset();
|
||||
AddClusterRoleDialog.close();
|
||||
} catch (err) {
|
||||
Notifications.error(err.toString());
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { ...dialogProps } = this.props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
className="AddRoleDialog"
|
||||
isOpen={AddClusterRoleDialog.isOpen.get()}
|
||||
close={AddClusterRoleDialog.close}
|
||||
>
|
||||
<Wizard
|
||||
header={<h5>Create ClusterRole</h5>}
|
||||
done={AddClusterRoleDialog.close}
|
||||
>
|
||||
<WizardStep
|
||||
contentClass="flex gaps column"
|
||||
nextLabel="Create"
|
||||
next={this.createRole}
|
||||
>
|
||||
<SubTitle title="ClusterRole Name" />
|
||||
<Input
|
||||
required autoFocus
|
||||
placeholder="Name"
|
||||
iconLeft="supervisor_account"
|
||||
value={this.clusterRoleName}
|
||||
onChange={v => this.clusterRoleName = v}
|
||||
/>
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./details.scss";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
|
||||
import { KubeEventDetails } from "../../+events/kube-event-details";
|
||||
import { kubeObjectDetailRegistry } from "../../../api/kube-object-detail-registry";
|
||||
import { DrawerTitle } from "../../drawer";
|
||||
import type { KubeObjectDetailsProps } from "../../kube-object";
|
||||
import { KubeObjectMeta } from "../../kube-object/kube-object-meta";
|
||||
import type { ClusterRole } from "../../../api/endpoints";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<ClusterRole> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterRoleDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: clusterRole } = this.props;
|
||||
|
||||
if (!clusterRole) return null;
|
||||
const rules = clusterRole.getRules();
|
||||
|
||||
return (
|
||||
<div className="ClusterRoleDetails">
|
||||
<KubeObjectMeta object={clusterRole}/>
|
||||
|
||||
<DrawerTitle title="Rules"/>
|
||||
{rules.map(({ resourceNames, apiGroups, resources, verbs }, index) => {
|
||||
return (
|
||||
<div className="rule" key={index}>
|
||||
{resources && (
|
||||
<>
|
||||
<div className="name">Resources</div>
|
||||
<div className="value">{resources.join(", ")}</div>
|
||||
</>
|
||||
)}
|
||||
{verbs && (
|
||||
<>
|
||||
<div className="name">Verbs</div>
|
||||
<div className="value">{verbs.join(", ")}</div>
|
||||
</>
|
||||
)}
|
||||
{apiGroups && (
|
||||
<>
|
||||
<div className="name">Api Groups</div>
|
||||
<div className="value">
|
||||
{apiGroups
|
||||
.map(apiGroup => apiGroup === "" ? `'${apiGroup}'` : apiGroup)
|
||||
.join(", ")
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{resourceNames && (
|
||||
<>
|
||||
<div className="name">Resource Names</div>
|
||||
<div className="value">{resourceNames.join(", ")}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "ClusterRole",
|
||||
apiVersions: ["rbac.authorization.k8s.io/v1"],
|
||||
components: {
|
||||
Details: (props) => <ClusterRoleDetails {...props}/>
|
||||
}
|
||||
});
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "ClusterRole",
|
||||
apiVersions: ["rbac.authorization.k8s.io/v1"],
|
||||
priority: 5,
|
||||
components: {
|
||||
Details: (props) => <KubeEventDetails {...props}/>
|
||||
}
|
||||
});
|
||||
@ -18,7 +18,6 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "./role-bindings";
|
||||
export * from "./role-binding-details";
|
||||
export * from "./add-role-binding-dialog";
|
||||
export * from "./view";
|
||||
export * from "./details";
|
||||
export * from "./add-dialog";
|
||||
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { apiManager } from "../../../api/api-manager";
|
||||
import { ClusterRole, clusterRoleApi } from "../../../api/endpoints";
|
||||
import { KubeObjectStore } from "../../../kube-object.store";
|
||||
import { autoBind } from "../../../utils";
|
||||
|
||||
export class ClusterRolesStore extends KubeObjectStore<ClusterRole> {
|
||||
api = clusterRoleApi;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
protected sortItems(items: ClusterRole[]) {
|
||||
return super.sortItems(items, [
|
||||
clusterRole => clusterRole.kind,
|
||||
clusterRole => clusterRole.getName(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export const clusterRolesStore = new ClusterRolesStore();
|
||||
|
||||
apiManager.registerStore(clusterRolesStore);
|
||||
@ -0,0 +1,11 @@
|
||||
.ClusterRoles {
|
||||
.help-icon {
|
||||
margin-left: $margin / 2;
|
||||
}
|
||||
|
||||
.TableCell {
|
||||
&.warning {
|
||||
@include table-cell-warning;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./view.scss";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import type { ClusterRole } from "../../../api/endpoints";
|
||||
import { KubeObjectListLayout } from "../../kube-object";
|
||||
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
|
||||
import type { ClusterRolesRouteParams } from "../user-management.route";
|
||||
import { AddClusterRoleDialog } from "./add-dialog";
|
||||
import { clusterRolesStore } from "./store";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
namespace = "namespace",
|
||||
age = "age",
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps<ClusterRolesRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterRoles extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<KubeObjectListLayout
|
||||
isConfigurable
|
||||
tableId="access_cluster_roles"
|
||||
className="ClusterRoles"
|
||||
store={clusterRolesStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: (clusterRole: ClusterRole) => clusterRole.getName(),
|
||||
[columnId.age]: (clusterRole: ClusterRole) => clusterRole.getTimeDiffFromNow(),
|
||||
}}
|
||||
searchFilters={[
|
||||
(clusterRole: ClusterRole) => clusterRole.getSearchFields(),
|
||||
]}
|
||||
renderHeaderTitle="Cluster Roles"
|
||||
renderTableHeader={[
|
||||
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||
{ className: "warning", showWithColumn: columnId.name },
|
||||
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||
]}
|
||||
renderTableContents={(clusterRole: ClusterRole) => [
|
||||
clusterRole.getName(),
|
||||
<KubeObjectStatusIcon key="icon" object={clusterRole} />,
|
||||
clusterRole.getAge(),
|
||||
]}
|
||||
addRemoveButtons={{
|
||||
onAdd: () => AddClusterRoleDialog.open(),
|
||||
addTooltip: "Create new ClusterRole",
|
||||
}}
|
||||
/>
|
||||
<AddClusterRoleDialog/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
.RoleBindingDetails {
|
||||
}
|
||||
@ -19,35 +19,32 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./role-binding-details.scss";
|
||||
import "./details.scss";
|
||||
|
||||
import React from "react";
|
||||
import { AddRemoveButtons } from "../add-remove-buttons";
|
||||
import type { IRoleBindingSubject, RoleBinding } from "../../api/endpoints";
|
||||
import { boundMethod, prevDefault } from "../../utils";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { DrawerTitle } from "../drawer";
|
||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { observable, reaction, makeObservable } from "mobx";
|
||||
import { roleBindingsStore } from "./role-bindings.store";
|
||||
import { AddRoleBindingDialog } from "./add-role-binding-dialog";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import React from "react";
|
||||
import { KubeEventDetails } from "../../+events/kube-event-details";
|
||||
import type { RoleBinding, RoleBindingSubject } from "../../../api/endpoints";
|
||||
import { kubeObjectDetailRegistry } from "../../../api/kube-object-detail-registry";
|
||||
import { prevDefault, boundMethod } from "../../../utils";
|
||||
import { AddRemoveButtons } from "../../add-remove-buttons";
|
||||
import { ConfirmDialog } from "../../confirm-dialog";
|
||||
import { DrawerTitle } from "../../drawer";
|
||||
import type { KubeObjectDetailsProps } from "../../kube-object";
|
||||
import { KubeObjectMeta } from "../../kube-object/kube-object-meta";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../../table";
|
||||
import { RoleBindingDialog } from "./dialog";
|
||||
import { roleBindingsStore } from "./store";
|
||||
import { ObservableHashSet } from "../../../../common/utils/hash-set";
|
||||
import { hashRoleBindingSubject } from "./hashers";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<RoleBinding> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class RoleBindingDetails extends React.Component<Props> {
|
||||
@observable selectedSubjects = observable.array<IRoleBindingSubject>([], { deep: false });
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
selectedSubjects = new ObservableHashSet<RoleBindingSubject>([], hashRoleBindingSubject);
|
||||
|
||||
async componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
@ -57,24 +54,13 @@ export class RoleBindingDetails extends React.Component<Props> {
|
||||
]);
|
||||
}
|
||||
|
||||
selectSubject(subject: IRoleBindingSubject) {
|
||||
const { selectedSubjects } = this;
|
||||
const isSelected = selectedSubjects.includes(subject);
|
||||
|
||||
selectedSubjects.replace(
|
||||
isSelected
|
||||
? selectedSubjects.filter(sub => sub !== subject) // unselect
|
||||
: selectedSubjects.concat(subject) // select
|
||||
);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
removeSelectedSubjects() {
|
||||
const { object: roleBinding } = this.props;
|
||||
const { selectedSubjects } = this;
|
||||
|
||||
ConfirmDialog.open({
|
||||
ok: () => roleBindingsStore.updateSubjects({ roleBinding, removeSubjects: selectedSubjects }),
|
||||
ok: () => roleBindingsStore.removeSubjects(roleBinding, selectedSubjects.toJSON()),
|
||||
labelOk: `Remove`,
|
||||
message: (
|
||||
<p>Remove selected bindings for <b>{roleBinding.getName()}</b>?</p>
|
||||
@ -94,9 +80,9 @@ export class RoleBindingDetails extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<div className="RoleBindingDetails">
|
||||
<KubeObjectMeta object={roleBinding}/>
|
||||
<KubeObjectMeta object={roleBinding} />
|
||||
|
||||
<DrawerTitle title="Reference"/>
|
||||
<DrawerTitle title="Reference" />
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableCell>Kind</TableCell>
|
||||
@ -110,26 +96,27 @@ export class RoleBindingDetails extends React.Component<Props> {
|
||||
</TableRow>
|
||||
</Table>
|
||||
|
||||
<DrawerTitle title="Bindings"/>
|
||||
<DrawerTitle title="Bindings" />
|
||||
{subjects.length > 0 && (
|
||||
<Table selectable className="bindings box grow">
|
||||
<TableHead>
|
||||
<TableCell checkbox/>
|
||||
<TableCell className="binding">Binding</TableCell>
|
||||
<TableCell checkbox />
|
||||
<TableCell className="binding">Name</TableCell>
|
||||
<TableCell className="type">Type</TableCell>
|
||||
<TableCell className="type">Namespace</TableCell>
|
||||
</TableHead>
|
||||
{
|
||||
subjects.map((subject, i) => {
|
||||
const { kind, name, namespace } = subject;
|
||||
const isSelected = selectedSubjects.includes(subject);
|
||||
const isSelected = selectedSubjects.has(subject);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={i} selected={isSelected}
|
||||
onClick={prevDefault(() => this.selectSubject(subject))}
|
||||
key={i}
|
||||
selected={isSelected}
|
||||
onClick={prevDefault(() => this.selectedSubjects.toggle(subject))}
|
||||
>
|
||||
<TableCell checkbox isChecked={isSelected}/>
|
||||
<TableCell checkbox isChecked={isSelected} />
|
||||
<TableCell className="binding">{name}</TableCell>
|
||||
<TableCell className="type">{kind}</TableCell>
|
||||
<TableCell className="ns">{namespace || "-"}</TableCell>
|
||||
@ -141,9 +128,9 @@ export class RoleBindingDetails extends React.Component<Props> {
|
||||
)}
|
||||
|
||||
<AddRemoveButtons
|
||||
onAdd={() => AddRoleBindingDialog.open(roleBinding)}
|
||||
onRemove={selectedSubjects.length ? this.removeSelectedSubjects : null}
|
||||
addTooltip={`Add bindings to ${roleRef.name}`}
|
||||
onAdd={() => RoleBindingDialog.open(roleBinding)}
|
||||
onRemove={selectedSubjects.size ? this.removeSelectedSubjects : null}
|
||||
addTooltip={`Edit bindings of ${roleRef.name}`}
|
||||
removeTooltip={`Remove selected bindings from ${roleRef.name}`}
|
||||
/>
|
||||
</div>
|
||||
@ -166,20 +153,3 @@ kubeObjectDetailRegistry.add({
|
||||
Details: (props) => <KubeEventDetails {...props} />
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "ClusterRoleBinding",
|
||||
apiVersions: ["rbac.authorization.k8s.io/v1"],
|
||||
components: {
|
||||
Details: (props) => <RoleBindingDetails {...props} />
|
||||
}
|
||||
});
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "ClusterRoleBinding",
|
||||
apiVersions: ["rbac.authorization.k8s.io/v1"],
|
||||
priority: 5,
|
||||
components: {
|
||||
Details: (props) => <KubeEventDetails {...props} />
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,303 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./dialog.scss";
|
||||
|
||||
import { computed, observable, makeObservable, action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
|
||||
import { rolesStore } from "../+roles/store";
|
||||
import { serviceAccountsStore } from "../+service-accounts/store";
|
||||
import { NamespaceSelect } from "../../+namespaces/namespace-select";
|
||||
import { ClusterRole, Role, roleApi, RoleBinding, RoleBindingSubject, ServiceAccount } from "../../../api/endpoints";
|
||||
import { Dialog, DialogProps } from "../../dialog";
|
||||
import { EditableList } from "../../editable-list";
|
||||
import { Icon } from "../../icon";
|
||||
import { showDetails } from "../../kube-object";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { Notifications } from "../../notifications";
|
||||
import { Select, SelectOption } from "../../select";
|
||||
import { Wizard, WizardStep } from "../../wizard";
|
||||
import { roleBindingsStore } from "./store";
|
||||
import { clusterRolesStore } from "../+cluster-roles/store";
|
||||
import { Input } from "../../input";
|
||||
import { getRoleRefSelectOption, ServiceAccountOption } from "../select-options";
|
||||
import { ObservableHashSet, nFircate } from "../../../utils";
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
interface DialogState {
|
||||
isOpen: boolean;
|
||||
data?: RoleBinding;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class RoleBindingDialog extends React.Component<Props> {
|
||||
static state = observable.object<DialogState>({
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
static open(roleBinding?: RoleBinding) {
|
||||
RoleBindingDialog.state.isOpen = true;
|
||||
RoleBindingDialog.state.data = roleBinding;
|
||||
}
|
||||
|
||||
static close() {
|
||||
RoleBindingDialog.state.isOpen = false;
|
||||
RoleBindingDialog.state.data = undefined;
|
||||
}
|
||||
|
||||
get roleBinding(): RoleBinding {
|
||||
return RoleBindingDialog.state.data;
|
||||
}
|
||||
|
||||
@computed get isEditing() {
|
||||
return !!this.roleBinding;
|
||||
}
|
||||
|
||||
@observable.ref selectedRoleRef: Role | ClusterRole | undefined = undefined;
|
||||
@observable bindingName = "";
|
||||
@observable bindingNamespace = "";
|
||||
selectedAccounts = new ObservableHashSet<ServiceAccount>([], sa => sa.metadata.uid);
|
||||
selectedUsers = observable.set<string>([]);
|
||||
selectedGroups = observable.set<string>([]);
|
||||
|
||||
@computed get selectedBindings(): RoleBindingSubject[] {
|
||||
const serviceAccounts = Array.from(this.selectedAccounts, sa => ({
|
||||
name: sa.getName(),
|
||||
kind: "ServiceAccount" as const,
|
||||
namespace: this.bindingNamespace,
|
||||
}));
|
||||
const users = Array.from(this.selectedUsers, user => ({
|
||||
name: user,
|
||||
kind: "User" as const,
|
||||
namespace: this.bindingNamespace,
|
||||
}));
|
||||
const groups = Array.from(this.selectedGroups, group => ({
|
||||
name: group,
|
||||
kind: "Group" as const,
|
||||
namespace: this.bindingNamespace,
|
||||
}));
|
||||
|
||||
return [
|
||||
...serviceAccounts,
|
||||
...users,
|
||||
...groups,
|
||||
];
|
||||
}
|
||||
|
||||
@computed get roleRefOptions(): SelectOption<Role | ClusterRole>[] {
|
||||
const roles = rolesStore.items
|
||||
.filter(role => role.getNs() === this.bindingNamespace)
|
||||
.map(getRoleRefSelectOption);
|
||||
const clusterRoles = clusterRolesStore.items
|
||||
.map(getRoleRefSelectOption);
|
||||
|
||||
return [
|
||||
...roles,
|
||||
...clusterRoles,
|
||||
];
|
||||
}
|
||||
|
||||
@computed get serviceAccountOptions(): ServiceAccountOption[] {
|
||||
return serviceAccountsStore.items.map(account => {
|
||||
const name = account.getName();
|
||||
const namespace = account.getNs();
|
||||
|
||||
return {
|
||||
value: `${account.getName()}%${account.getNs()}`,
|
||||
account,
|
||||
label: <><Icon small material="account_box" /> {name} ({namespace})</>
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@computed get selectedServiceAccountOptions(): ServiceAccountOption[] {
|
||||
return this.serviceAccountOptions.filter(({ account }) => this.selectedAccounts.has(account));
|
||||
}
|
||||
|
||||
@action
|
||||
onOpen = () => {
|
||||
const binding = this.roleBinding;
|
||||
|
||||
if (!binding) {
|
||||
return this.reset();
|
||||
}
|
||||
|
||||
this.selectedRoleRef = (binding.roleRef.kind === roleApi.kind ? rolesStore : clusterRolesStore)
|
||||
.items
|
||||
.find(item => item.getName() === binding.roleRef.name);
|
||||
|
||||
this.bindingName = binding.getName();
|
||||
this.bindingNamespace = binding.getNs();
|
||||
|
||||
const [saSubjects, uSubjects, gSubjects] = nFircate(binding.getSubjects(), "kind", ["ServiceAccount", "User", "Group"]);
|
||||
const accountNames = new Set(saSubjects.map(acc => acc.name));
|
||||
|
||||
this.selectedAccounts.replace(
|
||||
serviceAccountsStore.items
|
||||
.filter(sa => accountNames.has(sa.getName()))
|
||||
);
|
||||
this.selectedUsers.replace(uSubjects.map(user => user.name));
|
||||
this.selectedGroups.replace(gSubjects.map(group => group.name));
|
||||
};
|
||||
|
||||
@action
|
||||
reset = () => {
|
||||
this.selectedRoleRef = undefined;
|
||||
this.bindingName = "";
|
||||
this.bindingNamespace = "";
|
||||
this.selectedAccounts.clear();
|
||||
this.selectedUsers.clear();
|
||||
this.selectedGroups.clear();
|
||||
};
|
||||
|
||||
createBindings = async () => {
|
||||
const { selectedRoleRef, bindingNamespace: namespace, selectedBindings } = this;
|
||||
|
||||
try {
|
||||
const roleBinding = this.isEditing
|
||||
? await roleBindingsStore.updateSubjects(this.roleBinding, selectedBindings)
|
||||
: await roleBindingsStore.create({ name: this.bindingName, namespace }, {
|
||||
subjects: selectedBindings,
|
||||
roleRef: {
|
||||
name: selectedRoleRef.getName(),
|
||||
kind: selectedRoleRef.kind,
|
||||
}
|
||||
});
|
||||
|
||||
showDetails(roleBinding.selfLink);
|
||||
RoleBindingDialog.close();
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
renderContents() {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Namespace" />
|
||||
<NamespaceSelect
|
||||
themeName="light"
|
||||
isDisabled={this.isEditing}
|
||||
value={this.bindingNamespace}
|
||||
onChange={({ value }) => this.bindingNamespace = value}
|
||||
/>
|
||||
|
||||
<SubTitle title="Role Reference" />
|
||||
<Select
|
||||
themeName="light"
|
||||
placeholder="Select role or cluster role ..."
|
||||
isDisabled={this.isEditing}
|
||||
options={this.roleRefOptions}
|
||||
value={this.selectedRoleRef}
|
||||
onChange={({ value }) => {
|
||||
if (!this.selectedRoleRef || this.bindingName === this.selectedRoleRef.getName()) {
|
||||
this.bindingName = value.getName();
|
||||
}
|
||||
|
||||
this.selectedRoleRef = value;
|
||||
}}
|
||||
/>
|
||||
|
||||
<SubTitle title="Binding Name" />
|
||||
<Input
|
||||
disabled={this.isEditing}
|
||||
value={this.bindingName}
|
||||
onChange={value => this.bindingName = value}
|
||||
/>
|
||||
|
||||
<SubTitle title="Binding targets" />
|
||||
|
||||
<b>Users</b>
|
||||
<EditableList
|
||||
placeholder="Bind to User Account ..."
|
||||
add={(newUser) => this.selectedUsers.add(newUser)}
|
||||
items={Array.from(this.selectedUsers)}
|
||||
remove={({ oldItem }) => this.selectedUsers.delete(oldItem)}
|
||||
/>
|
||||
|
||||
<b>Groups</b>
|
||||
<EditableList
|
||||
placeholder="Bind to User Group ..."
|
||||
add={(newGroup) => this.selectedGroups.add(newGroup)}
|
||||
items={Array.from(this.selectedGroups)}
|
||||
remove={({ oldItem }) => this.selectedGroups.delete(oldItem)}
|
||||
/>
|
||||
|
||||
<b>Service Accounts</b>
|
||||
<Select
|
||||
isMulti
|
||||
themeName="light"
|
||||
placeholder="Select service accounts ..."
|
||||
autoConvertOptions={false}
|
||||
options={this.serviceAccountOptions}
|
||||
value={this.selectedServiceAccountOptions}
|
||||
onChange={(selected: ServiceAccountOption[] | null) => {
|
||||
if (selected) {
|
||||
this.selectedAccounts.replace(selected.map(opt => opt.account));
|
||||
} else {
|
||||
this.selectedAccounts.clear();
|
||||
}
|
||||
}}
|
||||
maxMenuHeight={200}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ...dialogProps } = this.props;
|
||||
const [action, nextLabel] = this.isEditing ? ["Edit", "Update"] : ["Add", "Create"];
|
||||
const disableNext = !this.selectedRoleRef || !this.selectedBindings.length || !this.bindingNamespace || !this.bindingName;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
className="AddRoleBindingDialog"
|
||||
isOpen={RoleBindingDialog.state.isOpen}
|
||||
close={RoleBindingDialog.close}
|
||||
onClose={this.reset}
|
||||
onOpen={this.onOpen}
|
||||
>
|
||||
<Wizard
|
||||
header={<h5>{action} RoleBinding</h5>}
|
||||
done={RoleBindingDialog.close}
|
||||
>
|
||||
<WizardStep
|
||||
nextLabel={nextLabel}
|
||||
next={this.createBindings}
|
||||
disabledNext={disableNext}
|
||||
>
|
||||
{this.renderContents()}
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { MD5 } from "crypto-js";
|
||||
import type { RoleBindingSubject } from "../../../api/endpoints";
|
||||
|
||||
export function hashRoleBindingSubject(subject: RoleBindingSubject): string {
|
||||
return MD5(JSON.stringify([
|
||||
["kind", subject.kind],
|
||||
["name", subject.name],
|
||||
["namespace", subject.namespace],
|
||||
["apiGroup", subject.apiGroup],
|
||||
])).toString();
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
export * from "./view";
|
||||
export * from "./details";
|
||||
export * from "./dialog";
|
||||
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { apiManager } from "../../../api/api-manager";
|
||||
import { RoleBinding, roleBindingApi, RoleBindingSubject } from "../../../api/endpoints";
|
||||
import { KubeObjectStore } from "../../../kube-object.store";
|
||||
import { HashSet } from "../../../utils";
|
||||
import { hashRoleBindingSubject } from "./hashers";
|
||||
|
||||
export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
||||
api = roleBindingApi;
|
||||
|
||||
protected sortItems(items: RoleBinding[]) {
|
||||
return super.sortItems(items, [
|
||||
roleBinding => roleBinding.kind,
|
||||
roleBinding => roleBinding.getName()
|
||||
]);
|
||||
}
|
||||
|
||||
protected async createItem(params: { name: string; namespace: string }, data?: Partial<RoleBinding>) {
|
||||
return roleBindingApi.create(params, data);
|
||||
}
|
||||
|
||||
async updateSubjects(roleBinding: RoleBinding, subjects: RoleBindingSubject[]) {
|
||||
return this.update(roleBinding, {
|
||||
roleRef: roleBinding.roleRef,
|
||||
subjects,
|
||||
});
|
||||
}
|
||||
|
||||
async removeSubjects(roleBinding: RoleBinding, subjectsToRemove: Iterable<RoleBindingSubject>) {
|
||||
const currentSubjects = new HashSet(roleBinding.getSubjects(), hashRoleBindingSubject);
|
||||
|
||||
for (const subject of subjectsToRemove) {
|
||||
currentSubjects.delete(subject);
|
||||
}
|
||||
|
||||
return this.updateSubjects(roleBinding, currentSubjects.toJSON());
|
||||
}
|
||||
}
|
||||
|
||||
export const roleBindingsStore = new RoleBindingsStore();
|
||||
|
||||
apiManager.registerStore(roleBindingsStore);
|
||||
@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import "./view.scss";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import type { RoleBinding } from "../../../api/endpoints";
|
||||
import { KubeObjectListLayout } from "../../kube-object";
|
||||
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
|
||||
import type { RoleBindingsRouteParams } from "../user-management.route";
|
||||
import { RoleBindingDialog } from "./dialog";
|
||||
import { roleBindingsStore } from "./store";
|
||||
import { rolesStore } from "../+roles/store";
|
||||
import { clusterRolesStore } from "../+cluster-roles/store";
|
||||
import { serviceAccountsStore } from "../+service-accounts/store";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
namespace = "namespace",
|
||||
bindings = "bindings",
|
||||
age = "age",
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps<RoleBindingsRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class RoleBindings extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<KubeObjectListLayout
|
||||
isConfigurable
|
||||
tableId="access_role_bindings"
|
||||
className="RoleBindings"
|
||||
store={roleBindingsStore}
|
||||
dependentStores={[rolesStore, clusterRolesStore, serviceAccountsStore]}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: (binding: RoleBinding) => binding.getName(),
|
||||
[columnId.namespace]: (binding: RoleBinding) => binding.getNs(),
|
||||
[columnId.bindings]: (binding: RoleBinding) => binding.getSubjectNames(),
|
||||
[columnId.age]: (binding: RoleBinding) => binding.getTimeDiffFromNow(),
|
||||
}}
|
||||
searchFilters={[
|
||||
(binding: RoleBinding) => binding.getSearchFields(),
|
||||
(binding: RoleBinding) => binding.getSubjectNames(),
|
||||
]}
|
||||
renderHeaderTitle="Role Bindings"
|
||||
renderTableHeader={[
|
||||
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||
{ className: "warning", showWithColumn: columnId.name },
|
||||
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||
{ title: "Bindings", className: "bindings", sortBy: columnId.bindings, id: columnId.bindings },
|
||||
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||
]}
|
||||
renderTableContents={(binding: RoleBinding) => [
|
||||
binding.getName(),
|
||||
<KubeObjectStatusIcon key="icon" object={binding} />,
|
||||
binding.getNs(),
|
||||
binding.getSubjectNames(),
|
||||
binding.getAge(),
|
||||
]}
|
||||
addRemoveButtons={{
|
||||
onAdd: () => RoleBindingDialog.open(),
|
||||
addTooltip: "Create new RoleBinding",
|
||||
}}
|
||||
/>
|
||||
<RoleBindingDialog />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
.AddRoleDialog {
|
||||
.AceEditor {
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
@ -19,29 +19,28 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./add-role-dialog.scss";
|
||||
import "./add-dialog.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observable, makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { Notifications } from "../notifications";
|
||||
import { rolesStore } from "./roles.store";
|
||||
import { Input } from "../input";
|
||||
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
||||
import { showDetails } from "../kube-object";
|
||||
|
||||
import { NamespaceSelect } from "../../+namespaces/namespace-select";
|
||||
import { Dialog, DialogProps } from "../../dialog";
|
||||
import { Input } from "../../input";
|
||||
import { showDetails } from "../../kube-object";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { Notifications } from "../../notifications";
|
||||
import { Wizard, WizardStep } from "../../wizard";
|
||||
import { rolesStore } from "./store";
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
const dialogState = observable.object({
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
@observer
|
||||
export class AddRoleDialog extends React.Component<Props> {
|
||||
static isOpen = observable.box(false);
|
||||
|
||||
@observable roleName = "";
|
||||
@observable namespace = "";
|
||||
|
||||
@ -51,17 +50,13 @@ export class AddRoleDialog extends React.Component<Props> {
|
||||
}
|
||||
|
||||
static open() {
|
||||
dialogState.isOpen = true;
|
||||
AddRoleDialog.isOpen.set(true);
|
||||
}
|
||||
|
||||
static close() {
|
||||
dialogState.isOpen = false;
|
||||
AddRoleDialog.isOpen.set(false);
|
||||
}
|
||||
|
||||
close = () => {
|
||||
AddRoleDialog.close();
|
||||
};
|
||||
|
||||
reset = () => {
|
||||
this.roleName = "";
|
||||
this.namespace = "";
|
||||
@ -73,7 +68,7 @@ export class AddRoleDialog extends React.Component<Props> {
|
||||
|
||||
showDetails(role.selfLink);
|
||||
this.reset();
|
||||
this.close();
|
||||
AddRoleDialog.close();
|
||||
} catch (err) {
|
||||
Notifications.error(err.toString());
|
||||
}
|
||||
@ -87,10 +82,10 @@ export class AddRoleDialog extends React.Component<Props> {
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
className="AddRoleDialog"
|
||||
isOpen={dialogState.isOpen}
|
||||
close={this.close}
|
||||
isOpen={AddRoleDialog.isOpen.get()}
|
||||
close={AddRoleDialog.close}
|
||||
>
|
||||
<Wizard header={header} done={this.close}>
|
||||
<Wizard header={header} done={AddRoleDialog.close}>
|
||||
<WizardStep
|
||||
contentClass="flex gaps column"
|
||||
nextLabel="Create"
|
||||
21
src/renderer/components/+user-management/+roles/details.scss
Normal file
21
src/renderer/components/+user-management/+roles/details.scss
Normal file
@ -0,0 +1,21 @@
|
||||
.RoleDetails {
|
||||
.rule {
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto;
|
||||
gap: $margin;
|
||||
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: $radius;
|
||||
padding: $padding * 1.5;
|
||||
|
||||
> .name {
|
||||
color: $textColorSecondary;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: $margin * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -19,16 +19,17 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./role-details.scss";
|
||||
import "./details.scss";
|
||||
|
||||
import React from "react";
|
||||
import { DrawerTitle } from "../drawer";
|
||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { observer } from "mobx-react";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object";
|
||||
import type { Role } from "../../api/endpoints";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import React from "react";
|
||||
|
||||
import { KubeEventDetails } from "../../+events/kube-event-details";
|
||||
import type { Role } from "../../../api/endpoints";
|
||||
import { kubeObjectDetailRegistry } from "../../../api/kube-object-detail-registry";
|
||||
import { DrawerTitle } from "../../drawer";
|
||||
import type { KubeObjectDetailsProps } from "../../kube-object";
|
||||
import { KubeObjectMeta } from "../../kube-object/kube-object-meta";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Role> {
|
||||
}
|
||||
@ -44,7 +45,6 @@ export class RoleDetails extends React.Component<Props> {
|
||||
return (
|
||||
<div className="RoleDetails">
|
||||
<KubeObjectMeta object={role}/>
|
||||
|
||||
<DrawerTitle title="Rules"/>
|
||||
{rules.map(({ resourceNames, apiGroups, resources, verbs }, index) => {
|
||||
return (
|
||||
@ -101,19 +101,3 @@ kubeObjectDetailRegistry.add({
|
||||
Details: (props) => <KubeEventDetails {...props} />
|
||||
}
|
||||
});
|
||||
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "ClusterRole",
|
||||
apiVersions: ["rbac.authorization.k8s.io/v1"],
|
||||
components: {
|
||||
Details: (props) => <RoleDetails {...props}/>
|
||||
}
|
||||
});
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "ClusterRole",
|
||||
apiVersions: ["rbac.authorization.k8s.io/v1"],
|
||||
priority: 5,
|
||||
components: {
|
||||
Details: (props) => <KubeEventDetails {...props}/>
|
||||
}
|
||||
});
|
||||
23
src/renderer/components/+user-management/+roles/index.ts
Normal file
23
src/renderer/components/+user-management/+roles/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
export * from "./view";
|
||||
export * from "./details";
|
||||
export * from "./add-dialog";
|
||||
48
src/renderer/components/+user-management/+roles/store.ts
Normal file
48
src/renderer/components/+user-management/+roles/store.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { apiManager } from "../../../api/api-manager";
|
||||
import { Role, roleApi } from "../../../api/endpoints";
|
||||
import { KubeObjectStore } from "../../../kube-object.store";
|
||||
import { autoBind } from "../../../utils";
|
||||
|
||||
export class RolesStore extends KubeObjectStore<Role> {
|
||||
api = roleApi;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
protected sortItems(items: Role[]) {
|
||||
return super.sortItems(items, [
|
||||
role => role.kind,
|
||||
role => role.getName(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<Role>) {
|
||||
return roleApi.create(params, data);
|
||||
}
|
||||
}
|
||||
|
||||
export const rolesStore = new RolesStore();
|
||||
|
||||
apiManager.registerStore(rolesStore);
|
||||
@ -19,17 +19,17 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./roles.scss";
|
||||
import "./view.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import type { IRolesRouteParams } from "../+user-management/user-management.route";
|
||||
import { rolesStore } from "./roles.store";
|
||||
import type { Role } from "../../api/endpoints";
|
||||
import { KubeObjectListLayout } from "../kube-object";
|
||||
import { AddRoleDialog } from "./add-role-dialog";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import type { Role } from "../../../api/endpoints";
|
||||
import { KubeObjectListLayout } from "../../kube-object";
|
||||
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
|
||||
import type { RolesRouteParams } from "../user-management.route";
|
||||
import { AddRoleDialog } from "./add-dialog";
|
||||
import { rolesStore } from "./store";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@ -37,7 +37,7 @@ enum columnId {
|
||||
age = "age",
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps<IRolesRouteParams> {
|
||||
interface Props extends RouteComponentProps<RolesRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
@ -68,7 +68,7 @@ export class Roles extends React.Component<Props> {
|
||||
renderTableContents={(role: Role) => [
|
||||
role.getName(),
|
||||
<KubeObjectStatusIcon key="icon" object={role} />,
|
||||
role.getNs() || "-",
|
||||
role.getNs(),
|
||||
role.getAge(),
|
||||
]}
|
||||
addRemoveButtons={{
|
||||
@ -20,4 +20,4 @@
|
||||
*/
|
||||
|
||||
.CreateServiceAccountDialog {
|
||||
}
|
||||
}
|
||||
@ -19,30 +19,29 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./create-service-account-dialog.scss";
|
||||
import "./create-dialog.scss";
|
||||
|
||||
import React from "react";
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { serviceAccountsStore } from "./service-accounts.store";
|
||||
import { Input } from "../input";
|
||||
import { systemName } from "../input/input_validators";
|
||||
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
||||
import { Notifications } from "../notifications";
|
||||
import { showDetails } from "../kube-object";
|
||||
|
||||
import { NamespaceSelect } from "../../+namespaces/namespace-select";
|
||||
import { Dialog, DialogProps } from "../../dialog";
|
||||
import { Input } from "../../input";
|
||||
import { systemName } from "../../input/input_validators";
|
||||
import { showDetails } from "../../kube-object";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { Notifications } from "../../notifications";
|
||||
import { Wizard, WizardStep } from "../../wizard";
|
||||
import { serviceAccountsStore } from "./store";
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
const dialogState = observable.object({
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
@observer
|
||||
export class CreateServiceAccountDialog extends React.Component<Props> {
|
||||
static isOpen = observable.box(false);
|
||||
|
||||
@observable name = "";
|
||||
@observable namespace = "default";
|
||||
|
||||
@ -52,17 +51,13 @@ export class CreateServiceAccountDialog extends React.Component<Props> {
|
||||
}
|
||||
|
||||
static open() {
|
||||
dialogState.isOpen = true;
|
||||
CreateServiceAccountDialog.isOpen.set(true);
|
||||
}
|
||||
|
||||
static close() {
|
||||
dialogState.isOpen = false;
|
||||
CreateServiceAccountDialog.isOpen.set(false);
|
||||
}
|
||||
|
||||
close = () => {
|
||||
CreateServiceAccountDialog.close();
|
||||
};
|
||||
|
||||
createAccount = async () => {
|
||||
const { name, namespace } = this;
|
||||
|
||||
@ -71,7 +66,7 @@ export class CreateServiceAccountDialog extends React.Component<Props> {
|
||||
|
||||
this.name = "";
|
||||
showDetails(serviceAccount.selfLink);
|
||||
this.close();
|
||||
CreateServiceAccountDialog.close();
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
}
|
||||
@ -86,10 +81,10 @@ export class CreateServiceAccountDialog extends React.Component<Props> {
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
className="CreateServiceAccountDialog"
|
||||
isOpen={dialogState.isOpen}
|
||||
close={this.close}
|
||||
isOpen={CreateServiceAccountDialog.isOpen.get()}
|
||||
close={CreateServiceAccountDialog.close}
|
||||
>
|
||||
<Wizard header={header} done={this.close}>
|
||||
<Wizard header={header} done={CreateServiceAccountDialog.close}>
|
||||
<WizardStep nextLabel="Create" next={this.createAccount}>
|
||||
<SubTitle title="Account Name" />
|
||||
<Input
|
||||
@ -25,4 +25,4 @@
|
||||
margin-right: $margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -19,22 +19,23 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./service-accounts-details.scss";
|
||||
import "./details.scss";
|
||||
|
||||
import React from "react";
|
||||
import { autorun, observable, makeObservable } from "mobx";
|
||||
import { Spinner } from "../spinner";
|
||||
import { ServiceAccountsSecret } from "./service-accounts-secret";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Secret, ServiceAccount } from "../../api/endpoints";
|
||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { Icon } from "../icon";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
|
||||
import { secretsStore } from "../../+config-secrets/secrets.store";
|
||||
import { KubeEventDetails } from "../../+events/kube-event-details";
|
||||
import { Secret, ServiceAccount } from "../../../api/endpoints";
|
||||
import { kubeObjectDetailRegistry } from "../../../api/kube-object-detail-registry";
|
||||
import { DrawerItem, DrawerTitle } from "../../drawer";
|
||||
import { Icon } from "../../icon";
|
||||
import { getDetailsUrl, KubeObjectDetailsProps } from "../../kube-object";
|
||||
import { KubeObjectMeta } from "../../kube-object/kube-object-meta";
|
||||
import { Spinner } from "../../spinner";
|
||||
import { ServiceAccountsSecret } from "./secret";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<ServiceAccount> {
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
export * from "./view";
|
||||
export * from "./details";
|
||||
export * from "./create-dialog";
|
||||
@ -19,13 +19,14 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./service-accounts-secret.scss";
|
||||
import "./secret.scss";
|
||||
|
||||
import React from "react";
|
||||
import moment from "moment";
|
||||
import { Icon } from "../icon";
|
||||
import type { Secret } from "../../api/endpoints/secret.api";
|
||||
import { prevDefault } from "../../utils";
|
||||
import React from "react";
|
||||
|
||||
import type { Secret } from "../../../api/endpoints/secret.api";
|
||||
import { prevDefault } from "../../../utils";
|
||||
import { Icon } from "../../icon";
|
||||
|
||||
interface Props {
|
||||
secret: Secret;
|
||||
@ -19,10 +19,10 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../utils";
|
||||
import { ServiceAccount, serviceAccountsApi } from "../../api/endpoints";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { apiManager } from "../../../api/api-manager";
|
||||
import { ServiceAccount, serviceAccountsApi } from "../../../api/endpoints";
|
||||
import { KubeObjectStore } from "../../../kube-object.store";
|
||||
import { autoBind } from "../../../utils";
|
||||
|
||||
export class ServiceAccountsStore extends KubeObjectStore<ServiceAccount> {
|
||||
api = serviceAccountsApi;
|
||||
@ -19,22 +19,22 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./service-accounts.scss";
|
||||
import "./view.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { ServiceAccount } from "../../api/endpoints/service-accounts.api";
|
||||
import React from "react";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import type { KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
||||
import { MenuItem } from "../menu";
|
||||
import { openServiceAccountKubeConfig } from "../kubeconfig-dialog";
|
||||
import { Icon } from "../icon";
|
||||
import { KubeObjectListLayout } from "../kube-object";
|
||||
import type { IServiceAccountsRouteParams } from "../+user-management";
|
||||
import { serviceAccountsStore } from "./service-accounts.store";
|
||||
import { CreateServiceAccountDialog } from "./create-service-account-dialog";
|
||||
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import type { ServiceAccountsRouteParams } from "../user-management.route";
|
||||
import { kubeObjectMenuRegistry } from "../../../../extensions/registries/kube-object-menu-registry";
|
||||
import type { ServiceAccount } from "../../../api/endpoints/service-accounts.api";
|
||||
import { Icon } from "../../icon";
|
||||
import { KubeObjectListLayout } from "../../kube-object";
|
||||
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
|
||||
import type { KubeObjectMenuProps } from "../../kube-object/kube-object-menu";
|
||||
import { openServiceAccountKubeConfig } from "../../kubeconfig-dialog";
|
||||
import { MenuItem } from "../../menu";
|
||||
import { CreateServiceAccountDialog } from "./create-dialog";
|
||||
import { serviceAccountsStore } from "./store";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@ -42,7 +42,7 @@ enum columnId {
|
||||
age = "age",
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps<IServiceAccountsRouteParams> {
|
||||
interface Props extends RouteComponentProps<ServiceAccountsRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
49
src/renderer/components/+user-management/select-options.tsx
Normal file
49
src/renderer/components/+user-management/select-options.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import type { ServiceAccount } from "../../api/endpoints";
|
||||
import type { KubeObject } from "../../api/kube-object";
|
||||
import { Icon } from "../icon";
|
||||
import type { SelectOption } from "../select";
|
||||
import { TooltipPosition } from "../tooltip";
|
||||
|
||||
export type ServiceAccountOption = SelectOption<string> & { account: ServiceAccount };
|
||||
|
||||
export function getRoleRefSelectOption<T extends KubeObject>(item: T): SelectOption<T> {
|
||||
return {
|
||||
value: item,
|
||||
label: (
|
||||
<>
|
||||
<Icon
|
||||
small
|
||||
material={item.kind === "Role" ? "person" : "people"}
|
||||
tooltip={{
|
||||
preferredPositions: TooltipPosition.LEFT,
|
||||
children: item.kind
|
||||
}}
|
||||
/>
|
||||
{" "}
|
||||
{item.getName()}
|
||||
</>
|
||||
),
|
||||
};
|
||||
}
|
||||
@ -19,45 +19,62 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL, IURLParams } from "../../../common/utils/buildUrl";
|
||||
|
||||
import type { RouteProps } from "react-router";
|
||||
|
||||
// Routes
|
||||
export const serviceAccountsRoute: RouteProps = {
|
||||
path: "/service-accounts"
|
||||
};
|
||||
export const podSecurityPoliciesRoute: RouteProps = {
|
||||
path: "/pod-security-policies"
|
||||
};
|
||||
export const rolesRoute: RouteProps = {
|
||||
path: "/roles"
|
||||
};
|
||||
export const clusterRolesRoute: RouteProps = {
|
||||
path: "/cluster-roles"
|
||||
};
|
||||
export const roleBindingsRoute: RouteProps = {
|
||||
path: "/role-bindings"
|
||||
};
|
||||
export const podSecurityPoliciesRoute: RouteProps = {
|
||||
path: "/pod-security-policies"
|
||||
export const clusterRoleBindingsRoute: RouteProps = {
|
||||
path: "/cluster-role-bindings"
|
||||
};
|
||||
|
||||
export const usersManagementRoute: RouteProps = {
|
||||
path: [
|
||||
serviceAccountsRoute,
|
||||
podSecurityPoliciesRoute,
|
||||
roleBindingsRoute,
|
||||
clusterRoleBindingsRoute,
|
||||
rolesRoute,
|
||||
podSecurityPoliciesRoute
|
||||
clusterRolesRoute,
|
||||
].map(route => route.path.toString())
|
||||
};
|
||||
|
||||
// Route params
|
||||
export interface IServiceAccountsRouteParams {
|
||||
export interface ServiceAccountsRouteParams {
|
||||
}
|
||||
|
||||
export interface IRoleBindingsRouteParams {
|
||||
export interface RoleBindingsRouteParams {
|
||||
}
|
||||
|
||||
export interface IRolesRouteParams {
|
||||
export interface ClusterRoleBindingsRouteParams {
|
||||
}
|
||||
|
||||
export interface RolesRouteParams {
|
||||
}
|
||||
|
||||
export interface ClusterRolesRouteParams {
|
||||
}
|
||||
|
||||
// URL-builders
|
||||
export const usersManagementURL = (params?: IURLParams) => serviceAccountsURL(params);
|
||||
export const serviceAccountsURL = buildURL<IServiceAccountsRouteParams>(serviceAccountsRoute.path);
|
||||
export const roleBindingsURL = buildURL<IRoleBindingsRouteParams>(roleBindingsRoute.path);
|
||||
export const rolesURL = buildURL<IRoleBindingsRouteParams>(rolesRoute.path);
|
||||
export const serviceAccountsURL = buildURL<ServiceAccountsRouteParams>(serviceAccountsRoute.path);
|
||||
export const podSecurityPoliciesURL = buildURL(podSecurityPoliciesRoute.path);
|
||||
export const roleBindingsURL = buildURL<RoleBindingsRouteParams>(roleBindingsRoute.path);
|
||||
export const clusterRoleBindingsURL = buildURL<ClusterRoleBindingsRouteParams>(clusterRoleBindingsRoute.path);
|
||||
export const rolesURL = buildURL<RoleBindingsRouteParams>(rolesRoute.path);
|
||||
export const clusterRolesURL = buildURL<ClusterRoleBindingsRouteParams>(clusterRolesRoute.path);
|
||||
|
||||
@ -20,15 +20,32 @@
|
||||
*/
|
||||
|
||||
import "./user-management.scss";
|
||||
import React from "react";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
||||
import { Roles } from "../+user-management-roles";
|
||||
import { RoleBindings } from "../+user-management-roles-bindings";
|
||||
import { ServiceAccounts } from "../+user-management-service-accounts";
|
||||
import { podSecurityPoliciesRoute, podSecurityPoliciesURL, roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL } from "./user-management.route";
|
||||
import React from "react";
|
||||
|
||||
import { PodSecurityPolicies } from "../+pod-security-policies";
|
||||
import { isAllowedResource } from "../../../common/rbac";
|
||||
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
||||
import { ClusterRoles } from "./+cluster-roles";
|
||||
import { ClusterRoleBindings } from "./+cluster-role-bindings";
|
||||
import { Roles } from "./+roles";
|
||||
import { RoleBindings } from "./+role-bindings";
|
||||
import { ServiceAccounts } from "./+service-accounts";
|
||||
import {
|
||||
clusterRoleBindingsRoute,
|
||||
clusterRoleBindingsURL,
|
||||
clusterRolesRoute,
|
||||
clusterRolesURL,
|
||||
podSecurityPoliciesRoute,
|
||||
podSecurityPoliciesURL,
|
||||
roleBindingsRoute,
|
||||
roleBindingsURL,
|
||||
rolesRoute,
|
||||
rolesURL,
|
||||
serviceAccountsRoute,
|
||||
serviceAccountsURL,
|
||||
} from "./user-management.route";
|
||||
|
||||
@observer
|
||||
export class UserManagement extends React.Component {
|
||||
@ -44,18 +61,16 @@ export class UserManagement extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("rolebindings") || isAllowedResource("clusterrolebindings")) {
|
||||
// TODO: seperate out these two pages
|
||||
if (isAllowedResource("clusterroles")) {
|
||||
tabRoutes.push({
|
||||
title: "Role Bindings",
|
||||
component: RoleBindings,
|
||||
url: roleBindingsURL(),
|
||||
routePath: roleBindingsRoute.path.toString(),
|
||||
title: "Cluster Roles",
|
||||
component: ClusterRoles,
|
||||
url: clusterRolesURL(),
|
||||
routePath: clusterRolesRoute.path.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("roles") || isAllowedResource("clusterroles")) {
|
||||
// TODO: seperate out these two pages
|
||||
if (isAllowedResource("roles")) {
|
||||
tabRoutes.push({
|
||||
title: "Roles",
|
||||
component: Roles,
|
||||
@ -64,6 +79,24 @@ export class UserManagement extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("clusterrolebindings")) {
|
||||
tabRoutes.push({
|
||||
title: "Cluster Role Bindings",
|
||||
component: ClusterRoleBindings,
|
||||
url: clusterRoleBindingsURL(),
|
||||
routePath: clusterRoleBindingsRoute.path.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("rolebindings")) {
|
||||
tabRoutes.push({
|
||||
title: "Role Bindings",
|
||||
component: RoleBindings,
|
||||
url: roleBindingsURL(),
|
||||
routePath: roleBindingsRoute.path.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("podsecuritypolicies")) {
|
||||
tabRoutes.push({
|
||||
title: "Pod Security Policies",
|
||||
|
||||
@ -41,7 +41,6 @@ import { Events } from "./+events/events";
|
||||
import { eventRoute } from "./+events";
|
||||
import { Apps, appsRoute } from "./+apps";
|
||||
import { KubeObjectDetails } from "./kube-object/kube-object-details";
|
||||
import { AddRoleBindingDialog } from "./+user-management-roles-bindings";
|
||||
import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale-dialog";
|
||||
import { CronJobTriggerDialog } from "./+workloads-cronjobs/cronjob-trigger-dialog";
|
||||
import { CustomResources } from "./+custom-resources/custom-resources";
|
||||
@ -201,7 +200,6 @@ export class App extends React.Component {
|
||||
<ConfirmDialog/>
|
||||
<KubeObjectDetails/>
|
||||
<KubeConfigDialog/>
|
||||
<AddRoleBindingDialog/>
|
||||
<DeploymentScaleDialog/>
|
||||
<StatefulSetScaleDialog/>
|
||||
<ReplicaSetScaleDialog/>
|
||||
|
||||
@ -23,7 +23,7 @@ import "./dock-tab.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { boundMethod, cssNames, prevDefault } from "../../utils";
|
||||
import { boundMethod, cssNames, prevDefault, isMiddleClick } from "../../utils";
|
||||
import { dockStore, IDockTab } from "./dock.store";
|
||||
import { Tab, TabProps } from "../tabs";
|
||||
import { Icon } from "../icon";
|
||||
@ -88,13 +88,13 @@ export class DockTab extends React.Component<DockTabProps> {
|
||||
const { className, moreActions, ...tabProps } = this.props;
|
||||
const { title, pinned } = tabProps.value;
|
||||
const label = (
|
||||
<div className="flex gaps align-center">
|
||||
<div className="flex gaps align-center" onAuxClick={isMiddleClick(prevDefault(this.close))}>
|
||||
<span className="title" title={title}>{title}</span>
|
||||
{moreActions}
|
||||
{!pinned && (
|
||||
<Icon
|
||||
small material="close"
|
||||
title="Close (Ctrl+W)"
|
||||
title="Close (Ctrl+Shift+W)"
|
||||
onClick={prevDefault(this.close)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -107,7 +107,7 @@ export class LogStore {
|
||||
});
|
||||
|
||||
// Add newly received logs to bottom
|
||||
this.podLogs.set(tabId, [...oldLogs, ...logs]);
|
||||
this.podLogs.set(tabId, [...oldLogs, ...logs.filter(Boolean)]);
|
||||
} catch (error) {
|
||||
this.handlerError(tabId, error);
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
.el-contents {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: $padding * 2;
|
||||
margin: $padding 0px;
|
||||
|
||||
.el-value-remove {
|
||||
.Icon {
|
||||
@ -35,7 +35,9 @@
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
padding: $padding $padding * 2;
|
||||
margin-bottom: 1px;
|
||||
margin-bottom: $padding / 4;
|
||||
backdrop-filter: brightness(0.75);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
:last-child {
|
||||
margin-bottom: unset;
|
||||
@ -46,4 +48,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,11 +21,11 @@
|
||||
|
||||
import "./editable-list.scss";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
|
||||
import { Icon } from "../icon";
|
||||
import { Input } from "../input";
|
||||
import { observable, makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { boundMethod } from "../../utils";
|
||||
|
||||
export interface Props<T> {
|
||||
@ -47,20 +47,14 @@ const defaultProps: Partial<Props<any>> = {
|
||||
@observer
|
||||
export class EditableList<T> extends React.Component<Props<T>> {
|
||||
static defaultProps = defaultProps as Props<any>;
|
||||
@observable currentNewItem = "";
|
||||
|
||||
constructor(props: Props<T>) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
onSubmit(val: string) {
|
||||
onSubmit(val: string, evt: React.KeyboardEvent) {
|
||||
const { add } = this.props;
|
||||
|
||||
if (val) {
|
||||
evt.preventDefault();
|
||||
add(val);
|
||||
this.currentNewItem = "";
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,17 +65,15 @@ export class EditableList<T> extends React.Component<Props<T>> {
|
||||
<div className="EditableList">
|
||||
<div className="el-header">
|
||||
<Input
|
||||
theme="round-black"
|
||||
value={this.currentNewItem}
|
||||
theme="round"
|
||||
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 key={`${item}${index}`} className="el-item">
|
||||
<div>{renderItem(item, index)}</div>
|
||||
<div className="el-value-remove">
|
||||
<Icon material="delete_outline" onClick={() => remove(({ index, oldItem: item }))} />
|
||||
|
||||
@ -106,7 +106,7 @@ export class HotbarEntityIcon extends React.Component<Props> {
|
||||
};
|
||||
const isActive = this.isActive(entity);
|
||||
const isPersisted = this.isPersisted(entity);
|
||||
const menuItems = this.contextMenu?.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === entity.metadata.source);
|
||||
const menuItems = this.contextMenu?.menuItems ?? [];
|
||||
|
||||
if (!isPersisted) {
|
||||
menuItems.unshift({
|
||||
|
||||
@ -48,7 +48,7 @@
|
||||
--flex-gap: #{$padding / 1.5};
|
||||
|
||||
position: relative;
|
||||
padding: $padding /4 * 3 0;
|
||||
padding: $padding / 4 * 3 0;
|
||||
border-bottom: 1px solid $halfGray;
|
||||
line-height: 1;
|
||||
|
||||
@ -110,23 +110,17 @@
|
||||
//- Themes
|
||||
|
||||
&.theme {
|
||||
&.round-black {
|
||||
&.round {
|
||||
&.invalid.dirty {
|
||||
label {
|
||||
border-color: $colorSoftError;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
background: var(--inputControlBackground);
|
||||
border: 1px solid var(--inputControlBorder);
|
||||
border-radius: 5px;
|
||||
padding: $padding;
|
||||
color: var(--textColorTertiary);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--inputControlHoverBorder);
|
||||
}
|
||||
border-radius: $radius;
|
||||
border: 1px solid $halfGray;
|
||||
color: inherit;
|
||||
padding: $padding / 4 * 3 $padding / 4 * 3;
|
||||
|
||||
&:focus-within {
|
||||
border-color: $colorInfo;
|
||||
@ -136,6 +130,18 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&.black {
|
||||
label {
|
||||
background: var(--inputControlBackground);
|
||||
border-color: var(--inputControlBorder);
|
||||
color: var(--textColorTertiary);
|
||||
padding: $padding;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--inputControlHoverBorder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ type InputElement = HTMLInputElement | HTMLTextAreaElement;
|
||||
type InputElementProps = InputHTMLAttributes<InputElement> & TextareaHTMLAttributes<InputElement> & DOMAttributes<InputElement>;
|
||||
|
||||
export type InputProps<T = string> = Omit<InputElementProps, "onChange" | "onSubmit"> & {
|
||||
theme?: "round-black";
|
||||
theme?: "round-black" | "round";
|
||||
className?: string;
|
||||
value?: T;
|
||||
autoSelectOnFocus?: boolean
|
||||
@ -55,7 +55,7 @@ export type InputProps<T = string> = Omit<InputElementProps, "onChange" | "onSub
|
||||
contentRight?: string | React.ReactNode; // Any component of string goes after iconRight
|
||||
validators?: InputValidator | InputValidator[];
|
||||
onChange?(value: T, evt: React.ChangeEvent<InputElement>): void;
|
||||
onSubmit?(value: T): void;
|
||||
onSubmit?(value: T, evt: React.KeyboardEvent<InputElement>): void;
|
||||
};
|
||||
|
||||
interface State {
|
||||
@ -90,7 +90,7 @@ export class Input extends React.Component<InputProps, State> {
|
||||
return this.state.valid;
|
||||
}
|
||||
|
||||
setValue(value: string) {
|
||||
setValue(value = "") {
|
||||
if (value !== this.getValue()) {
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(this.input.constructor.prototype, "value").set;
|
||||
|
||||
@ -236,16 +236,15 @@ export class Input extends React.Component<InputProps, State> {
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
onChange(evt: React.ChangeEvent<any>) {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(evt.currentTarget.value, evt);
|
||||
}
|
||||
|
||||
onChange(evt: React.ChangeEvent<InputElement>) {
|
||||
this.props.onChange?.(evt.currentTarget.value, evt);
|
||||
this.validate();
|
||||
this.autoFitHeight();
|
||||
|
||||
// mark input as dirty for the first time only onBlur() to avoid immediate error-state show when start typing
|
||||
if (!this.state.dirty) this.setState({ dirtyOnBlur: true });
|
||||
if (!this.state.dirty) {
|
||||
this.setState({ dirtyOnBlur: true });
|
||||
}
|
||||
|
||||
// re-render component when used as uncontrolled input
|
||||
// when used @defaultValue instead of @value changing real input.value doesn't call render()
|
||||
@ -255,17 +254,19 @@ export class Input extends React.Component<InputProps, State> {
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
onKeyDown(evt: React.KeyboardEvent<any>) {
|
||||
onKeyDown(evt: React.KeyboardEvent<InputElement>) {
|
||||
const modified = evt.shiftKey || evt.metaKey || evt.altKey || evt.ctrlKey;
|
||||
|
||||
if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown(evt);
|
||||
}
|
||||
this.props.onKeyDown?.(evt);
|
||||
|
||||
switch (evt.key) {
|
||||
case "Enter":
|
||||
if (this.props.onSubmit && !modified && !evt.repeat && this.isValid) {
|
||||
this.props.onSubmit(this.getValue());
|
||||
this.props.onSubmit(this.getValue(), evt);
|
||||
|
||||
if (this.isUncontrolled) {
|
||||
this.setValue();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -303,6 +304,20 @@ export class Input extends React.Component<InputProps, State> {
|
||||
}
|
||||
}
|
||||
|
||||
get themeSelection(): Record<string, boolean> {
|
||||
const { theme } = this.props;
|
||||
|
||||
if (!theme) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
theme: true,
|
||||
round: true,
|
||||
black: theme === "round-black",
|
||||
};
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
bindRef(elem: InputElement) {
|
||||
this.input = elem;
|
||||
@ -318,7 +333,7 @@ export class Input extends React.Component<InputProps, State> {
|
||||
const { focused, dirty, valid, validating, errors } = this.state;
|
||||
|
||||
const className = cssNames("Input", this.props.className, {
|
||||
[`theme ${theme}`]: theme,
|
||||
...this.themeSelection,
|
||||
focused,
|
||||
disabled,
|
||||
invalid: !valid,
|
||||
|
||||
@ -26,10 +26,11 @@ import "./select.scss";
|
||||
import React, { ReactNode } from "react";
|
||||
import { computed, makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { boundMethod, cssNames } from "../../utils";
|
||||
import ReactSelect, { ActionMeta, components, OptionTypeBase, Props as ReactSelectProps, Styles } from "react-select";
|
||||
import Creatable, { CreatableProps } from "react-select/creatable";
|
||||
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { boundMethod, cssNames } from "../../utils";
|
||||
|
||||
const { Menu } = components;
|
||||
|
||||
@ -65,8 +66,10 @@ export class Select extends React.Component<SelectProps> {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@computed get theme() {
|
||||
return this.props.themeName || ThemeStore.getInstance().activeTheme.type;
|
||||
@computed get themeClass() {
|
||||
const themeName = this.props.themeName || ThemeStore.getInstance().activeTheme.type;
|
||||
|
||||
return `theme-${themeName}`;
|
||||
}
|
||||
|
||||
private styles: Styles<OptionTypeBase, boolean> = {
|
||||
@ -128,7 +131,6 @@ export class Select extends React.Component<SelectProps> {
|
||||
className, menuClass, isCreatable, autoConvertOptions,
|
||||
value, options, components = {}, ...props
|
||||
} = this.props;
|
||||
const themeClass = `theme-${this.theme}`;
|
||||
const WrappedMenu = components.Menu ?? Menu;
|
||||
|
||||
const selectProps: Partial<SelectProps> = {
|
||||
@ -138,14 +140,14 @@ export class Select extends React.Component<SelectProps> {
|
||||
options: autoConvertOptions ? this.options : options,
|
||||
onChange: this.onChange,
|
||||
onKeyDown: this.onKeyDown,
|
||||
className: cssNames("Select", themeClass, className),
|
||||
className: cssNames("Select", this.themeClass, className),
|
||||
classNamePrefix: "Select",
|
||||
components: {
|
||||
...components,
|
||||
Menu: props => (
|
||||
<WrappedMenu
|
||||
{...props}
|
||||
className={cssNames(menuClass, themeClass, props.className)}
|
||||
className={cssNames(menuClass, this.themeClass, props.className)}
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
import type { ClusterContext } from "./components/context";
|
||||
|
||||
import { action, computed, makeObservable, observable, reaction, when } from "mobx";
|
||||
import { autoBind, bifurcateArray, noop, rejectPromiseBy } from "./utils";
|
||||
import { autoBind, noop, rejectPromiseBy } from "./utils";
|
||||
import { KubeObject, KubeStatus } from "./api/kube-object";
|
||||
import type { IKubeWatchEvent } from "./api/kube-watch-api";
|
||||
import { ItemStore } from "./item.store";
|
||||
@ -309,51 +309,31 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
||||
});
|
||||
}
|
||||
|
||||
getSubscribeApis(): KubeApi[] {
|
||||
return [this.api];
|
||||
}
|
||||
|
||||
subscribe(apis = this.getSubscribeApis()) {
|
||||
subscribe() {
|
||||
const abortController = new AbortController();
|
||||
const [clusterScopedApis, namespaceScopedApis] = bifurcateArray(apis, api => api.isNamespaced);
|
||||
|
||||
for (const api of namespaceScopedApis) {
|
||||
const store = apiManager.getStore(api);
|
||||
|
||||
// This waits for the context and namespaces to be ready or fails fast if the disposer is called
|
||||
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([store.contextReady, store.namespacesReady])])
|
||||
if (this.api.isNamespaced) {
|
||||
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])])
|
||||
.then(() => {
|
||||
if (
|
||||
store.context.cluster.isGlobalWatchEnabled
|
||||
&& store.loadedNamespaces.length === 0
|
||||
) {
|
||||
return store.watchNamespace(api, "", abortController);
|
||||
if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) {
|
||||
return this.watchNamespace("", abortController);
|
||||
}
|
||||
|
||||
for (const namespace of this.loadedNamespaces) {
|
||||
store.watchNamespace(api, namespace, abortController);
|
||||
this.watchNamespace(namespace, abortController);
|
||||
}
|
||||
})
|
||||
.catch(noop); // ignore DOMExceptions
|
||||
} else {
|
||||
this.watchNamespace("", abortController);
|
||||
}
|
||||
|
||||
for (const api of clusterScopedApis) {
|
||||
/**
|
||||
* if the api is cluster scoped then we will never assign to `loadedNamespaces`
|
||||
* and thus `store.namespacesReady` will never resolve. Futhermore, we don't care
|
||||
* about watching namespaces.
|
||||
*/
|
||||
apiManager.getStore(api).watchNamespace(api, "", abortController);
|
||||
}
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
return () => abortController.abort();
|
||||
}
|
||||
|
||||
private watchNamespace(api: KubeApi<T>, namespace: string, abortController: AbortController) {
|
||||
private watchNamespace(namespace: string, abortController: AbortController) {
|
||||
let timedRetry: NodeJS.Timeout;
|
||||
const watch = () => api.watch({
|
||||
const watch = () => this.api.watch({
|
||||
namespace,
|
||||
abortController,
|
||||
callback
|
||||
|
||||
@ -38,3 +38,4 @@ export * from "./convertMemory";
|
||||
export * from "./convertCpu";
|
||||
export * from "./metricUnitsToNumber";
|
||||
export * from "./display-booleans";
|
||||
export * from "./isMiddleClick";
|
||||
|
||||
36
src/renderer/utils/isMiddleClick.ts
Normal file
36
src/renderer/utils/isMiddleClick.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type React from "react";
|
||||
|
||||
// Helper for inlining middleClick checks
|
||||
// <form onAuxClick={isMiddleClick(() => console.log('do some action'))}>
|
||||
// <input name="text"/>
|
||||
// <button type="submit">Action</button>
|
||||
// </form>
|
||||
|
||||
export function isMiddleClick<E extends React.MouseEvent>(callback: (evt: E) => any) {
|
||||
return function (evt: E) {
|
||||
if(evt.button === 1) {
|
||||
return callback(evt);
|
||||
}
|
||||
};
|
||||
}
|
||||
151
yarn.lock
151
yarn.lock
@ -1034,10 +1034,10 @@
|
||||
lz-string "^1.4.4"
|
||||
pretty-format "^26.6.2"
|
||||
|
||||
"@testing-library/jest-dom@^5.11.10":
|
||||
version "5.11.10"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.10.tgz#1cd90715023e1627f5ed26ab3b38e6f22d77046c"
|
||||
integrity sha512-FuKiq5xuk44Fqm0000Z9w0hjOdwZRNzgx7xGGxQYepWFZy+OYUMOT/wPI4nLYXCaVltNVpU1W/qmD88wLWDsqQ==
|
||||
"@testing-library/jest-dom@^5.13.0":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.13.0.tgz#0a365684e2c1159f857f5915be50089fc5657df0"
|
||||
integrity sha512-+jXXTn8GjRnZkJfzG/tqK/2Q7dGlBInR412WE7Aml7CT3wdSpx5dMQC0HOwVQoZ3cNTmQUy8fCVGUV/Zhoyvcw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.2"
|
||||
"@types/testing-library__jest-dom" "^5.9.1"
|
||||
@ -4419,24 +4419,21 @@ css-element-queries@^1.2.3:
|
||||
resolved "https://registry.yarnpkg.com/css-element-queries/-/css-element-queries-1.2.3.tgz#e14940b1fcd4bf0da60ea4145d05742d7172e516"
|
||||
integrity sha512-QK9uovYmKTsV2GXWQiMOByVNrLn2qz6m3P7vWpOR4IdD6I3iXoDw5qtgJEN3Xq7gIbdHVKvzHjdAtcl+4Arc4Q==
|
||||
|
||||
css-loader@^3.5.3:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645"
|
||||
integrity sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==
|
||||
css-loader@^5.2.6:
|
||||
version "5.2.6"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.6.tgz#c3c82ab77fea1f360e587d871a6811f4450cc8d1"
|
||||
integrity sha512-0wyN5vXMQZu6BvjbrPdUJvkCzGEO24HC7IS7nW4llc6BBFC+zwR9CKtYGv63Puzsg10L/o12inMY5/2ByzfD6w==
|
||||
dependencies:
|
||||
camelcase "^5.3.1"
|
||||
cssesc "^3.0.0"
|
||||
icss-utils "^4.1.1"
|
||||
loader-utils "^1.2.3"
|
||||
normalize-path "^3.0.0"
|
||||
postcss "^7.0.32"
|
||||
postcss-modules-extract-imports "^2.0.0"
|
||||
postcss-modules-local-by-default "^3.0.2"
|
||||
postcss-modules-scope "^2.2.0"
|
||||
postcss-modules-values "^3.0.0"
|
||||
icss-utils "^5.1.0"
|
||||
loader-utils "^2.0.0"
|
||||
postcss "^8.2.15"
|
||||
postcss-modules-extract-imports "^3.0.0"
|
||||
postcss-modules-local-by-default "^4.0.0"
|
||||
postcss-modules-scope "^3.0.0"
|
||||
postcss-modules-values "^4.0.0"
|
||||
postcss-value-parser "^4.1.0"
|
||||
schema-utils "^2.7.0"
|
||||
semver "^6.3.0"
|
||||
schema-utils "^3.0.0"
|
||||
semver "^7.3.5"
|
||||
|
||||
css-parse@~2.0.0:
|
||||
version "2.0.0"
|
||||
@ -7308,13 +7305,18 @@ icss-utils@^3.0.1:
|
||||
dependencies:
|
||||
postcss "^6.0.2"
|
||||
|
||||
icss-utils@^4.0.0, icss-utils@^4.1.1:
|
||||
icss-utils@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
|
||||
integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==
|
||||
dependencies:
|
||||
postcss "^7.0.14"
|
||||
|
||||
icss-utils@^5.0.0, icss-utils@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
|
||||
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
|
||||
|
||||
identity-obj-proxy@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14"
|
||||
@ -10020,10 +10022,10 @@ nan@^2.12.1, nan@^2.13.2, nan@^2.14.0:
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
|
||||
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
|
||||
|
||||
nanoid@^3.1.22:
|
||||
version "3.1.22"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844"
|
||||
integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==
|
||||
nanoid@^3.1.23:
|
||||
version "3.1.23"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
|
||||
integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
|
||||
|
||||
nanomatch@^1.2.9:
|
||||
version "1.2.13"
|
||||
@ -11467,38 +11469,33 @@ postcss-loader@~3.0.0:
|
||||
postcss-load-config "^2.0.0"
|
||||
schema-utils "^1.0.0"
|
||||
|
||||
postcss-modules-extract-imports@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e"
|
||||
integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==
|
||||
dependencies:
|
||||
postcss "^7.0.5"
|
||||
postcss-modules-extract-imports@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
|
||||
integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
|
||||
|
||||
postcss-modules-local-by-default@^3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0"
|
||||
integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==
|
||||
postcss-modules-local-by-default@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c"
|
||||
integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==
|
||||
dependencies:
|
||||
icss-utils "^4.1.1"
|
||||
postcss "^7.0.32"
|
||||
icss-utils "^5.0.0"
|
||||
postcss-selector-parser "^6.0.2"
|
||||
postcss-value-parser "^4.1.0"
|
||||
|
||||
postcss-modules-scope@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee"
|
||||
integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==
|
||||
dependencies:
|
||||
postcss "^7.0.6"
|
||||
postcss-selector-parser "^6.0.0"
|
||||
|
||||
postcss-modules-values@^3.0.0:
|
||||
postcss-modules-scope@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10"
|
||||
integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
|
||||
integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==
|
||||
dependencies:
|
||||
icss-utils "^4.0.0"
|
||||
postcss "^7.0.6"
|
||||
postcss-selector-parser "^6.0.4"
|
||||
|
||||
postcss-modules-values@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
|
||||
integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==
|
||||
dependencies:
|
||||
icss-utils "^5.0.0"
|
||||
|
||||
postcss-nested@5.0.5:
|
||||
version "5.0.5"
|
||||
@ -11507,14 +11504,6 @@ postcss-nested@5.0.5:
|
||||
dependencies:
|
||||
postcss-selector-parser "^6.0.4"
|
||||
|
||||
postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.4:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.5.tgz#042d74e137db83e6f294712096cb413f5aa612c4"
|
||||
integrity sha512-aFYPoYmXbZ1V6HZaSvat08M97A8HqO6Pjz+PiNpw/DhuRrC72XWAdp3hL6wusDCN31sSmcZyMGa2hZEuX+Xfhg==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-selector-parser@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
|
||||
@ -11524,6 +11513,14 @@ postcss-selector-parser@^6.0.2:
|
||||
indexes-of "^1.0.1"
|
||||
uniq "^1.0.1"
|
||||
|
||||
postcss-selector-parser@^6.0.4:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.5.tgz#042d74e137db83e6f294712096cb413f5aa612c4"
|
||||
integrity sha512-aFYPoYmXbZ1V6HZaSvat08M97A8HqO6Pjz+PiNpw/DhuRrC72XWAdp3hL6wusDCN31sSmcZyMGa2hZEuX+Xfhg==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-value-parser@^3.3.0:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
|
||||
@ -11543,7 +11540,7 @@ postcss@^6.0.14, postcss@^6.0.2, postcss@^6.0.9:
|
||||
source-map "^0.6.1"
|
||||
supports-color "^5.4.0"
|
||||
|
||||
postcss@^7.0.0, postcss@^7.0.14, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6:
|
||||
postcss@^7.0.0, postcss@^7.0.14, postcss@^7.0.27:
|
||||
version "7.0.35"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24"
|
||||
integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==
|
||||
@ -11552,14 +11549,14 @@ postcss@^7.0.0, postcss@^7.0.14, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.
|
||||
source-map "^0.6.1"
|
||||
supports-color "^6.1.0"
|
||||
|
||||
postcss@^8.1.6, postcss@^8.2.1, postcss@^8.2.14:
|
||||
version "8.2.14"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.14.tgz#dcf313eb8247b3ce8078d048c0e8262ca565ad2b"
|
||||
integrity sha512-+jD0ZijcvyCqPQo/m/CW0UcARpdFylq04of+Q7RKX6f/Tu+dvpUI/9Sp81+i6/vJThnOBX09Quw0ZLOVwpzX3w==
|
||||
postcss@^8.1.6, postcss@^8.2.1, postcss@^8.2.14, postcss@^8.2.15:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.0.tgz#b1a713f6172ca427e3f05ef1303de8b65683325f"
|
||||
integrity sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==
|
||||
dependencies:
|
||||
colorette "^1.2.2"
|
||||
nanoid "^3.1.22"
|
||||
source-map "^0.6.1"
|
||||
nanoid "^3.1.23"
|
||||
source-map-js "^0.6.2"
|
||||
|
||||
postinstall-postinstall@^2.1.0:
|
||||
version "2.1.0"
|
||||
@ -12830,7 +12827,7 @@ schema-utils@1.0.0, schema-utils@^1.0.0:
|
||||
ajv-errors "^1.0.0"
|
||||
ajv-keywords "^3.1.0"
|
||||
|
||||
schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.7.0:
|
||||
schema-utils@^2.6.1, schema-utils@^2.6.5:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
|
||||
integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
|
||||
@ -12892,23 +12889,18 @@ semver-diff@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
|
||||
semver@7.x, semver@^7.3.2:
|
||||
version "7.3.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
|
||||
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
|
||||
|
||||
semver@^6.0.0, semver@^6.2.0, semver@^6.3.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
semver@^7.2.1, semver@^7.3.4:
|
||||
semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
|
||||
version "7.3.5"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
|
||||
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@^6.0.0, semver@^6.2.0, semver@^6.3.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
semver@~5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
|
||||
@ -13278,6 +13270,11 @@ source-list-map@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
|
||||
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
|
||||
|
||||
source-map-js@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
|
||||
integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
|
||||
|
||||
source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user