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

Adding logs tab bottom toolbar (#1951)

* Adding bottom toolbar to logs tab

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Making bottom toolbar responsive

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Using generic search input clear button

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fixing log test selectors

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev 2021-01-15 11:34:11 +03:00 committed by GitHub
parent c48816ca5c
commit 83ed44f670
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 223 additions and 190 deletions

View File

@ -505,16 +505,16 @@ describe("Lens integration tests", () => {
await app.client.waitForVisible(".Drawer");
await app.client.click(".drawer-title .Menu li:nth-child(2)");
// Check if controls are available
await app.client.waitForVisible(".PodLogs .VirtualList");
await app.client.waitForVisible(".PodLogControls");
await app.client.waitForVisible(".PodLogControls .SearchInput");
await app.client.waitForVisible(".PodLogControls .SearchInput input");
await app.client.waitForVisible(".Logs .VirtualList");
await app.client.waitForVisible(".LogResourceSelector");
await app.client.waitForVisible(".LogResourceSelector .SearchInput");
await app.client.waitForVisible(".LogResourceSelector .SearchInput input");
// Search for semicolon
await app.client.keys(":");
await app.client.waitForVisible(".PodLogs .list span.active");
await app.client.waitForVisible(".Logs .list span.active");
// Click through controls
await app.client.click(".PodLogControls .timestamps-icon");
await app.client.click(".PodLogControls .undo-icon");
await app.client.click(".LogControls .show-timestamps");
await app.client.click(".LogControls .show-previous");
});
});

View File

@ -38,4 +38,4 @@ export * from "../../renderer/components/+events/kube-event-details";
// specific exports
export * from "../../renderer/components/status-brick";
export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store";
export { createPodLogsTab } from "../../renderer/components/dock/pod-logs.store";
export { createPodLogsTab } from "../../renderer/components/dock/log.store";

View File

@ -30,7 +30,7 @@ export class Checkbox extends React.PureComponent<CheckboxProps> {
render() {
const { label, inline, className, value, theme, children, ...inputProps } = this.props;
const componentClass = cssNames("Checkbox flex", className, {
const componentClass = cssNames("Checkbox flex align-center", className, {
inline,
checked: value,
disabled: this.props.disabled,

View File

@ -7,7 +7,7 @@ import { DockTab } from "./dock-tab";
import { IDockTab } from "./dock.store";
import { isEditResourceTab } from "./edit-resource.store";
import { isInstallChartTab } from "./install-chart.store";
import { isPodLogsTab } from "./pod-logs.store";
import { isLogsTab } from "./log.store";
import { TerminalTab } from "./terminal-tab";
import { isTerminalTab } from "./terminal.store";
import { isUpgradeChartTab } from "./upgrade-chart.store";
@ -33,7 +33,7 @@ export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: Props) =
return <DockTab value={tab} icon={<Icon svg="install" />} />;
}
if (isPodLogsTab(tab)) {
if (isLogsTab(tab)) {
return <DockTab value={tab} icon="subject" />;
}
};

View File

@ -16,8 +16,8 @@ import { EditResource } from "./edit-resource";
import { isEditResourceTab } from "./edit-resource.store";
import { InstallChart } from "./install-chart";
import { isInstallChartTab } from "./install-chart.store";
import { PodLogs } from "./pod-logs";
import { isPodLogsTab } from "./pod-logs.store";
import { Logs } from "./logs";
import { isLogsTab } from "./log.store";
import { TerminalWindow } from "./terminal-window";
import { createTerminalTab, isTerminalTab } from "./terminal.store";
import { UpgradeChart } from "./upgrade-chart";
@ -64,7 +64,7 @@ export class Dock extends React.Component<Props> {
{isInstallChartTab(tab) && <InstallChart tab={tab} />}
{isUpgradeChartTab(tab) && <UpgradeChart tab={tab} />}
{isTerminalTab(tab) && <TerminalWindow tab={tab} />}
{isPodLogsTab(tab) && <PodLogs tab={tab} />}
{isLogsTab(tab) && <Logs tab={tab} />}
</div>
);
}

View File

@ -2,7 +2,6 @@
@include hidden-scrollbar;
background: $dockInfoBackground;
border-bottom: 1px solid $dockInfoBorderColor;
padding: $padding $padding * 2;
flex-shrink: 0;

View File

@ -0,0 +1,6 @@
.LogControls {
@include hidden-scrollbar;
background: $dockInfoBackground;
padding: $padding $padding * 2;
}

View File

@ -0,0 +1,68 @@
import "./log-controls.scss";
import React from "react";
import { observer } from "mobx-react";
import { Pod } from "../../api/endpoints";
import { cssNames, saveFileDialog } from "../../utils";
import { IPodLogsData, podLogsStore } from "./log.store";
import { Checkbox } from "../checkbox";
import { Icon } from "../icon";
interface Props {
tabData: IPodLogsData
logs: string[]
save: (data: Partial<IPodLogsData>) => void
reload: () => void
}
export const LogControls = observer((props: Props) => {
const { tabData, save, reload, logs } = props;
const { showTimestamps, previous } = tabData;
const since = logs.length ? podLogsStore.getTimestamps(logs[0]) : null;
const pod = new Pod(tabData.pod);
const toggleTimestamps = () => {
save({ showTimestamps: !showTimestamps });
};
const togglePrevious = () => {
save({ previous: !previous });
reload();
};
const downloadLogs = () => {
const fileName = pod.getName();
const logsToDownload = showTimestamps ? logs : podLogsStore.logsWithoutTimestamps;
saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain");
};
return (
<div className={cssNames("LogControls flex gaps align-center justify-space-between wrap")}>
<div className="time-range">
{since && `Logs from ${new Date(since[0]).toLocaleString()}`}
</div>
<div className="flex gaps align-center">
<Checkbox
label="Show timestamps"
value={showTimestamps}
onChange={toggleTimestamps}
className="show-timestamps"
/>
<Checkbox
label="Show previous terminated container"
value={previous}
onChange={togglePrevious}
className="show-previous"
/>
<Icon
material="get_app"
onClick={downloadLogs}
tooltip="Download"
className="download-icon"
/>
</div>
</div>
);
});

View File

@ -1,4 +1,4 @@
.PodLogList {
.LogList {
--overlay-bg: #8cc474b8;
--overlay-active-bg: orange;

View File

@ -1,4 +1,4 @@
import "./pod-log-list.scss";
import "./log-list.scss";
import React from "react";
import AnsiUp from "ansi_up";
@ -14,7 +14,7 @@ import { Button } from "../button";
import { Icon } from "../icon";
import { Spinner } from "../spinner";
import { VirtualList } from "../virtual-list";
import { podLogsStore } from "./pod-logs.store";
import { podLogsStore } from "./log.store";
interface Props {
logs: string[]
@ -26,7 +26,7 @@ interface Props {
const colorConverter = new AnsiUp();
@observer
export class PodLogList extends React.Component<Props> {
export class LogList extends React.Component<Props> {
@observable isJumpButtonVisible = false;
@observable isLastLineVisible = true;
@ -206,19 +206,23 @@ export class PodLogList extends React.Component<Props> {
const rowHeights = new Array(this.logs.length).fill(this.lineHeight);
if (isInitLoading) {
return <Spinner center/>;
return (
<div className="LogList flex box grow align-center justify-center">
<Spinner center/>
</div>
);
}
if (!this.logs.length) {
return (
<div className="PodLogList flex box grow align-center justify-center">
<div className="LogList flex box grow align-center justify-center">
There are no logs available for container
</div>
);
}
return (
<div className={cssNames("PodLogList flex", { isLoading })}>
<div className={cssNames("LogList flex", { isLoading })}>
<VirtualList
items={this.logs}
rowHeights={rowHeights}

View File

@ -1,4 +1,4 @@
.PodLogControls {
.LogResourceSelector {
.Select {
min-width: 150px;
}

View File

@ -0,0 +1,66 @@
import "./log-resource-selector.scss";
import React from "react";
import { observer } from "mobx-react";
import { IPodContainer, Pod } from "../../api/endpoints";
import { Badge } from "../badge";
import { Select, SelectOption } from "../select";
import { IPodLogsData } from "./log.store";
interface Props {
tabData: IPodLogsData
save: (data: Partial<IPodLogsData>) => void
reload: () => void
}
export const LogResourceSelector = observer((props: Props) => {
const { tabData, save, reload } = props;
const { selectedContainer, containers, initContainers } = tabData;
const pod = new Pod(tabData.pod);
const onContainerChange = (option: SelectOption) => {
const { containers, initContainers } = tabData;
save({
selectedContainer: containers
.concat(initContainers)
.find(container => container.name === option.value)
});
reload();
};
const getSelectOptions = (containers: IPodContainer[]) => {
return containers.map(container => {
return {
value: container.name,
label: container.name
};
});
};
const containerSelectOptions = [
{
label: `Containers`,
options: getSelectOptions(containers)
},
{
label: `Init Containers`,
options: getSelectOptions(initContainers),
}
];
return (
<div className="LogResourceSelector flex gaps align-center">
<span>Namespace</span> <Badge label={pod.getNs()}/>
<span>Pod</span> <Badge label={pod.getName()}/>
<span>Container</span>
<Select
options={containerSelectOptions}
value={{ label: selectedContainer.name, value: selectedContainer.name }}
onChange={onContainerChange}
autoConvertOptions={false}
/>
</div>
);
});

View File

@ -1,10 +1,13 @@
.PodLogsSearch {
.LogSearch {
.SearchInput {
min-width: 150px;
width: 150px;
.find-count {
margin-left: 2px;
}
label {
padding-bottom: 7px;
}
}
}

View File

@ -1,4 +1,4 @@
import "./pod-log-search.scss";
import "./log-search.scss";
import React, { useEffect } from "react";
import { observer } from "mobx-react";
@ -16,7 +16,7 @@ interface Props extends PodLogSearchProps {
logs: string[]
}
export const PodLogSearch = observer((props: Props) => {
export const LogSearch = observer((props: Props) => {
const { logs, onSearch, toPrevOverlay, toNextOverlay } = props;
const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = searchStore;
const jumpDisabled = !searchQuery || !occurrences.length;
@ -57,11 +57,11 @@ export const PodLogSearch = observer((props: Props) => {
}, [logs]);
return (
<div className="PodLogsSearch flex box grow justify-flex-end gaps align-center">
<div className="LogSearch flex box grow justify-flex-end gaps align-center">
<SearchInput
value={searchQuery}
onChange={setSearch}
showClearIcon={false}
showClearIcon={true}
contentRight={totalFinds > 0 && findCounts}
onClear={onClear}
onKeyDown={onKeyDown}
@ -78,11 +78,6 @@ export const PodLogSearch = observer((props: Props) => {
onClick={onNextOverlay}
disabled={jumpDisabled}
/>
<Icon
material="close"
tooltip={`Clear`}
onClick={onClear}
/>
</div>
);
});

View File

@ -21,7 +21,7 @@ type PodLogLine = string;
export const logRange = 500;
@autobind()
export class PodLogsStore extends DockTabStore<IPodLogsData> {
export class LogStore extends DockTabStore<IPodLogsData> {
private refresher = interval(10, () => {
const id = dockStore.selectedTabId;
@ -39,7 +39,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
autorun(() => {
const { selectedTab, isOpen } = dockStore;
if (isPodLogsTab(selectedTab) && isOpen) {
if (isLogsTab(selectedTab) && isOpen) {
this.refresher.start();
} else {
this.refresher.stop();
@ -203,7 +203,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
}
}
export const podLogsStore = new PodLogsStore();
export const podLogsStore = new LogStore();
export function createPodLogsTab(data: IPodLogsData, tabParams: Partial<IDockTab> = {}) {
const podId = data.pod.getId();
@ -227,6 +227,6 @@ export function createPodLogsTab(data: IPodLogsData, tabParams: Partial<IDockTab
return tab;
}
export function isPodLogsTab(tab: IDockTab) {
export function isLogsTab(tab: IDockTab) {
return tab && tab.kind === TabKind.POD_LOGS;
}

View File

@ -6,9 +6,11 @@ import { searchStore } from "../../../common/search-store";
import { autobind } from "../../utils";
import { IDockTab } from "./dock.store";
import { InfoPanel } from "./info-panel";
import { PodLogControls } from "./pod-log-controls";
import { PodLogList } from "./pod-log-list";
import { IPodLogsData, podLogsStore } from "./pod-logs.store";
import { LogResourceSelector } from "./log-resource-selector";
import { LogList } from "./log-list";
import { IPodLogsData, podLogsStore } from "./log.store";
import { LogSearch } from "./log-search";
import { LogControls } from "./log-controls";
interface Props {
className?: string
@ -16,10 +18,10 @@ interface Props {
}
@observer
export class PodLogs extends React.Component<Props> {
export class Logs extends React.Component<Props> {
@observable isLoading = true;
private logListElement = React.createRef<PodLogList>(); // A reference for VirtualList component
private logListElement = React.createRef<LogList>(); // A reference for VirtualList component
componentDidMount() {
disposeOnUnmount(this,
@ -79,25 +81,26 @@ export class PodLogs extends React.Component<Props> {
}, 100);
}
render() {
renderResourceSelector() {
const logs = podLogsStore.logs;
const searchLogs = this.tabData.showTimestamps ? logs : podLogsStore.logsWithoutTimestamps;
const controls = (
<PodLogControls
ready={!this.isLoading}
tabId={this.tabId}
<div className="flex gaps">
<LogResourceSelector
tabData={this.tabData}
logs={logs}
save={this.save}
reload={this.reload}
/>
<LogSearch
onSearch={this.onSearch}
logs={searchLogs}
toPrevOverlay={this.toOverlay}
toNextOverlay={this.toOverlay}
/>
</div>
);
return (
<div className="PodLogs flex column">
<InfoPanel
tabId={this.props.tab.id}
controls={controls}
@ -105,13 +108,28 @@ export class PodLogs extends React.Component<Props> {
showButtons={false}
showStatusPanel={false}
/>
<PodLogList
);
}
render() {
const logs = podLogsStore.logs;
return (
<div className="PodLogs flex column">
{this.renderResourceSelector()}
<LogList
logs={logs}
id={this.tabId}
isLoading={this.isLoading}
load={this.load}
ref={this.logListElement}
/>
<LogControls
logs={logs}
tabData={this.tabData}
save={this.save}
reload={this.reload}
/>
</div>
);
}

View File

@ -1,126 +0,0 @@
import "./pod-log-controls.scss";
import React from "react";
import { observer } from "mobx-react";
import { IPodLogsData, podLogsStore } from "./pod-logs.store";
import { Select, SelectOption } from "../select";
import { Badge } from "../badge";
import { Icon } from "../icon";
import { cssNames, saveFileDialog } from "../../utils";
import { Pod } from "../../api/endpoints";
import { PodLogSearch, PodLogSearchProps } from "./pod-log-search";
interface Props extends PodLogSearchProps {
ready: boolean
tabId: string
tabData: IPodLogsData
logs: string[]
save: (data: Partial<IPodLogsData>) => void
reload: () => void
onSearch: (query: string) => void
}
export const PodLogControls = observer((props: Props) => {
const { tabData, save, reload, logs } = props;
const { selectedContainer, showTimestamps, previous } = tabData;
const since = logs.length ? podLogsStore.getTimestamps(logs[0]) : null;
const pod = new Pod(tabData.pod);
const toggleTimestamps = () => {
save({ showTimestamps: !showTimestamps });
};
const togglePrevious = () => {
save({ previous: !previous });
reload();
};
const downloadLogs = () => {
const fileName = selectedContainer ? selectedContainer.name : pod.getName();
const logsToDownload = showTimestamps ? logs : podLogsStore.logsWithoutTimestamps;
saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain");
};
const onContainerChange = (option: SelectOption) => {
const { containers, initContainers } = tabData;
save({
selectedContainer: containers
.concat(initContainers)
.find(container => container.name === option.value)
});
reload();
};
const containerSelectOptions = () => {
const { containers, initContainers } = tabData;
return [
{
label: `Containers`,
options: containers.map(container => {
return { value: container.name };
}),
},
{
label: `Init Containers`,
options: initContainers.map(container => {
return { value: container.name };
}),
}
];
};
const formatOptionLabel = (option: SelectOption) => {
const { value, label } = option;
return label || <><Icon small material="view_carousel"/> {value}</>;
};
return (
<div className="PodLogControls flex gaps align-center">
<span>Pod:</span> <Badge label={pod.getName()}/>
<span>Namespace:</span> <Badge label={pod.getNs()}/>
<span>Container</span>
<Select
options={containerSelectOptions()}
value={{ value: selectedContainer.name }}
formatOptionLabel={formatOptionLabel}
onChange={onContainerChange}
autoConvertOptions={false}
/>
<div className="time-range">
{since && (
<>
Since{" "}
<b>{new Date(since[0]).toLocaleString()}</b>
</>
)}
</div>
<div className="flex box grow gaps align-center">
<Icon
material="av_timer"
onClick={toggleTimestamps}
className={cssNames("timestamps-icon", { active: showTimestamps })}
tooltip={`${showTimestamps ? `Hide` : `Show`} timestamps`}
/>
<Icon
material="history"
onClick={togglePrevious}
className={cssNames("undo-icon", { active: previous })}
tooltip={(previous ? `Show current logs` : `Show previous terminated container logs`)}
/>
<Icon
material="get_app"
onClick={downloadLogs}
tooltip={`Save`}
className="download-icon"
/>
<PodLogSearch
{...props}
logs={showTimestamps ? logs : podLogsStore.logsWithoutTimestamps}
/>
</div>
</div>
);
});