@@ -368,16 +365,16 @@ export class AddCluster extends React.Component {
Add cluster : Add clusters }
onClick={this.addClusters}
waiting={this.isWaiting}
- tooltip={addDisabled ? _i18n._("Select at least one cluster to add.") : undefined}
+ tooltip={submitDisabled ? _i18n._("Select at least one cluster to add.") : undefined}
tooltipOverrideDisabled
/>
-
-
+
+
);
}
}
diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx
new file mode 100644
index 0000000000..b5a036ad88
--- /dev/null
+++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx
@@ -0,0 +1,79 @@
+import '@testing-library/jest-dom/extend-expect';
+import { fireEvent, render, screen } from '@testing-library/react';
+import React from 'react';
+import { extensionDiscovery } from "../../../../extensions/extension-discovery";
+import { ConfirmDialog } from "../../confirm-dialog";
+import { Notifications } from "../../notifications";
+import { Extensions } from "../extensions";
+
+jest.mock("../../../../extensions/extension-discovery", () => ({
+ ...jest.requireActual("../../../../extensions/extension-discovery"),
+ extensionDiscovery: {
+ localFolderPath: "/fake/path",
+ uninstallExtension: jest.fn(() => Promise.resolve())
+ }
+}));
+
+jest.mock("../../../../extensions/extension-loader", () => ({
+ ...jest.requireActual("../../../../extensions/extension-loader"),
+ extensionLoader: {
+ userExtensions: new Map([
+ ["extensionId", {
+ id: "extensionId",
+ manifest: {
+ name: "test",
+ version: "1.2.3"
+ },
+ absolutePath: "/absolute/path",
+ manifestPath: "/symlinked/path/package.json",
+ isBundled: false,
+ isEnabled: true
+ }]
+ ])
+ }
+}));
+
+jest.mock("../../notifications", () => ({
+ ok: jest.fn(),
+ error: jest.fn(),
+ info: jest.fn()
+}));
+
+describe("Extensions", () => {
+ it("disables uninstall and disable buttons while uninstalling", async () => {
+ render(<>
>);
+
+ expect(screen.getByText("Disable").closest("button")).not.toBeDisabled();
+ expect(screen.getByText("Uninstall").closest("button")).not.toBeDisabled();
+
+ fireEvent.click(screen.getByText("Uninstall"));
+
+ // Approve confirm dialog
+ fireEvent.click(screen.getByText("Yes"));
+
+ expect(extensionDiscovery.uninstallExtension).toHaveBeenCalledWith("/absolute/path");
+ expect(screen.getByText("Disable").closest("button")).toBeDisabled();
+ expect(screen.getByText("Uninstall").closest("button")).toBeDisabled();
+ });
+
+ it("displays error notification on uninstall error", () => {
+ (extensionDiscovery.uninstallExtension as any).mockImplementationOnce(() =>
+ Promise.reject()
+ );
+ render(<>
>);
+
+ expect(screen.getByText("Disable").closest("button")).not.toBeDisabled();
+ expect(screen.getByText("Uninstall").closest("button")).not.toBeDisabled();
+
+ fireEvent.click(screen.getByText("Uninstall"));
+
+ // Approve confirm dialog
+ fireEvent.click(screen.getByText("Yes"));
+
+ setTimeout(() => {
+ expect(screen.getByText("Disable").closest("button")).not.toBeDisabled();
+ expect(screen.getByText("Uninstall").closest("button")).not.toBeDisabled();
+ expect(Notifications.error).toHaveBeenCalledTimes(1);
+ }, 100);
+ });
+});
diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx
index 03f34f96ee..327dccee99 100644
--- a/src/renderer/components/+extensions/extensions.tsx
+++ b/src/renderer/components/+extensions/extensions.tsx
@@ -1,8 +1,8 @@
import { t, Trans } from "@lingui/macro";
import { remote, shell } from "electron";
import fse from "fs-extra";
-import { computed, observable } from "mobx";
-import { observer } from "mobx-react";
+import { computed, observable, reaction } from "mobx";
+import { disposeOnUnmount, observer } from "mobx-react";
import os from "os";
import path from "path";
import React from "react";
@@ -15,6 +15,7 @@ import logger from "../../../main/logger";
import { _i18n } from "../../i18n";
import { prevDefault } from "../../utils";
import { Button } from "../button";
+import { ConfirmDialog } from "../confirm-dialog";
import { Icon } from "../icon";
import { DropFileInput, Input, InputValidator, InputValidators, SearchInput } from "../input";
import { PageLayout } from "../layout/page-layout";
@@ -38,6 +39,12 @@ interface InstallRequestValidated extends InstallRequestPreloaded {
tempFile: string; // temp system path to packed extension for unpacking
}
+interface ExtensionState {
+ displayName: string;
+ // Possible states the extension can be
+ state: "uninstalling";
+}
+
@observer
export class Extensions extends React.Component {
private supportedFormats = [".tar", ".tgz"];
@@ -49,17 +56,47 @@ export class Extensions extends React.Component {
}
};
+ @observable
+ extensionState = observable.map
();
+
@observable search = "";
@observable installPath = "";
+ /**
+ * Extensions that were removed from extensions but are still in "uninstalling" state
+ */
+ @computed get removedUninstalling() {
+ return Array.from(this.extensionState.entries()).filter(([id, extension]) =>
+ extension.state === "uninstalling" && !this.extensions.find(extension => extension.id === id)
+ ).map(([id, extension]) => ({ ...extension, id }));
+ }
+
+ componentDidMount() {
+ disposeOnUnmount(this,
+ reaction(() => this.extensions, (extensions) => {
+ const removedUninstalling = this.removedUninstalling;
+
+ removedUninstalling.forEach(({ displayName }) => {
+ Notifications.ok(
+ Extension {displayName} successfully uninstalled!
+ );
+ });
+
+ removedUninstalling.forEach(({ id }) => {
+ this.extensionState.delete(id);
+ });
+ })
+ );
+ }
+
@computed get extensions() {
const searchText = this.search.toLowerCase();
return Array.from(extensionLoader.userExtensions.values()).filter(ext => {
const { name, description } = ext.manifest;
return [
name.toLowerCase().includes(searchText),
- description.toLowerCase().includes(searchText),
- ].some(v => v);
+ description?.toLowerCase().includes(searchText),
+ ].some(value => value);
});
}
@@ -277,15 +314,33 @@ export class Extensions extends React.Component {
}
}
+ confirmUninstallExtension = (extension: InstalledExtension) => {
+ const displayName = extensionDisplayName(extension.manifest.name, extension.manifest.version);
+
+ ConfirmDialog.open({
+ message: Are you sure you want to uninstall extension {displayName} ?
,
+ labelOk: Yes ,
+ labelCancel: No ,
+ ok: () => this.uninstallExtension(extension)
+ });
+ };
+
async uninstallExtension(extension: InstalledExtension) {
- const extensionName = extensionDisplayName(extension.manifest.name, extension.manifest.version);
+ const displayName = extensionDisplayName(extension.manifest.name, extension.manifest.version);
try {
+ this.extensionState.set(extension.id, {
+ state: "uninstalling",
+ displayName
+ });
+
await extensionDiscovery.uninstallExtension(extension.absolutePath);
} catch (error) {
Notifications.error(
- Uninstalling extension {extensionName} has failed: {error?.message ?? ""}
+ Uninstalling extension {displayName} has failed: {error?.message ?? ""}
);
+ // Remove uninstall state on uninstall failure
+ this.extensionState.delete(extension.id);
}
}
@@ -304,12 +359,13 @@ export class Extensions extends React.Component {
);
}
- return extensions.map(ext => {
- const { manifestPath: extId, isEnabled, manifest } = ext;
+ return extensions.map(extension => {
+ const { id, isEnabled, manifest } = extension;
const { name, description } = manifest;
+ const isUninstalling = this.extensionState.get(id)?.state === "uninstalling";
return (
-
+
Name: {name}
@@ -320,13 +376,17 @@ export class Extensions extends React.Component {
{!isEnabled && (
- ext.isEnabled = true}>Enable
+ {
+ extension.isEnabled = true;
+ }}>Enable
)}
{isEnabled && (
- ext.isEnabled = false}>Disable
+ {
+ extension.isEnabled = false;
+ }}>Disable
)}
- {
- this.uninstallExtension(ext);
+ {
+ this.confirmUninstallExtension(extension);
}}>Uninstall
diff --git a/src/renderer/components/button/button.scss b/src/renderer/components/button/button.scss
index f586a03c5a..b850a3be59 100644
--- a/src/renderer/components/button/button.scss
+++ b/src/renderer/components/button/button.scss
@@ -12,6 +12,7 @@
flex-shrink: 0;
line-height: 1;
font-size: $font-size;
+ user-select: none;
&[href] {
display: inline-block;
diff --git a/src/renderer/components/dock/pod-log-controls.scss b/src/renderer/components/dock/pod-log-controls.scss
new file mode 100644
index 0000000000..795d06c67e
--- /dev/null
+++ b/src/renderer/components/dock/pod-log-controls.scss
@@ -0,0 +1,5 @@
+.PodLogControls {
+ .Select {
+ min-width: 150px;
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/components/dock/pod-log-controls.tsx b/src/renderer/components/dock/pod-log-controls.tsx
index 17ad8a2ddf..bb4132dc34 100644
--- a/src/renderer/components/dock/pod-log-controls.tsx
+++ b/src/renderer/components/dock/pod-log-controls.tsx
@@ -1,3 +1,4 @@
+import "./pod-log-controls.scss";
import React from "react";
import { observer } from "mobx-react";
import { IPodLogsData, podLogsStore } from "./pod-logs.store";
@@ -21,10 +22,9 @@ interface Props extends PodLogSearchProps {
}
export const PodLogControls = observer((props: Props) => {
- if (!props.ready) return null;
const { tabData, save, reload, tabId, logs } = props;
const { selectedContainer, showTimestamps, previous } = tabData;
- const rawLogs = podLogsStore.logs.get(tabId);
+ const rawLogs = podLogsStore.logs.get(tabId) || [];
const since = rawLogs.length ? podLogsStore.getTimestamps(rawLogs[0]) : null;
const pod = new Pod(tabData.pod);
diff --git a/src/renderer/components/dock/pod-log-list.scss b/src/renderer/components/dock/pod-log-list.scss
new file mode 100644
index 0000000000..9c14f79fa4
--- /dev/null
+++ b/src/renderer/components/dock/pod-log-list.scss
@@ -0,0 +1,78 @@
+.PodLogList {
+ --overlay-bg: #8cc474b8;
+ --overlay-active-bg: orange;
+
+ // fix for `this.logsElement.scrollTop = this.logsElement.scrollHeight`
+ // `overflow: overlay` don't allow scroll to the last line
+ overflow: auto;
+
+ position: relative;
+ color: $textColorAccent;
+ background: $logsBackground;
+ flex-grow: 1;
+
+ .VirtualList {
+ height: 100%;
+
+ .list {
+ overflow-x: scroll!important;
+
+ .LogRow {
+ padding: 2px 16px;
+ height: 18px; // Must be equal to lineHeight variable in pod-log-list.tsx
+ font-family: $font-monospace;
+ font-size: smaller;
+ white-space: pre;
+
+ &:hover {
+ background: $logRowHoverBackground;
+ }
+
+ span {
+ -webkit-font-smoothing: auto; // Better readability on non-retina screens
+ }
+
+ span.overlay {
+ border-radius: 2px;
+ -webkit-font-smoothing: auto;
+ background-color: var(--overlay-bg);
+
+ span {
+ background-color: var(--overlay-bg)!important; // Rewriting inline styles from AnsiUp library
+ }
+
+ &.active {
+ background-color: var(--overlay-active-bg);
+
+ span {
+ background-color: var(--overlay-active-bg)!important; // Rewriting inline styles from AnsiUp library
+ }
+ }
+ }
+ }
+ }
+ }
+
+ &.isLoading {
+ cursor: wait;
+ }
+
+ &.isScrollHidden {
+ .VirtualList .list {
+ overflow-x: hidden!important; // fixing scroll to bottom issues in PodLogs
+ }
+ }
+
+ .JumpToBottom {
+ position: absolute;
+ right: 30px;
+ padding: $unit / 2 $unit * 1.5;
+ border-radius: $unit * 2;
+ z-index: 2;
+ top: 20px;
+
+ .Icon {
+ --size: $unit * 2;
+ }
+ }
+}
diff --git a/src/renderer/components/dock/pod-log-list.tsx b/src/renderer/components/dock/pod-log-list.tsx
new file mode 100644
index 0000000000..e882b38a5b
--- /dev/null
+++ b/src/renderer/components/dock/pod-log-list.tsx
@@ -0,0 +1,224 @@
+import "./pod-log-list.scss";
+
+import React from "react";
+import AnsiUp from "ansi_up";
+import DOMPurify from "dompurify";
+import debounce from "lodash/debounce";
+import { Trans } from "@lingui/macro";
+import { action, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Align, ListOnScrollProps } from "react-window";
+
+import { searchStore } from "../../../common/search-store";
+import { cssNames } from "../../utils";
+import { Button } from "../button";
+import { Icon } from "../icon";
+import { Spinner } from "../spinner";
+import { VirtualList } from "../virtual-list";
+import { logRange } from "./pod-logs.store";
+
+interface Props {
+ logs: string[]
+ isLoading: boolean
+ load: () => void
+ id: string
+}
+
+const colorConverter = new AnsiUp();
+
+@observer
+export class PodLogList extends React.Component
{
+ @observable isJumpButtonVisible = false;
+ @observable isLastLineVisible = true;
+
+ private virtualListDiv = React.createRef(); // A reference for outer container in VirtualList
+ private virtualListRef = React.createRef(); // A reference for VirtualList component
+ private lineHeight = 18; // Height of a log line. Should correlate with styles in pod-log-list.scss
+
+ componentDidMount() {
+ this.scrollToBottom();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ const { logs, id } = this.props;
+ if (id != prevProps.id) {
+ this.isLastLineVisible = true;
+ return;
+ }
+ if (logs == prevProps.logs || !this.virtualListDiv.current) return;
+ const newLogsLoaded = prevProps.logs.length < logs.length;
+ const scrolledToBeginning = this.virtualListDiv.current.scrollTop === 0;
+ const fewLogsLoaded = logs.length < logRange;
+ if (this.isLastLineVisible) {
+ this.scrollToBottom(); // Scroll down to keep user watching/reading experience
+ return;
+ }
+ if (scrolledToBeginning && newLogsLoaded) {
+ this.virtualListDiv.current.scrollTop = (logs.length - prevProps.logs.length) * this.lineHeight;
+ }
+ if (fewLogsLoaded) {
+ this.isJumpButtonVisible = false;
+ }
+ if (!logs.length) {
+ this.isLastLineVisible = false;
+ }
+ }
+
+ /**
+ * Checks if JumpToBottom button should be visible and sets its observable
+ * @param props Scrolling props from virtual list core
+ */
+ @action
+ setButtonVisibility = (props: ListOnScrollProps) => {
+ const offset = 100 * this.lineHeight;
+ const { scrollHeight } = this.virtualListDiv.current;
+ const { scrollOffset } = props;
+ if (scrollHeight - scrollOffset < offset) {
+ this.isJumpButtonVisible = false;
+ } else {
+ this.isJumpButtonVisible = true;
+ }
+ };
+
+ /**
+ * Checks if last log line considered visible to user, setting its observable
+ * @param props Scrolling props from virtual list core
+ */
+ @action
+ setLastLineVisibility = (props: ListOnScrollProps) => {
+ const { scrollHeight, clientHeight } = this.virtualListDiv.current;
+ const { scrollOffset, scrollDirection } = props;
+ if (scrollDirection == "backward") {
+ this.isLastLineVisible = false;
+ } else {
+ if (clientHeight + scrollOffset === scrollHeight) {
+ this.isLastLineVisible = true;
+ }
+ }
+ };
+
+ /**
+ * Check if user scrolled to top and new logs should be loaded
+ * @param props Scrolling props from virtual list core
+ */
+ checkLoadIntent = (props: ListOnScrollProps) => {
+ const { scrollOffset } = props;
+ if (scrollOffset === 0) {
+ this.props.load();
+ }
+ };
+
+ @action
+ scrollToBottom = () => {
+ if (!this.virtualListDiv.current) return;
+ this.isJumpButtonVisible = false;
+ this.virtualListDiv.current.scrollTop = this.virtualListDiv.current.scrollHeight;
+ };
+
+ scrollToItem = (index: number, align: Align) => {
+ this.virtualListRef.current.scrollToItem(index, align);
+ };
+
+ onScroll = debounce((props: ListOnScrollProps) => {
+ if (!this.virtualListDiv.current) return;
+ this.setButtonVisibility(props);
+ this.setLastLineVisibility(props);
+ this.checkLoadIntent(props);
+ }, 700); // Increasing performance and giving some time for virtual list to settle down
+
+ /**
+ * A function is called by VirtualList for rendering each of the row
+ * @param rowIndex index of the log element in logs array
+ * @returns A react element with a row itself
+ */
+ getLogRow = (rowIndex: number) => {
+ const { searchQuery, isActiveOverlay } = searchStore;
+ const item = this.props.logs[rowIndex];
+ const contents: React.ReactElement[] = [];
+ const ansiToHtml = (ansi: string) => DOMPurify.sanitize(colorConverter.ansi_to_html(ansi));
+ if (searchQuery) { // If search is enabled, replace keyword with backgrounded
+ // Case-insensitive search (lowercasing query and keywords in line)
+ const regex = new RegExp(searchStore.escapeRegex(searchQuery), "gi");
+ const matches = item.matchAll(regex);
+ const modified = item.replace(regex, match => match.toLowerCase());
+ // Splitting text line by keyword
+ const pieces = modified.split(searchQuery.toLowerCase());
+ pieces.forEach((piece, index) => {
+ const active = isActiveOverlay(rowIndex, index);
+ const lastItem = index === pieces.length - 1;
+ const overlayValue = matches.next().value;
+ const overlay = !lastItem
+ ?
+ : null;
+ contents.push(
+
+
+ {overlay}
+
+ );
+ });
+ }
+ return (
+
+ {contents.length > 1 ? contents : (
+
+ )}
+
+ );
+ };
+
+ render() {
+ const { logs, isLoading } = this.props;
+ const isInitLoading = isLoading && !logs.length;
+ const rowHeights = new Array(logs.length).fill(this.lineHeight);
+ if (isInitLoading) {
+ return ;
+ }
+ if (!logs.length) {
+ return (
+
+ There are no logs available for container
+
+ );
+ }
+ return (
+
+
+ {this.isJumpButtonVisible && (
+
+ )}
+
+ );
+ }
+}
+
+interface JumpToBottomProps {
+ onClick: () => void
+}
+
+const JumpToBottom = ({ onClick }: JumpToBottomProps) => {
+ return (
+ {
+ evt.currentTarget.blur();
+ onClick();
+ }}
+ >
+ Jump to bottom
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/renderer/components/dock/pod-logs.scss b/src/renderer/components/dock/pod-logs.scss
deleted file mode 100644
index 47909b4fb9..0000000000
--- a/src/renderer/components/dock/pod-logs.scss
+++ /dev/null
@@ -1,86 +0,0 @@
-.PodLogs {
- --overlay-bg: #8cc474b8;
- --overlay-active-bg: orange;
-
- .logs {
- overflow: auto;
- position: relative;
- color: $textColorAccent;
- background: $logsBackground;
- flex-grow: 1;
-
- .VirtualList {
- height: 100%;
-
- .list {
- .LogRow {
- padding: 2px 16px;
- height: 18px; // Must be equal to lineHeight variable in pod-logs.scss
- font-family: $font-monospace;
- font-size: smaller;
- white-space: pre;
-
- &:hover {
- background: $logRowHoverBackground;
- }
-
- span {
- -webkit-font-smoothing: auto; // Better readability on non-retina screens
- }
-
- span.overlay {
- border-radius: 2px;
- -webkit-font-smoothing: auto;
- background-color: var(--overlay-bg);
-
- span {
- background-color: var(--overlay-bg)!important; // Rewriting inline styles from AnsiUp library
- }
-
- &.active {
- background-color: var(--overlay-active-bg);
-
- span {
- background-color: var(--overlay-active-bg)!important; // Rewriting inline styles from AnsiUp library
- }
- }
- }
- }
- }
- }
- }
-
- .jump-to-bottom {
- position: absolute;
- right: 30px;
- padding: $unit / 2 $unit * 1.5;
- border-radius: $unit * 2;
- opacity: 0;
- z-index: 2;
- top: 20px;
-
- &.active {
- opacity: 1;
- }
-
- .Icon {
- --size: $unit * 2;
- }
- }
-
- .PodLogControls {
- .Select {
- min-width: 150px;
- }
- }
-
- .logs .VirtualList .list {
- overflow-x: scroll!important;
- }
-
- &.noscroll {
- .logs .VirtualList .list {
- overflow-x: hidden!important; // fixing scroll to bottom issues in PodLogs
- }
- }
-}
\ No newline at end of file
diff --git a/src/renderer/components/dock/pod-logs.tsx b/src/renderer/components/dock/pod-logs.tsx
index be5a0d5167..71579be112 100644
--- a/src/renderer/components/dock/pod-logs.tsx
+++ b/src/renderer/components/dock/pod-logs.tsx
@@ -1,65 +1,30 @@
-import "./pod-logs.scss";
import React from "react";
-import AnsiUp from 'ansi_up';
-import DOMPurify from "dompurify";
-import { Trans } from "@lingui/macro";
-import { action, computed, observable, reaction } from "mobx";
+import { computed, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
-import { _i18n } from "../../i18n";
-import { autobind, cssNames } from "../../utils";
-import { Icon } from "../icon";
-import { Spinner } from "../spinner";
+
+import { searchStore } from "../../../common/search-store";
+import { autobind } from "../../utils";
import { IDockTab } from "./dock.store";
import { InfoPanel } from "./info-panel";
-import { IPodLogsData, logRange, podLogsStore } from "./pod-logs.store";
-import { Button } from "../button";
import { PodLogControls } from "./pod-log-controls";
-import { VirtualList } from "../virtual-list";
-import { searchStore } from "../../../common/search-store";
-import { ListOnScrollProps } from "react-window";
+import { PodLogList } from "./pod-log-list";
+import { IPodLogsData, podLogsStore } from "./pod-logs.store";
interface Props {
className?: string
tab: IDockTab
}
-const lineHeight = 18; // Height of a log line. Should correlate with styles in pod-logs.scss
-
@observer
export class PodLogs extends React.Component {
- @observable ready = false;
- @observable preloading = false; // Indicator for setting Spinner (loader) at the top of the logs
- @observable showJumpToBottom = false;
- @observable hideHorizontalScroll = true; // Hiding scrollbar allows to scroll logs down to last element
+ @observable isLoading = true;
- private logsElement = React.createRef(); // A reference for outer container in VirtualList
- private virtualListRef = React.createRef(); // A reference for VirtualList component
- private lastLineIsShown = true; // used for proper auto-scroll content after refresh
- private colorConverter = new AnsiUp();
+ private logListElement = React.createRef(); // A reference for VirtualList component
componentDidMount() {
- disposeOnUnmount(this, [
- reaction(() => this.props.tab.id, async () => {
- await this.load();
- this.scrollToBottom();
- }, { fireImmediately: true }),
-
- // Check if need to show JumpToBottom if new log amount is less than previous one
- reaction(() => podLogsStore.logs.get(this.tabId), () => {
- const { tabId } = this;
- if (podLogsStore.logs.has(tabId) && podLogsStore.logs.get(tabId).length < logRange) {
- this.showJumpToBottom = false;
- }
- })
- ]);
- }
-
- componentDidUpdate() {
- // scroll logs only when it's already in the end,
- // otherwise it can interrupt reading by jumping after loading new logs update
- if (this.logsElement.current && this.lastLineIsShown) {
- this.logsElement.current.scrollTop = this.logsElement.current.scrollHeight;
- }
+ disposeOnUnmount(this,
+ reaction(() => this.props.tab.id, this.reload, { fireImmediately: true })
+ );
}
get tabData() {
@@ -76,33 +41,16 @@ export class PodLogs extends React.Component {
}
load = async () => {
- this.ready = false;
+ this.isLoading = true;
await podLogsStore.load(this.tabId);
- this.ready = true;
+ this.isLoading = false;
};
reload = async () => {
podLogsStore.clearLogs(this.tabId);
- this.lastLineIsShown = true;
await this.load();
};
- /**
- * Function loads more logs (usually after user scrolls to top) and sets proper
- * scrolling position
- */
- loadMore = async () => {
- const lines = podLogsStore.lines;
- if (lines < logRange) return;
- this.preloading = true;
- await podLogsStore.load(this.tabId);
- this.preloading = false;
- if (podLogsStore.lines > lines) {
- // Set scroll position back to place where preloading started
- this.logsElement.current.scrollTop = (podLogsStore.lines - lines) * lineHeight;
- }
- };
-
/**
* A function for various actions after search is happened
* @param query {string} A text from search field
@@ -118,9 +66,9 @@ export class PodLogs extends React.Component {
@autobind()
toOverlay() {
const { activeOverlayLine } = searchStore;
- if (!this.virtualListRef.current || activeOverlayLine === undefined) return;
+ if (!this.logListElement.current || activeOverlayLine === undefined) return;
// Scroll vertically
- this.virtualListRef.current.scrollToItem(activeOverlayLine, "center");
+ this.logListElement.current.scrollToItem(activeOverlayLine, "center");
// Scroll horizontally in timeout since virtual list need some time to prepare its contents
setTimeout(() => {
const overlay = document.querySelector(".PodLogs .list span.active");
@@ -145,139 +93,10 @@ export class PodLogs extends React.Component {
return logs;
}
- onScroll = (props: ListOnScrollProps) => {
- if (!this.logsElement.current) return;
- const toBottomOffset = 100 * lineHeight; // 100 lines * 18px (height of each line)
- const { scrollHeight, clientHeight } = this.logsElement.current;
- const { scrollDirection, scrollOffset, scrollUpdateWasRequested } = props;
- if (scrollDirection == "forward") {
- if (scrollHeight - scrollOffset < toBottomOffset) {
- this.showJumpToBottom = false;
- }
- if (clientHeight + scrollOffset === scrollHeight) {
- this.lastLineIsShown = true;
- }
- } else {
- this.lastLineIsShown = false;
- // Trigger loading only if scrolled by user
- if (scrollOffset === 0 && !scrollUpdateWasRequested) {
- this.loadMore();
- }
- if (scrollHeight - scrollOffset > toBottomOffset) {
- this.showJumpToBottom = true;
- }
- }
- };
-
- @action
- scrollToBottom = () => {
- if (!this.virtualListRef.current) return;
- this.hideHorizontalScroll = true;
- this.virtualListRef.current.scrollToItem(this.logs.length, "end");
- this.showJumpToBottom = false;
- // Showing horizontal scrollbar after VirtualList settles down
- setTimeout(() => this.hideHorizontalScroll = false, 500);
- };
-
- /**
- * A function is called by VirtualList for rendering each of the row
- * @param rowIndex {Number} index of the log element in logs array
- * @returns A react element with a row itself
- */
- getLogRow = (item: string, rowIndex: number) => {
- const { searchQuery, isActiveOverlay } = searchStore;
- const contents: React.ReactElement[] = [];
- const ansiToHtml = (ansi: string) => DOMPurify.sanitize(this.colorConverter.ansi_to_html(ansi));
- if (searchQuery) { // If search is enabled, replace keyword with backgrounded
- // Case-insensitive search (lowercasing query and keywords in line)
- const regex = new RegExp(searchStore.escapeRegex(searchQuery), "gi");
- const matches = item.matchAll(regex);
- const modified = item.replace(regex, match => match.toLowerCase());
- // Splitting text line by keyword
- const pieces = modified.split(searchQuery.toLowerCase());
- pieces.forEach((piece, index) => {
- const active = isActiveOverlay(rowIndex, index);
- const lastItem = index === pieces.length - 1;
- const overlayValue = matches.next().value;
- const overlay = !lastItem ?
- :
- null;
- contents.push(
-
-
- {overlay}
-
- );
- });
- }
- return (
-
- {contents.length > 1 ? contents : (
-
- )}
-
- );
- };
-
- renderJumpToBottom() {
- if (!this.logsElement) return null;
- return (
- {
- evt.currentTarget.blur();
- this.scrollToBottom();
- }}
- >
- Jump to bottom
-
-
- );
- }
-
- renderLogs() {
- // Generating equal heights for each row with ability to do multyrow logs in future
- // e. g. for wrapping logs feature
- const rowHeights = new Array(this.logs.length).fill(lineHeight);
- if (!this.ready) {
- return ;
- }
- if (!this.logs.length) {
- return (
-
- There are no logs available for container.
-
- );
- }
- return (
- <>
- {this.preloading && (
-
-
-
- )}
-
- >
- );
- }
-
render() {
- const { className } = this.props;
const controls = (
{
/>
);
return (
-
+
-
- {this.renderJumpToBottom()}
- {this.renderLogs()}
-
+
);
}
diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md
index 3bbff0b8a5..fe22568bda 100644
--- a/static/RELEASE_NOTES.md
+++ b/static/RELEASE_NOTES.md
@@ -2,7 +2,7 @@
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
-## 4.0.0-beta.4 (current version)
+## 4.0.0-rc.1 (current version)
- Extension API
- Improved pod logs
@@ -12,14 +12,21 @@ Here you can find description of changes we've built into each release. While we
- Add LoadBalancer information to Ingress view
- Add search by ip to Pod view
- Move tracker to an extension
-- Add support page (as an extension)
- Ability to restart deployment
+- Add stateful set scale slider
- Status bar visual fixes
-- Fix proxy upgrade socket timeouts
-- Fix UI staleness after network issues
- Add +/- buttons in scale deployment popup screen
- Update chart details when selecting another chart
- Use latest alpine version (3.12) for shell sessions
+- Open last active cluster after switching workspaces
+- Replace deprecated stable helm repository with bitnami
+- Catch errors return error response when fetching chart or chart values fails
+- Update EULA url
+- Change add-cluster to single column layout
+- Replace cluster warning event polling with watches
+- Fix pod usage metrics on Kubernetes >=1.19
+- Fix proxy upgrade socket timeouts
+- Fix UI staleness after network issues
- Fix errors on app quit
- Fix kube-auth-proxy to accept only target cluster hostname
diff --git a/yarn.lock b/yarn.lock
index 6e39a26a83..caaf0bd5d2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9879,10 +9879,10 @@ mobx@^5.15.4:
resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.4.tgz#9da1a84e97ba624622f4e55a0bf3300fb931c2ab"
integrity sha512-xRFJxSU2Im3nrGCdjSuOTFmxVDGeqOHL+TyADCGbT0k4HHqGmx5u2yaHNryvoORpI4DfbzjJ5jPmuv+d7sioFw==
-mobx@^5.15.5:
- version "5.15.5"
- resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.5.tgz#69715dc8662f64d153309bfe95169b8df4b4be4b"
- integrity sha512-hzk17T+/IIYLPWClRcfoA6Q5aZhFpDCr1oh8RZzu+esWP77IX/lS0V/Ee1Np+aOPKFfbSInF0reHH0L/aFfSrw==
+mobx@^5.15.7:
+ version "5.15.7"
+ resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.7.tgz#b9a5f2b6251f5d96980d13c78e9b5d8d4ce22665"
+ integrity sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==
mock-fs@^4.12.0:
version "4.12.0"