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

Starting attempt to move kubeconfig sync tests to application builder

- Blocked on catalog sync not being injectable yet

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-01-18 11:38:45 -05:00
parent d2ff35551c
commit 6f8de6ab80
15 changed files with 1141 additions and 44 deletions

View File

@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../../test-utils/get-global-override";
import { getGlobalOverride } from "../test-utils/get-global-override";
import watchInjectable from "./watch.injectable";
export default getGlobalOverride(watchInjectable, () => () => {

View File

@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import { watch } from "chokidar";
import type { Stats } from "fs";
import type TypedEventEmitter from "typed-emitter";
import type { SingleOrMany } from "../../utils";
import type { SingleOrMany } from "../utils";
export interface AlwaysStatWatcherEvents {
add: (path: string, stats: Stats) => void;

View File

@ -13,7 +13,7 @@ import extensionPackageRootDirectoryInjectable from "../extension-installer/exte
import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
import loggerInjectable from "../../common/logger.injectable";
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
import watchInjectable from "../../common/fs/watch/watch.injectable";
import watchInjectable from "../../common/fs/watch.injectable";
import accessPathInjectable from "../../common/fs/access-path.injectable";
import copyInjectable from "../../common/fs/copy.injectable";
import ensureDirInjectable from "../../common/fs/ensure-dir.injectable";

View File

@ -13,7 +13,7 @@ import { delay } from "../../renderer/utils";
import { observable, runInAction, when } from "mobx";
import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
import watchInjectable from "../../common/fs/watch/watch.injectable";
import watchInjectable from "../../common/fs/watch.injectable";
import extensionApiVersionInjectable from "../../common/vars/extension-api-version.injectable";
import removePathInjectable from "../../common/fs/remove.injectable";
import type { JoinPaths } from "../../common/path/join-paths.injectable";

View File

@ -17,7 +17,7 @@ import { requestInitialExtensionDiscovery } from "../../renderer/ipc";
import type { ReadJson } from "../../common/fs/read-json-file.injectable";
import type { Logger } from "../../common/logger";
import type { PathExists } from "../../common/fs/path-exists.injectable";
import type { Watch } from "../../common/fs/watch/watch.injectable";
import type { Watch } from "../../common/fs/watch.injectable";
import type { Stats } from "fs";
import type { LStat } from "../../common/fs/lstat.injectable";
import type { ReadDirectory } from "../../common/fs/read-directory.injectable";

View File

@ -0,0 +1,882 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`kubeconfig sync showing reactive catalog renders 1`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="home-button"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to some-product-name!
</h2>
<p>
To get you started we have auto-detected your clusters in your
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`kubeconfig sync showing reactive catalog when navigating to the catalog renders 1`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive focusable"
data-testid="home-button"
tabindex="0"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="mainLayout"
style="--sidebar-width: 200px;"
>
<div
class="sidebar"
>
<div
class="flex flex-col w-full"
>
<div
class="catalog"
>
Catalog
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
role="tree"
>
<li
aria-selected="true"
class="MuiTreeItem-root Mui-selected"
data-testid="*-tab"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
/>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
Browse
</div>
</div>
</li>
<li
aria-expanded="true"
class="MuiTreeItem-root bordered Mui-expanded"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="expand_more"
>
expand_more
</span>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="parent"
>
Categories
</div>
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root"
data-testid="entity.k8slens.dev/General-tab"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="settings"
>
settings
</span>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="flex"
>
<div>
General
</div>
</div>
</div>
</div>
</li>
<li
class="MuiTreeItem-root"
data-testid="entity.k8slens.dev/KubernetesCluster-tab"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon focusable small"
>
<span
class="icon"
data-icon-name=""
/>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="flex"
>
<div>
Clusters
</div>
</div>
</div>
</div>
</li>
<li
class="MuiTreeItem-root"
data-testid="entity.k8slens.dev/WebLink-tab"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="public"
>
public
</span>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="flex"
>
<div>
Web Links
</div>
</div>
</div>
</div>
</li>
</div>
</div>
</ul>
</li>
</ul>
</div>
<div
class="ResizingAnchor horizontal trailing"
/>
</div>
<div
class="contents"
>
<div
class="views"
>
<div
class="ItemListLayout flex column Catalog"
data-testid="catalog-list-for-browse-all"
>
<div
class="header flex gaps align-center"
>
<h5
class="title"
>
Browse All
</h5>
<div
class="info-panel box grow"
>
0 items
</div>
<div
class="Input SearchInput focused"
>
<label
class="input-area flex gaps align-center"
id=""
>
<input
class="input box grow"
placeholder="Search..."
spellcheck="false"
value=""
/>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="search"
>
search
</span>
</i>
</label>
<div
class="input-info flex gaps"
/>
</div>
</div>
<div
class="items box grow flex column"
>
<div
class="Table flex column Catalog box grow dark selectable scrollable sortable autoSize virtual"
>
<div
class="TableHead sticky nowrap topLine"
>
<div
class="TableCell entityName nowrap sorting"
id="name"
>
<div
class="content"
>
Name
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell nowrap sorting"
id="kind"
>
<div
class="content"
>
Kind
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell sourceCell nowrap sorting"
id="source"
>
<div
class="content"
>
Source
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell labelsCell scrollable nowrap"
id="labels"
>
<div
class="content"
>
Labels
</div>
</div>
<div
class="TableCell statusCell nowrap sorting"
id="status"
>
<div
class="content"
>
Status
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell menu nowrap"
>
<div
class="content"
>
<i
class="Icon material interactive focusable"
id="menu-actions-for-item-object-list-content"
tabindex="0"
>
<span
class="icon"
data-icon-name="more_vert"
>
more_vert
</span>
</i>
</div>
</div>
</div>
<div
class="NoItems flex box grow"
>
<div
class="box center"
>
Item list is empty
</div>
</div>
</div>
<div
class="AddRemoveButtons flex gaps"
/>
</div>
</div>
</div>
</div>
<div
class="footer"
/>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import type { RenderResult } from "@testing-library/react";
import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
import writeFileInjectable from "../../common/fs/write-file.injectable";
import { dumpConfigYaml } from "../../common/kube-helpers";
import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
import { flushPromises } from "../../common/test-utils/flush-promises";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
describe("kubeconfig sync showing reactive catalog", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let windowDi: DiContainer;
let mainDi: DiContainer;
beforeEach(async () => {
builder = getApplicationBuilder();
// builder.mainDi.override(loggerInjectable, () => console as any);
rendered = await builder.render();
windowDi = builder.applicationWindow.only.di;
mainDi = builder.mainDi;
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when navigating to the catalog", () => {
beforeEach(() => {
const navigateToCatalog = windowDi.inject(navigateToCatalogInjectable);
navigateToCatalog();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when a config file is written under ~/.kube", () => {
beforeEach(async () => {
const writeFile = mainDi.inject(writeFileInjectable);
const joinPaths = mainDi.inject(joinPathsInjectable);
const homeDirectoryPath = mainDi.inject(homeDirectoryPathInjectable);
const configContents = dumpConfigYaml({
clusters: [{
name: "some-cluster-name",
server: "https://1.2.3.4",
skipTLSVerify: false,
}],
users: [{
name: "some-user-name",
}],
contexts: [{
cluster: "some-cluster-name",
name: "some-context-name",
user: "some-user-name",
}],
});
await writeFile(joinPaths(homeDirectoryPath, ".kube", "config"), configContents);
await flushPromises();
});
it.only("eventually shows the cluster as a new entity", async () => {
await rendered.findByTestId("catalog-entity-row-for-some-cluster-name", undefined, {
timeout: 10_000,
});
}, 100_000);
});
});
});

View File

@ -25,8 +25,8 @@ import type { AsyncFnMock } from "@async-fn/jest";
import type { Stat } from "../../../common/fs/stat.injectable";
import asyncFn from "@async-fn/jest";
import statInjectable from "../../../common/fs/stat.injectable";
import type { Watcher } from "../../../common/fs/watch/watch.injectable";
import watchInjectable from "../../../common/fs/watch/watch.injectable";
import type { Watcher } from "../../../common/fs/watch.injectable";
import watchInjectable from "../../../common/fs/watch.injectable";
import EventEmitter from "events";
import type { ReadStream, Stats } from "fs";
import createReadFileStreamInjectable from "../../../common/fs/create-read-file-stream.injectable";

View File

@ -86,7 +86,7 @@ const computeKubeconfigDiffInjectable = getInjectable({
logger.debug(`Added new cluster from sync`, { filePath, contextName });
} catch (error) {
logger.warn(`Failed to create cluster from model: ${error}`, { filePath, contextName });
logger.warn(`Failed to create cluster with context="${contextName}" from path="${filePath}"`, error);
}
}
} catch (error) {

View File

@ -0,0 +1,66 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { Stats } from "fs";
import watchInjectable from "../../../common/fs/watch.injectable";
import loggerInjectable from "../../../common/logger.injectable";
export interface CreateKubeSyncWatcherOptions {
isDirectorySync: boolean;
onChange: (filePath: string, stats: Stats) => void;
onAdd: (filePath: string, stats: Stats) => void;
onRemove: (filePath: string) => void;
onError: (error: Error) => void;
}
export interface KubeSyncWatcher {
stop: () => void;
}
export type CreateKubeSyncWatcher = (filePath: string, opts: CreateKubeSyncWatcherOptions) => KubeSyncWatcher;
const createKubeSyncWatcherInjectable = getInjectable({
id: "create-kube-sync-watcher",
instantiate: (di): CreateKubeSyncWatcher => {
const watch = di.inject(watchInjectable);
const logger = di.inject(loggerInjectable);
return (filePath, { isDirectorySync, ...handlers }) => {
const watcher = watch<true>(filePath, {
followSymlinks: true,
depth: isDirectorySync ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
disableGlobbing: true,
ignorePermissionErrors: true,
usePolling: false,
awaitWriteFinish: {
pollInterval: 100,
stabilityThreshold: 1000,
},
atomic: 150, // for "atomic writes"
alwaysStat: true,
});
watcher
.on("change", handlers.onChange)
.on("add", handlers.onAdd)
.on("unlink", handlers.onRemove)
.on("error", handlers.onError);
return {
stop: () => {
void (async () => {
try {
await watcher.close();
} catch (error) {
logger.warn(`[KUBE-SYNC-WATCHER]: failed to stop watching "${filePath}": ${error}`);
}
})();
},
};
};
},
});
export default createKubeSyncWatcherInjectable;

View File

@ -11,8 +11,8 @@ import { inspect } from "util";
import type { CatalogEntity } from "../../../common/catalog";
import type { Cluster } from "../../../common/cluster/cluster";
import statInjectable from "../../../common/fs/stat.injectable";
import type { Watcher } from "../../../common/fs/watch/watch.injectable";
import watchInjectable from "../../../common/fs/watch/watch.injectable";
import type { KubeSyncWatcher } from "./create-watcher.injectable";
import createKubeSyncWatcherInjectable from "./create-watcher.injectable";
import type { Disposer } from "../../../common/utils";
import { getOrInsertWith, iter } from "../../../common/utils";
import diffChangedKubeconfigInjectable from "./diff-changed-kubeconfig.injectable";
@ -38,8 +38,8 @@ const ignoreGlobs = [
* Even if you have a cert-file, key-file, and client-cert files that is only
* 12kb of extra data (at 4096 bytes each) which allows for around 150 entries.
*/
const folderSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB
const fileSyncMaxAllowedFileReadSize = 16 * folderSyncMaxAllowedFileReadSize; // 32 MiB
const dirSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB
const fileSyncMaxAllowedFileReadSize = 16 * dirSyncMaxAllowedFileReadSize; // 32 MiB
const watchKubeconfigFileChangesInjectable = getInjectable({
id: "watch-kubeconfig-file-changes",
@ -47,39 +47,27 @@ const watchKubeconfigFileChangesInjectable = getInjectable({
const diffChangedKubeconfig = di.inject(diffChangedKubeconfigInjectable);
const logger = di.inject(kubeconfigSyncLoggerInjectable);
const stat = di.inject(statInjectable);
const watch = di.inject(watchInjectable);
const createKubeSyncWatcher = di.inject(createKubeSyncWatcherInjectable);
return (filePath) => {
const rootSource = observable.map<string, ObservableMap<string, [Cluster, CatalogEntity]>>();
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
let watcher: Watcher<true>;
let watcher: KubeSyncWatcher;
(async () => {
try {
const stats = await stat(filePath);
const isFolderSync = stats.isDirectory();
const isDirectorySync = stats.isDirectory();
const cleanupFns = new Map<string, Disposer>();
const maxAllowedFileReadSize = isFolderSync
? folderSyncMaxAllowedFileReadSize
const maxAllowedFileReadSize = isDirectorySync
? dirSyncMaxAllowedFileReadSize
: fileSyncMaxAllowedFileReadSize;
watcher = watch<true>(filePath, {
followSymlinks: true,
depth: isFolderSync ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
disableGlobbing: true,
ignorePermissionErrors: true,
usePolling: false,
awaitWriteFinish: {
pollInterval: 100,
stabilityThreshold: 1000,
},
atomic: 150, // for "atomic writes"
alwaysStat: true,
});
watcher
.on("change", (childFilePath, stats): void => {
watcher = createKubeSyncWatcher(filePath, {
isDirectorySync,
onChange: (childFilePath, stats): void => {
console.log("change", childFilePath);
const cleanup = cleanupFns.get(childFilePath);
if (!cleanup) {
@ -94,9 +82,11 @@ const watchKubeconfigFileChangesInjectable = getInjectable({
stats,
maxAllowedFileReadSize,
}));
})
.on("add", (childFilePath, stats): void => {
if (isFolderSync) {
},
onAdd: (childFilePath, stats): void => {
console.log("add", childFilePath);
if (isDirectorySync) {
const fileName = path.basename(childFilePath);
for (const ignoreGlob of ignoreGlobs) {
@ -112,20 +102,24 @@ const watchKubeconfigFileChangesInjectable = getInjectable({
stats,
maxAllowedFileReadSize,
}));
})
.on("unlink", (childFilePath) => {
},
onRemove: (childFilePath) => {
cleanupFns.get(childFilePath)?.();
cleanupFns.delete(childFilePath);
rootSource.delete(childFilePath);
})
.on("error", error => logger.error(`watching file/folder failed: ${error}`, { filePath }));
},
onError: (error) => {
console.log("error", error);
logger.error(`watching file/folder failed: ${error}`, { filePath });
},
});
} catch (error) {
logger.warn(`failed to start watching changes: ${error}`);
}
})();
return [derivedSource, () => {
watcher?.close();
watcher?.stop();
}];
};
},

View File

@ -307,6 +307,7 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
getItems={() => catalogEntityStore.entities.get()}
customizeTableRowProps={entity => ({
disabled: !entity.isEnabled(),
testId: `catalog-entity-row-for-${entity.getId()}`,
})}
{...getCategoryColumns({ activeCategory })}
onDetails={this.onDetails}

View File

@ -9,7 +9,7 @@ import { readFile } from "fs/promises";
import { hasCorrectExtension } from "./has-correct-extension";
import type { RawTemplates } from "./create-resource-templates.injectable";
import joinPathsInjectable from "../../../../common/path/join-paths.injectable";
import watchInjectable from "../../../../common/fs/watch/watch.injectable";
import watchInjectable from "../../../../common/fs/watch.injectable";
import getRelativePathInjectable from "../../../../common/path/get-relative-path.injectable";
import homeDirectoryPathInjectable from "../../../../common/os/home-directory-path.injectable";
import getDirnameOfPathInjectable from "../../../../common/path/get-dirname.injectable";

View File

@ -192,7 +192,7 @@ export const getApplicationBuilder = ({ useFakeTime = true }: ApplicationBuilder
const overrideFsWithFakes = getOverrideFsWithFakes();
overrideFsWithFakes(mainDi);
overrideFsWithFakes(mainDi, true);
// Set up ~/.kube as existing as a folder
{

View File

@ -10,6 +10,9 @@ import type {
readJsonSync as readJsonSyncImpl,
writeJsonSync as writeJsonSyncImpl,
} from "fs-extra";
import createKubeSyncWatcherInjectable from "../main/catalog-sources/kubeconfig-sync/create-watcher.injectable";
import { isErrnoException } from "../common/utils";
import joinPathsInjectable from "../common/path/join-paths.injectable";
export const getOverrideFsWithFakes = () => {
const root = createFsFromVolume(Volume.fromJSON({}));
@ -41,7 +44,7 @@ export const getOverrideFsWithFakes = () => {
root.mkdirpSync(path, mode);
}) as typeof ensureDirSyncImpl;
return (di: DiContainer) => {
return (di: DiContainer, overrideWatches = false) => {
di.override(fsInjectable, () => ({
pathExists: async (path) => root.existsSync(path),
pathExistsSync: root.existsSync,
@ -63,5 +66,76 @@ export const getOverrideFsWithFakes = () => {
createReadStream: root.createReadStream as any,
stat: root.promises.stat as any,
}));
if (overrideWatches) {
di.override(createKubeSyncWatcherInjectable, (di) => {
const joinPaths = di.inject(joinPathsInjectable);
return ((path, options) => {
const watcher = root.watch( path, {
recursive: options.isDirectorySync,
});
const seenPaths = new Set<string>();
console.log("watching", path);
watcher.addListener("rename", (eventType, filename: string) => {
try {
const stats = root.statSync(filename);
options.onAdd(filename, stats);
} catch (error) {
if (isErrnoException(error) && error.code === "ENOENT") {
options.onRemove(filename);
} else {
options.onError(error as Error);
}
}
});
watcher.addListener("change", (...args) => {
const [,filename] = args;
if (options.isDirectorySync) {
// For testing purposes just emit change events for all files
for (const entry of root.readdirSync(filename) as string[]) {
const path = joinPaths(filename, entry);
try {
const stats = root.statSync(path);
if (seenPaths.has(path)) {
options.onChange(path, stats);
} else {
seenPaths.add(path);
options.onAdd(path, stats);
}
} catch (error) {
options.onError(error as Error);
}
}
} else {
try {
const stats = root.statSync(filename);
if (seenPaths.has(filename)) {
options.onChange(filename, stats);
} else {
seenPaths.add(filename);
options.onAdd(filename, stats);
}
} catch (error) {
options.onError(error as Error);
}
}
});
return {
stop: () => {
watcher.close();
},
};
});
});
}
};
};