diff --git a/packages/core/src/core/jobs/job.ts b/packages/core/src/core/jobs/job.ts index fde794e56fa..326a081ee50 100644 --- a/packages/core/src/core/jobs/job.ts +++ b/packages/core/src/core/jobs/job.ts @@ -18,5 +18,10 @@ export interface Abortable { abort(): void } +export interface FlowControllable { + xon(): void + xoff(): void +} + /** in the future, a WatchableJob may be more than Abortable, e.g. Suspendable */ export type WatchableJob = Abortable diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a14bd060239..ba3b10ea657 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -79,7 +79,7 @@ export { isMetadataBearing as isResourceWithMetadata } from './models/entity' export { isWatchable, Watchable, Watcher, WatchPusher } from './core/jobs/watchable' -export { Abortable } from './core/jobs/job' +export { Abortable, FlowControllable } from './core/jobs/job' import { Tab } from './webapp/tab' // eslint-disable-next-line @typescript-eslint/no-unused-vars export async function History(tab: Tab) { diff --git a/packages/core/src/models/execOptions.ts b/packages/core/src/models/execOptions.ts index 003666ac2da..28e606af9e8 100644 --- a/packages/core/src/models/execOptions.ts +++ b/packages/core/src/models/execOptions.ts @@ -16,9 +16,9 @@ import { ExecType } from './command' import { Tab } from '../webapp/tab' -import { Streamable, StreamableFactory } from './streamable' +import { Stream, Streamable, StreamableFactory } from './streamable' import { Block } from '../webapp/models/block' -import { Abortable } from '../core/jobs/job' +import { Abortable, FlowControllable } from '../core/jobs/job' export interface ExecOptions { /** force execution in a given tab? */ @@ -89,7 +89,7 @@ export interface ExecOptions { stderr?: (str: string) => any // eslint-disable-line @typescript-eslint/no-explicit-any /** on job init, pass the job, and get back a stdout */ - onInit?: (job: Abortable) => (str: Streamable) => void + onInit?: (job: Abortable & FlowControllable) => Stream | Promise parameters?: any // eslint-disable-line @typescript-eslint/no-explicit-any entity?: any // eslint-disable-line @typescript-eslint/no-explicit-any diff --git a/packages/core/src/models/streamable.ts b/packages/core/src/models/streamable.ts index f55b640b5f2..489d1116e2f 100644 --- a/packages/core/src/models/streamable.ts +++ b/packages/core/src/models/streamable.ts @@ -19,6 +19,6 @@ import { ScalarResponse } from './entity' export type Streamable = ScalarResponse // SimpleEntity | Table | MixedResponse | MultiModalResponse export default Streamable -export type Stream = (response: Streamable) => Promise +export type Stream = (response: Streamable) => void | Promise -export type StreamableFactory = () => Promise +export type StreamableFactory = () => Stream | Promise diff --git a/plugins/plugin-bash-like/src/pty/client.ts b/plugins/plugin-bash-like/src/pty/client.ts index 76f32a22ea1..31508354090 100644 --- a/plugins/plugin-bash-like/src/pty/client.ts +++ b/plugins/plugin-bash-like/src/pty/client.ts @@ -708,7 +708,7 @@ const remoteChannelFactory: ChannelFactory = async (tab: Tab) => { const { proto, port, path, uid, gid } = resp.content const protoHostPortContextRoot = window.location.href .replace(/#?\/?$/, '') - .replace(/^http(s?):\/\/([^:/]+)(:\d+)?/, `${proto}://$2${port === -1 ? '$3' : ':'+port}`) + .replace(/^http(s?):\/\/([^:/]+)(:\d+)?/, `${proto}://$2${port === -1 ? '$3' : ':' + port}`) .replace(/\/(index\.html)?$/, '') const url = new URL(path, protoHostPortContextRoot).href debug('websocket url', url, proto, port, path, uid, gid) @@ -938,11 +938,20 @@ export const doExec = ( if (execOptions.onInit) { const job = { + xon: () => { + debug('xon requested') + ws.send(JSON.stringify({ type: 'xon', uuid: ourUUID })) + }, + xoff: () => { + debug('xoff requested') + ws.send(JSON.stringify({ type: 'xoff', uuid: ourUUID })) + }, abort: () => { + debug('abort requested') ws.send(JSON.stringify({ type: 'kill', uuid: ourUUID })) } } - execOptions.stdout = execOptions.onInit(job) + execOptions.stdout = await execOptions.onInit(job) } } diff --git a/plugins/plugin-bash-like/src/pty/server.ts b/plugins/plugin-bash-like/src/pty/server.ts index ada771a04b6..e632e366884 100755 --- a/plugins/plugin-bash-like/src/pty/server.ts +++ b/plugins/plugin-bash-like/src/pty/server.ts @@ -251,6 +251,24 @@ export const onConnection = (exitNow: ExitHandler, uid?: number, gid?: number) = } = JSON.parse(data) switch (msg.type) { + case 'xon': { + const shell = msg.uuid && (await shells[msg.uuid]) + if (shell) { + const RESUME = '\x11' // this is XON + shell.write(RESUME) + } + break + } + + case 'xoff': { + const shell = msg.uuid && (await shells[msg.uuid]) + if (shell) { + const PAUSE = '\x13' // this is XOFF + shell.write(PAUSE) + } + break + } + case 'kill': { const shell = msg.uuid && (await shells[msg.uuid]) if (shell) { diff --git a/plugins/plugin-client-common/src/components/Content/Eval.tsx b/plugins/plugin-client-common/src/components/Content/Eval.tsx index 81b73c8bed1..e148bb2b9b1 100644 --- a/plugins/plugin-client-common/src/components/Content/Eval.tsx +++ b/plugins/plugin-client-common/src/components/Content/Eval.tsx @@ -18,11 +18,8 @@ import * as React from 'react' import * as Debug from 'debug' import { - ParsedOptions, - Tab as KuiTab, ScalarResource, SupportedStringContent, - MultiModalResponse, isCommandStringContent, FunctionThatProducesContent, ReactProvider, @@ -33,18 +30,12 @@ import { } from '@kui-shell/core' import { Loading } from '../../' -import KuiMMRContent from './KuiContent' +import KuiMMRContent, { KuiMMRProps } from './KuiContent' const debug = Debug('plugins/sidecar/Eval') -interface EvalProps { - tab: KuiTab +interface EvalProps extends Omit { command: string | FunctionThatProducesContent - args: { - argvNoOptions: string[] - parsedOptions: ParsedOptions - } - response: MultiModalResponse contentType?: SupportedStringContent } @@ -129,6 +120,6 @@ export default class Eval extends React.PureComponent { contentType: this.state.contentType } - return + return } } diff --git a/plugins/plugin-client-common/src/components/Content/KuiContent.tsx b/plugins/plugin-client-common/src/components/Content/KuiContent.tsx index 0ef23062fba..d56839f69fe 100644 --- a/plugins/plugin-client-common/src/components/Content/KuiContent.tsx +++ b/plugins/plugin-client-common/src/components/Content/KuiContent.tsx @@ -42,7 +42,7 @@ import HTMLString from './HTMLString' import HTMLDom from './Scalar/HTMLDom' import RadioTableSpi from '../spi/RadioTable' -interface KuiMMRProps { +export interface KuiMMRProps { tab: KuiTab mode: Content response: MultiModalResponse @@ -55,7 +55,7 @@ interface KuiMMRProps { export default class KuiMMRContent extends React.PureComponent { public render() { - const { tab, mode, response, willUpdateToolbar, args } = this.props + const { tab, mode, response, willUpdateToolbar } = this.props if (isStringWithOptionalContentType(mode)) { if (mode.contentType === 'text/html') { @@ -74,18 +74,16 @@ export default class KuiMMRContent extends React.PureComponent { ) } } else if (isCommandStringContent(mode)) { - return ( - - ) + return } else if (isFunctionContent(mode)) { - return + return } else if (isScalarContent(mode)) { if (isReactProvider(mode)) { return mode.react({ willUpdateToolbar }) diff --git a/plugins/plugin-client-common/src/components/Views/Sidecar/Toolbar.tsx b/plugins/plugin-client-common/src/components/Views/Sidecar/Toolbar.tsx index de67dd81c4b..0356b776509 100644 --- a/plugins/plugin-client-common/src/components/Views/Sidecar/Toolbar.tsx +++ b/plugins/plugin-client-common/src/components/Views/Sidecar/Toolbar.tsx @@ -31,6 +31,12 @@ export type Props = { } } +/** helper to ensure exhaustiveness of the switch statement below */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function assertUnreachable(x: never): never { + throw new Error('Did not expect to get here') +} + export default class Toolbar extends React.PureComponent { private icon() { if (this.props.toolbarText) { @@ -40,9 +46,13 @@ export default class Toolbar extends React.PureComponent { return case 'warning': return - default: + case 'error': return } + + // this bit of magic ensures exhaustiveness of the switch; + // reference: https://stackoverflow.com/a/39419171 + return assertUnreachable(type) } } diff --git a/plugins/plugin-client-common/src/components/spi/Icons/impl/PatternFly.tsx b/plugins/plugin-client-common/src/components/spi/Icons/impl/PatternFly.tsx index 888021b8eca..676dcfe23fa 100644 --- a/plugins/plugin-client-common/src/components/spi/Icons/impl/PatternFly.tsx +++ b/plugins/plugin-client-common/src/components/spi/Icons/impl/PatternFly.tsx @@ -35,8 +35,8 @@ import { ArrowLeftIcon as Back, ArrowRightIcon as Forward, InfoCircleIcon as Info, - WarningTriangleIcon as Warning, - ExclamationTriangleIcon as Oops, + ExclamationTriangleIcon as Warning, + BombIcon as Oops, ListIcon as List, ThIcon as Grid, CaretLeftIcon as PreviousPage, diff --git a/plugins/plugin-client-common/web/css/static/ansi_up.scss b/plugins/plugin-client-common/web/css/static/ansi_up.scss new file mode 100644 index 00000000000..90bbb7a33e5 --- /dev/null +++ b/plugins/plugin-client-common/web/css/static/ansi_up.scss @@ -0,0 +1,93 @@ +/* + * Copyright 2020 IBM Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.ansi-black-fg { + color: var(--color-black); +} +.ansi-black-bg, +.ansi-bright-black-bg { + background-color: var(--color-black); +} + +.ansi-bright-black-fg { + color: var(--color-text-02); +} +.ansi-bright-black-bg { + background-color: var(--color-text-02); +} + +.ansi-red-fg, +.ansi-bright-red-fg { + color: var(--color-red); +} +.ansi-red-bg, +.ansi-bright-red-bg { + background-color: var(--color-red); +} + +.ansi-green-fg, +.ansi-bright-green-fg { + color: var(--color-green); +} +.ansi-green-bg, +.ansi-bright-green-bg { + background-color: var(--color-green); +} + +.ansi-yellow-fg, +.ansi-bright-yellow-fg { + color: var(--color-yellow); +} +.ansi-yellow-bg, +.ansi-bright-yellow-bg { + background-color: var(--color-yellow); +} + +.ansi-blue-fg, +.ansi-bright-blue-fg { + color: var(--color-blue); +} +.ansi-blue-bg, +.ansi-bright-blue-bg { + background-color: var(--color-blue); +} + +.ansi-magenta-fg, +.ansi-bright-magenta-fg { + color: var(--color-green); +} +.ansi-magenta-bg, +.ansi-bright-magenta-bg { + background-color: var(--color-green); +} + +.ansi-cyan-fg, +.ansi-bright-cyan-fg { + color: var(--color-cyan); +} +.ansi-cyan-bg, +.ansi-bright-cyan-bg { + background-color: var(--color-cyan); +} + +.ansi-white-fg, +.ansi-bright-white-fg { + color: var(--color-white); +} +.ansi-white-bg, +.ansi-bright-white-bg { + background-color: var(--color-white); +} diff --git a/plugins/plugin-client-common/web/css/static/repl.scss b/plugins/plugin-client-common/web/css/static/repl.scss index a1d0580444c..26406045d0c 100644 --- a/plugins/plugin-client-common/web/css/static/repl.scss +++ b/plugins/plugin-client-common/web/css/static/repl.scss @@ -149,6 +149,9 @@ /* probably not 100% right, but: for pty output, don't show kui's "caret", since xtermjs has its own */ display: none; } +.repl-block.processing .repl-input input { + color: var(--color-name); +} .repl-block.processing .repl-result-spinner-inner { /* animation: spin 750ms linear infinite; */ height: auto; diff --git a/plugins/plugin-client-common/web/css/static/sidecar-main.css b/plugins/plugin-client-common/web/css/static/sidecar-main.css index 6ee6f2c6a20..9468f9bb4d1 100644 --- a/plugins/plugin-client-common/web/css/static/sidecar-main.css +++ b/plugins/plugin-client-common/web/css/static/sidecar-main.css @@ -21,6 +21,10 @@ margin: 0.75em 0 0; } + .kui--sidecar-text-content { + font-size: 0.875em; + } + .sidecar-bottom-stripe { flex-basis: 2.5em; display: flex; @@ -88,7 +92,9 @@ fill: var(--color-sidecar-toolbar-foreground); } .sidecar-toolbar-text[data-type="warning"] svg path { - fill: var(--color-warning); + fill: var(--color-base0A); + stroke: var(--color-base00); + stroke-width: 4%; } .sidecar-toolbar-text[data-type="warning"] svg path[data-icon-path="inner-path"], @@ -97,6 +103,7 @@ } .sidecar-toolbar-text[data-type="error"] svg path { fill: var(--color-error); + stroke: var(--color-base00); } .sidecar-toolbar-text:not([data-type]), .sidecar-bottom-stripe-mode-bits.sidecar-bottom-stripe-button-container:empty { diff --git a/plugins/plugin-client-common/web/css/static/ui.css b/plugins/plugin-client-common/web/css/static/ui.css index f1006dd0d1d..58b4e1d28b8 100644 --- a/plugins/plugin-client-common/web/css/static/ui.css +++ b/plugins/plugin-client-common/web/css/static/ui.css @@ -1,3 +1,5 @@ +@import "ansi_up"; + * { box-sizing: border-box; } @@ -203,6 +205,14 @@ body.still-loading .repl { } /* generic */ +.kui--hero-text { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + margin: 1em; + font-size: 2em; +} .flex-fill { flex: 1; } @@ -541,6 +551,9 @@ body.subwindow .result-as-multi-table-flex-wrap .big-top-pad:not(:last-child) > .scrollable.scrollable-auto { overflow-y: auto; overflow-x: hidden; + &.scrollable-x { + overflow-x: auto; + } } .scrollable { overflow-y: scroll; @@ -550,105 +563,6 @@ body.subwindow .result-as-multi-table-flex-wrap .big-top-pad:not(:last-child) > overflow-y: hidden; overflow-x: scroll; } -/* Track */ -.scrollable::-webkit-scrollbar-track, -.scrollable-x::-webkit-scrollbar-track, -.scrollable-auto::-webkit-scrollbar-track { - box-shadow: inset 0 0 8px var(--color-scrollbar-track); - border-radius: 10px; - border-radius: 10px; -} -/* Handle */ -.scrollable::-webkit-scrollbar-thumb, -.scrollable-x::-webkit-scrollbar-thumb, -.scrollable-auto::-webkit-scrollbar-thumb { - border-radius: 10px; - background: var(--color-scrollbar-thumb); - border: 1px solid var(--color-scrollbar-thumb-border); - outline: 1px solid var(--color-scrollbar-thumb-outline); -} -.result-as-table.scrollable::-webkit-scrollbar-thumb, -.result-as-table.scrollable::-webkit-scrollbar-track { - box-shadow: none; -} -.scrollable::-webkit-scrollbar, -.scrollable-auto::-webkit-scrollbar { - width: 9px; - height: 9px; -} -.scrollable-x::-webkit-scrollbar { - height: 8px; -} - -/* highlight.js tweaks */ -.hljs { - padding: 0; -} -.hljs-number, -.hljs-literal { - color: var(--color-brand-03); -} - -/* help */ -.help-options { - display: table; -} -.help-option { - display: table-row; -} -.help-option > div { - display: table-cell; - padding: 0.5ex 1ex; - white-space: normal; -} -.help-option-left-column { - font-weight: bold; /* make the command name bold */ - text-align: right; - border-left: 3px solid transparent; - width: 10em; -} -.help-option-interior-node-designation { - /* any designation that this command is a "context-changing" command, as opposed to a leaf-node command that actually does something */ - font-weight: normal; - opacity: 0.6; -} -.help-option .help-option-synonyms-column { - padding-right: 1.5em; - width: 10em; -} -.sidecar-visible .help-option .help-option-synonyms-column { - /* with the sidecar visible, there isn't room for the synonyms column */ - width: 0; -} -.sidecar-visible .help-option .help-option-synonym { - color: transparent; - width: 0; - padding: 0; -} -.help-option-synonyms-list { - display: flex; - align-items: center; - font-size: 80%; - opacity: 0.6; /* fira mono has no 300 font-weight specimen */ -} -.help-option-synonym:not(:last-child) { - padding-right: 1ex; -} -.help-option-synonym:not(:last-child):after { - content: ","; -} - -/* notification area */ -#notification-area { - display: flex; - align-items: center; -} -#notification-area > div { - margin-left: 0.5ex; -} -#notification-area a:not(:hover) { - color: inherit; -} /* delayed appearance animation */ .fade-in { @@ -798,151 +712,6 @@ body.oops-total-catastrophe #restart-needed-warning { opacity: 1; } -/* tooltips */ -.force-no-hover [data-balloon]:before, -.force-no-hover [data-balloon]:after { - /* e.g. the screenshot plugin wants to disallow tooltips on the - "capture screenshot" button while capturing a screenshot! */ - display: none; -} -[data-balloon]:hover { - cursor: pointer; - opacity: 1 !important; -} -[data-balloon]:after { - width: 13em; - white-space: normal !important; - text-align: center; -} -[kui-theme-style] .use-dark-tooltips [data-balloon]:after { - background-color: rgba(17, 17, 17, 0.9); - color: var(--color-white) !important; -} -[kui-theme-style] .use-dark-tooltips [data-balloon]:before { - background-color: var(--color-base00); -} -[kui-theme-style] [data-balloon]:after { - background-color: var(--color-base05); - color: var(--color-base01) !important; -} -[kui-theme-style] [data-balloon]:before { - background-image: none; - background-color: var(--color-base05); - mask-repeat: no-repeat; - - mask-position: 50% 100%; - -webkit-mask-position: 50% 100%; - mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba(17,17,17,0.9)%22%20transform%3D%22rotate(0)%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E"); - -webkit-mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba(17,17,17,0.9)%22%20transform%3D%22rotate(0)%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E"); -} -[kui-theme-style] [data-balloon][data-balloon-pos="left"]:before { - mask-position: 100% 50%; - -webkit-mask-position: 100% 50%; - mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba(17,17,17,0.9)%22%20transform%3D%22rotate(-90 18 18)%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E"); - -webkit-mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba(17,17,17,0.9)%22%20transform%3D%22rotate(-90 18 18)%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E"); -} -[kui-theme-style] [data-balloon][data-balloon-pos="down"]:before, -[kui-theme-style] [data-balloon][data-balloon-pos="down-left"]:before, -[kui-theme-style] [data-balloon][data-balloon-pos="down-right"]:before { - mask-position: 50% 0%; - -webkit-mask-position: 50% 0%; - mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba(17,17,17,0.9)%22%20transform%3D%22rotate(180 18 6)%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E"); - -webkit-mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba(17,17,17,0.9)%22%20transform%3D%22rotate(180 18 6)%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E"); -} -[kui-theme-style] [data-balloon][data-balloon-pos="right"]:before { - mask-position: 0% 50%; - -webkit-mask-position: 0% 50%; - mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba(17,17,17,0.9)%22%20transform%3D%22rotate(90 6 6)%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E"); - -webkit-mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba(17,17,17,0.9)%22%20transform%3D%22rotate(90 6 6)%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E"); -} -.main [data-balloon]:after { - font-size: 0.875rem !important; -} -[data-balloon][data-balloon-break]::before, -[data-balloon][data-balloon-break]::after { - /* you can add (\u000a if innerText) to data-balloon attrs to force a line break; make sure to add data-balloon-break attr, too */ - white-space: pre-wrap !important; -} - -/* help widget */ -.help-widget { - padding-left: 0.5ex; - font-size: 0.875em; -} - -.go-away-able { - transition-property: opacity; -} -.go-away { - opacity: 0; -} -.go-away-able .go-away-button { - transition-property: all; -} -.go-away-able .go-away-button:hover { - filter: brightness(1.2); - cursor: pointer; -} - -.list-paginator { - display: flex; - justify-content: flex-end; - font-family: var(--font-sans-serif); - color: var(--color-text-02); - background-color: transparent; - border: 1px solid var(--color-ui-04); - border-top: none; - height: 2em; -} -.list-paginator .list-paginator-left-buttons { - display: flex; - align-items: center; - padding: 0 1em; - flex: 1; -} -.list-paginator .list-paginator-left-buttons > span { - /* a list paginator left button */ - transition-property: all; -} -.list-paginator .list-paginator-left-buttons > span:hover { - opacity: 1; -} -.list-paginator .list-paginator-left-buttons > span:not(:first-child) { - /* a list paginator left button, except for the first one */ - margin-left: 1em; -} -.list-paginator .list-paginator-right-buttons { - display: flex; - font-weight: 400; - font-size: 0.875em; -} -.list-paginator .list-paginator-description { - display: flex; - align-items: center; - padding: 0.5rem 1rem; -} -.list-paginator .list-paginator-button { - border-left: 1px solid var(--color-ui-04); - transition-property: all; - width: 2.625rem; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.25em; -} -.list-paginator .list-paginator-button svg path { - fill: var(--color-text-02); -} -.list-paginator .list-paginator-button.list-paginator-button-disabled svg path { - fill: var(--color-ui-05); -} -.list-paginator .list-paginator-button:not(.list-paginator-button-disabled):hover { - cursor: pointer; -} -.list-paginator .list-paginator-button:not(.list-paginator-button-disabled):hover svg path { - fill: var(--color-brand-01); -} - .wrap-normal { /* i want this element to line wrap normally */ white-space: normal; @@ -1179,45 +948,12 @@ span[data-extra-decoration="application error"] { color: var(--color-red); } -.repl-block.processing .repl-input input { - color: var(--color-name); -} - -.entity.actions.app .repl-result-prefix { - color: hsla(275, 41%, 32%, 1); -} -.entity.activations.is-sequence-true .repl-result-prefix { - color: hsla(12, 45%, 45%, 1); -} -.entity .repl-result-prefix, -.entity .border-right { - border-right-color: var(--color-content-divider); -} -.entity.actions.sequence .repl-result-prefix { - color: hsla(265, 87%, 26%, 1); -} -.entity.packages .repl-result-prefix { - color: hsla(275, 21%, 22%, 1); -} - /* sidecar bottom stripe */ .clickable:not(.clickable-color):hover, .entity.actions:hover .entity-name-group:not(.header-cell) .entity-name { color: var(--color-brand-01); } -.graphical-clickable:hover { - filter: brightness(1.25); -} - -.help-option:hover { - color: #3e6550; -} - -.help-option:hover .help-option-left-column { - border-left-color: rgba(62, 101, 80, 0.2); -} - .log-lines .log-line .log-line-bar.is-waitTime, .legend-icon.is-waitTime { background: var(--color-ui-05); @@ -1410,27 +1146,35 @@ body[kui-theme-style="dark"] .kui--inverted-color-context { .color-base08 { color: var(--color-base08); + fill: currentColor; } .color-base09 { color: var(--color-base09); + fill: currentColor; } .color-base0A { color: var(--color-base0A); + fill: currentColor; } .color-base0B { color: var(--color-base0B); + fill: currentColor; } .color-base0C { color: var(--color-base0C); + fill: currentColor; } .color-base0D { color: var(--color-base0D); + fill: currentColor; } .color-base0E { color: var(--color-base0E); + fill: currentColor; } .color-base0F { color: var(--color-base0F); + fill: currentColor; } @media all and (max-width: 1100px) { @@ -1450,42 +1194,3 @@ body[kui-theme-style="dark"] .kui--inverted-color-context { display: flex; overflow: hidden; /* notes: chrome doesn't seem to need this; otherwise, repl scrolling will cause sidecar to overflow vertically */ } - -/* row selection */ -.has-row-selection .row-selection-context .clickable { - display: inline-block; -} -.row-selection-context.selected-row .selected-entity { - font-size: 1.25em; - color: var(--color-brand-03); - animation: selected-pulse 300ms 3; -} -.row-selection-context.selected-row .selected-entity svg path { - fill: var(--color-name) !important; -} -.row-selection-context:not(.selected-row) .selected-entity svg path { - fill: var(--color-text-02) !important; -} -.row-selection-context:not(.selected-row) .selected-entity:not(:hover) .kui--radio-checked svg, -.row-selection-context:not(.selected-row) .selected-entity:hover .kui--radio-unchecked svg, -.row-selection-context.selected-row .selected-entity .kui--radio-unchecked svg { - display: none; -} -.kui--radio-checked, -.kui--radio-unchecked { - display: flex; -} -.kui--radio-checked svg, -.kui--radio-unchecked svg { - width: 16px; - height: 16px; -} - -#offline-button { - display: none; -} - -#openwhisk-api-host, -#openwhisk-namespace { - pointer-events: none; -} diff --git a/plugins/plugin-kubectl/i18n/logs_en_US.json b/plugins/plugin-kubectl/i18n/logs_en_US.json index fc8577e21b3..c077f270f5f 100644 --- a/plugins/plugin-kubectl/i18n/logs_en_US.json +++ b/plugins/plugin-kubectl/i18n/logs_en_US.json @@ -6,6 +6,10 @@ "Level": "Level", "Message": "Message", "Details": "Details", + "Logs are live streaming": "Logs are live streaming", + "Log streaming is paused": "Log streaming is paused", + "Pause Streaming": "Pause Streaming", + "Resume Streaming": "Resume Streaming", "Occurred at": "Occurred at {0}", "Stack Trace": "Stack Trace", "Error": "Error" diff --git a/plugins/plugin-kubectl/i18n/resources_de.json b/plugins/plugin-kubectl/i18n/resources_de.json index 96e8a48383a..2a84fcf65af 100644 --- a/plugins/plugin-kubectl/i18n/resources_de.json +++ b/plugins/plugin-kubectl/i18n/resources_de.json @@ -1,6 +1,6 @@ { "startedOn": "Gestartet am", - "createdOn": "Erstellt am", + "createdOn": "Erstellt am {0}", "readonly": "Sie verwenden den schreibgeschützten Anzeigemodus", "contextsTableTitle": "Kubernetes-Kontexte", "deleteResource": "Diese Ressource löschen", diff --git a/plugins/plugin-kubectl/i18n/resources_en_US.json b/plugins/plugin-kubectl/i18n/resources_en_US.json index 72d707286f5..df18c2d657c 100644 --- a/plugins/plugin-kubectl/i18n/resources_en_US.json +++ b/plugins/plugin-kubectl/i18n/resources_en_US.json @@ -4,7 +4,7 @@ "Namespace": "Namespace", "Usage": "Usage", "startedOn": "Started on", - "createdOn": "Created on", + "createdOn": "Created on {0}", "readonly": "You are in read-only view mode", "contextsTableTitle": "Kubernetes Contexts", "deleteResource": "Delete this resource", diff --git a/plugins/plugin-kubectl/i18n/resources_es.json b/plugins/plugin-kubectl/i18n/resources_es.json index 1f4b8af8222..75488356e75 100644 --- a/plugins/plugin-kubectl/i18n/resources_es.json +++ b/plugins/plugin-kubectl/i18n/resources_es.json @@ -1,6 +1,6 @@ { "startedOn": "Iniciado el", - "createdOn": "Fecha de creación", + "createdOn": "Creado el", "readonly": "Está en modalidad de vista de solo lectura", "contextsTableTitle": "Contextos de Kubernetes", "deleteResource": "Suprimir este recurso", diff --git a/plugins/plugin-kubectl/i18n/resources_fr.json b/plugins/plugin-kubectl/i18n/resources_fr.json index fb0de99b701..800cf13a039 100644 --- a/plugins/plugin-kubectl/i18n/resources_fr.json +++ b/plugins/plugin-kubectl/i18n/resources_fr.json @@ -1,6 +1,6 @@ { "startedOn": "Démarré le", - "createdOn": "Créé le", + "createdOn": "Créé le {0}", "readonly": "Vous êtes en mode d'affichage lecture seule", "contextsTableTitle": "Contextes Kubernetes", "deleteResource": "Supprimer cette ressource", diff --git a/plugins/plugin-kubectl/i18n/resources_it.json b/plugins/plugin-kubectl/i18n/resources_it.json index b83b239bf90..bc55ab16d75 100644 --- a/plugins/plugin-kubectl/i18n/resources_it.json +++ b/plugins/plugin-kubectl/i18n/resources_it.json @@ -1,6 +1,6 @@ { "startedOn": "Avviato il", - "createdOn": "Creato il", + "createdOn": "Creato il {0}", "readonly": "Si sta utilizzando la modalità di visualizzazione sola lettura", "contextsTableTitle": "Contesti Kubernetes", "deleteResource": "Elimina questa risorsa", diff --git a/plugins/plugin-kubectl/i18n/resources_ja.json b/plugins/plugin-kubectl/i18n/resources_ja.json index c9f20b8b256..93ce9c03e83 100644 --- a/plugins/plugin-kubectl/i18n/resources_ja.json +++ b/plugins/plugin-kubectl/i18n/resources_ja.json @@ -1,6 +1,6 @@ { "startedOn": "開始日時", - "createdOn": "作成日時", + "createdOn": "作成日: {0}", "readonly": "読み取り専用表示モードです", "contextsTableTitle": "Kubernetes コンテキスト", "deleteResource": "このリソースの削除", diff --git a/plugins/plugin-kubectl/i18n/resources_ko.json b/plugins/plugin-kubectl/i18n/resources_ko.json index c79e6b67809..1663e396df5 100644 --- a/plugins/plugin-kubectl/i18n/resources_ko.json +++ b/plugins/plugin-kubectl/i18n/resources_ko.json @@ -1,6 +1,5 @@ { "startedOn": "시작일", - "createdOn": "작성일", "readonly": "읽기 전용 보기 모드임", "contextsTableTitle": "Kubernetes 컨텍스트", "deleteResource": "이 리소스 삭제", diff --git a/plugins/plugin-kubectl/i18n/resources_pt_BR.json b/plugins/plugin-kubectl/i18n/resources_pt_BR.json index 64882ed3f31..c582e806261 100644 --- a/plugins/plugin-kubectl/i18n/resources_pt_BR.json +++ b/plugins/plugin-kubectl/i18n/resources_pt_BR.json @@ -1,6 +1,6 @@ { "startedOn": "Iniciado em", - "createdOn": "Criado no", + "createdOn": "Criado em {0}", "readonly": "Você está no modo de visualização somente leitura", "contextsTableTitle": "Contextos do Kubernetes", "deleteResource": "Excluir este recurso", diff --git a/plugins/plugin-kubectl/i18n/resources_zh_CN.json b/plugins/plugin-kubectl/i18n/resources_zh_CN.json index 7f5edbe4a1e..bbe0b9d7850 100644 --- a/plugins/plugin-kubectl/i18n/resources_zh_CN.json +++ b/plugins/plugin-kubectl/i18n/resources_zh_CN.json @@ -1,6 +1,6 @@ { "startedOn": "开始日期", - "createdOn": "创建日期", + "createdOn": "创建于 {0}", "readonly": "您处于只读模式", "contextsTableTitle": "Kubernetes 上下文", "deleteResource": "删除此资源", diff --git a/plugins/plugin-kubectl/i18n/resources_zh_TW.json b/plugins/plugin-kubectl/i18n/resources_zh_TW.json index b82edcaad53..571e9d80f96 100644 --- a/plugins/plugin-kubectl/i18n/resources_zh_TW.json +++ b/plugins/plugin-kubectl/i18n/resources_zh_TW.json @@ -1,6 +1,6 @@ { "startedOn": "開始日期", - "createdOn": "建立於", + "createdOn": "創建於 {0}", "readonly": "您正在使用唯讀檢視模式", "contextsTableTitle": "Kubernetes 環境定義", "deleteResource": "刪除此資源", diff --git a/plugins/plugin-kubectl/logs/src/controller/kubectl/logs.ts b/plugins/plugin-kubectl/logs/src/controller/kubectl/logs.ts index b6d735538a2..60dd7d13ef4 100644 --- a/plugins/plugin-kubectl/logs/src/controller/kubectl/logs.ts +++ b/plugins/plugin-kubectl/logs/src/controller/kubectl/logs.ts @@ -17,12 +17,13 @@ import Debug from 'debug' import PrettyPrintAnsiString from 'ansi_up' import * as colors from 'colors/safe' -import { Abortable, Arguments, Registrar, Streamable } from '@kui-shell/core' +import { Abortable, Arguments, ExecType, FlowControllable, Registrar, Streamable } from '@kui-shell/core' import { isUsage, doHelp, KubeOptions, doExecWithPty, + doExecWithStdout, defaultFlags as flags, isHelpRequest } from '@kui-shell/plugin-kubectl' @@ -38,7 +39,7 @@ interface LogOptions extends KubeOptions { const debug = Debug('plugin-kubectl/logs/controller/kubectl/logs') -const literal = (match, p1, p2) => `${p1}${colors.blue(p2)}` +const literal = (match, p1, p2) => `${p1}${colors.gray(p2)}` const literal2 = (match, p1, p2) => `${p1}${colors.cyan(p2)}` const deemphasize = (match, p1, p2) => `${p1}${colors.gray(p2)}` const deemphasize2 = (match, p1, p2, p3, p4) => `${deemphasize(match, p1, p2)}${p4}` @@ -70,7 +71,7 @@ function decorateLogLines(lines: string): string { // various timestamp formats .replace( /(\w{3}\s+\d\d?\s+\d{2}:\d{2}:\d{2}|\d{2}:\d{2}:\d{2}.\d{6}|\w{3}\s+\w{3}\s+\d\d?\s+\d{2}:\d{2}:\d{2}\s+\d{4}|\[(\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2} [+-]\d{4})\]|\w{3},\s+\d{2}\s+\w{3}\s+\d{4}\s+\d{2}:\d{2}:\d{2}\s+\w{3}|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(.\d{3}?)|\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}(.\d+)?)/g, - (match, p1) => colors.cyan(p1) + (match, p1) => colors.bold(colors.cyan(p1)) ) // start/restart .replace(/(success|succeeded|starting|started|restarting|restarted)/gi, (match, p1) => colors.green(p1)) @@ -99,6 +100,10 @@ export async function doLogs(args: Arguments) { return doHelp('kubectl', args) } + if (args.execOptions.raw) { + return doExecWithStdout(args) + } + const streamed = args.parsedOptions.follow || args.parsedOptions.f // if we are streaming (logs -f), and the user did not specify a @@ -121,9 +126,6 @@ export async function doLogs(args: Arguments) { args.command = args.command + ' --tail=' + tail } - // set up the PTY stream; we want to stream to this stdout sink - const stdout = await args.createOutputStream() - // a bit of plumbing: tell the PTY that we will be handling everything const myExecOptions = Object.assign({}, args.execOptions, { rethrowErrors: true, // we want to handle errors @@ -133,7 +135,10 @@ export async function doLogs(args: Arguments) { // the PTY will call this when the PTY process is ready; in // return, we send it back a consumer of streaming output - onInit: (ptyJob: Abortable) => { + onInit: async (ptyJob: Abortable & FlowControllable) => { + // set up the PTY stream; we want to stream to this stdout sink + const stdout = args.execOptions.onInit ? await args.execOptions.onInit(ptyJob) : await args.createOutputStream() + let curLine: string // _ is one chunk of streaming output @@ -150,16 +155,23 @@ export async function doLogs(args: Arguments) { // eslint-disable-next-line no-control-regex const fullLine = /\x1b/.test(joined) ? joined : decorateLogLines(joined) - const lineDom = document.createElement('pre') - lineDom.classList.add('pre-wrap', 'kubeui--logs') const formatter = new PrettyPrintAnsiString() - // eslint-disable-next-line @typescript-eslint/camelcase - formatter.use_classes = true - lineDom.innerHTML = formatter.ansi_to_html(fullLine) - curLine = undefined - - // here is where we emit to the REPL: - stdout(lineDom) + formatter.use_classes = true // eslint-disable-line @typescript-eslint/camelcase + const innerHTML = formatter.ansi_to_html(fullLine) + + if (args.execOptions.type === ExecType.TopLevel) { + const lineDom = document.createElement('pre') + lineDom.classList.add('pre-wrap', 'kubeui--logs') + // eslint-disable-next-line @typescript-eslint/camelcase + formatter.use_classes = true + lineDom.innerHTML = innerHTML + curLine = undefined + + // here is where we emit to the REPL: + stdout(lineDom) + } else { + stdout(innerHTML) + } } else if (curLine) { // we did not get a terminal newline in this chunk curLine = curLine + _ diff --git a/plugins/plugin-kubectl/logs/src/test/logs/logs.ts b/plugins/plugin-kubectl/logs/src/test/logs/logs.ts index b805791eceb..9b863cc7a63 100644 --- a/plugins/plugin-kubectl/logs/src/test/logs/logs.ts +++ b/plugins/plugin-kubectl/logs/src/test/logs/logs.ts @@ -15,7 +15,7 @@ */ import * as assert from 'assert' -import { Common, CLI, ReplExpect, Selectors, SidecarExpect } from '@kui-shell/test' +import { Common, CLI, ReplExpect, Selectors } from '@kui-shell/test' import { createNS, allocateNS, deleteNS } from '@kui-shell/plugin-kubectl/tests/lib/k8s/utils' import { readFileSync } from 'fs' @@ -102,7 +102,7 @@ describe(`kubectl logs getty ${process.env.MOCHA_RUN_TARGET || ''}`, function(th } if (hasLogs) { - it('should show logs from sidecar', () => { + /* it('should show logs from sidecar', () => { return CLI.command(`kubectl get pod ${podName} -n ${ns} -o yaml`, this.app) .then(ReplExpect.justOK) .then(SidecarExpect.open) @@ -115,7 +115,7 @@ describe(`kubectl logs getty ${process.env.MOCHA_RUN_TARGET || ''}`, function(th }) ) .catch(Common.oops(this, true)) - }) + }) */ } } diff --git a/plugins/plugin-kubectl/src/controller/kubectl/get.ts b/plugins/plugin-kubectl/src/controller/kubectl/get.ts index 7d51d789103..a54b6d2795d 100644 --- a/plugins/plugin-kubectl/src/controller/kubectl/get.ts +++ b/plugins/plugin-kubectl/src/controller/kubectl/get.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { CodedError, Arguments, Registrar, MultiModalResponse, isHeadless, KResponse } from '@kui-shell/core' +import { Arguments, CodedError, KResponse, MultiModalResponse, Registrar, isHeadless, i18n } from '@kui-shell/core' import flags from './flags' import { exec } from './exec' @@ -29,6 +29,8 @@ import { KubeResource, isKubeResource } from '../../lib/model/resource' import { KubeOptions, isEntityRequest, isTableRequest, formatOf, isWatchRequest, getNamespace } from './options' import { stringToTable, KubeTableResponse, isKubeTableResponse } from '../../lib/view/formatTable' +const strings = i18n('plugin-kubectl') + /** * For now, we handle watch ourselves, so strip these options off the command line * @@ -129,6 +131,12 @@ export async function doGetAsMMR( version, originatingCommand: args.command, isKubeResource: true, + toolbarText: !resource.metadata.creationTimestamp + ? undefined + : { + type: 'info', + text: strings('createdOn', new Date(resource.metadata.creationTimestamp).toLocaleString()) + }, onclick: { kind: `kubectl get ${kindAndNamespaceOf(resource)}`, name: `kubectl get ${kindAndNamespaceOf(resource)} ${resource.metadata.name}`, diff --git a/plugins/plugin-kubectl/src/lib/view/modes/logs.tsx b/plugins/plugin-kubectl/src/lib/view/modes/logs.tsx new file mode 100644 index 00000000000..231d2a8dd75 --- /dev/null +++ b/plugins/plugin-kubectl/src/lib/view/modes/logs.tsx @@ -0,0 +1,205 @@ +/* + * Copyright 2020 IBM Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from 'react' +import { + i18n, + REPL, + Abortable, + FlowControllable, + Streamable, + Tab, + ModeRegistration, + ToolbarProps +} from '@kui-shell/core' + +import { Pod, isPod } from '../../model/resource' +import { Loading } from '@kui-shell/plugin-client-common' + +const strings = i18n('plugin-kubectl', 'logs') + +interface Props { + repl: REPL + pod: Pod + toolbarController: ToolbarProps + container?: string +} + +interface State { + ref?: HTMLElement + logs: string[] + isLive: boolean + job: Abortable & FlowControllable +} + +class Logs extends React.PureComponent { + public constructor(props: Props) { + super(props) + + this.state = { + logs: [], + isLive: false, + job: undefined + } + + this.initStream(true) + } + + public componentWillUnmount() { + if (this.state.job) { + this.state.job.abort() + } + } + + private updateToolbar(isLive = this.state.isLive) { + this.props.toolbarController.willUpdateToolbar( + { + type: isLive ? 'info' : 'warning', + text: isLive ? strings('Logs are live streaming') : strings('Log streaming is paused') + }, + [ + { + mode: 'toggle-streaming', + label: isLive ? strings('Pause Streaming') : strings('Resume Streaming'), + kind: 'view', + command: this.toggleStreaming.bind(this, !isLive), + order: -1 + } + ] + ) + } + + private doFlowControl(desiredStateIsLive = this.state.isLive) { + if (this.state.job) { + if (desiredStateIsLive) { + this.state.job.xon() + } else { + this.state.job.xoff() + } + } + } + + private toggleStreaming(desiredState: boolean) { + if (this.state.isLive !== desiredState) { + this.doFlowControl(desiredState) + this.updateToolbar(desiredState) + this.setState(curState => { + if (curState.isLive !== desiredState) { + return { isLive: desiredState } + } + }) + } + } + + private initStream(isLive: boolean) { + setTimeout(async () => { + const { pod, repl } = this.props + const cmd = `kubectl logs ${pod.metadata.name} -n ${pod.metadata.namespace} ${isLive ? '-f' : ''}` + + this.updateToolbar(isLive) + + repl.qexec(cmd, undefined, undefined, { + onInit: (job: Abortable & FlowControllable) => { + this.setState({ isLive, job }) + + return (_: Streamable) => { + if (typeof _ === 'string') { + this.setState(curState => ({ + logs: curState.logs.concat([_]) + })) + } + } + } + }) + }) + } + + private logs() { + if (this.state.logs.length === 0) { + return this.nothingToShow() + } else { + return this.state.logs.map((__html, idx) => ( +
+      ))
+    }
+  }
+
+  private doScroll(ref = this.state.ref) {
+    ref.scrollTop = ref.scrollHeight
+  }
+
+  private notDoneLoading() {
+    return 
+  }
+
+  private nothingToShow() {
+    return 
No log data
+ } + + public render() { + if (!this.state.job) { + return this.notDoneLoading() + } + + if (this.state.ref) { + setTimeout(() => this.doScroll()) + } + + return ( +
{ + if (ref) { + if (this.state.ref !== ref) { + this.doScroll(ref) + this.setState({ ref }) + } + } + }} + > + {this.logs()} +
+ ) + } +} + +/** + * The content renderer for the summary tab + * + */ +async function content({ REPL }: Tab, pod: Pod) { + return { + react: function LogsProvider(toolbarController: ToolbarProps) { + return + } + } +} + +/** + * The Summary mode applies to all KubeResources, and uses + * `renderContent` to render the view. + * + */ +const logsMode: ModeRegistration = { + when: isPod, + mode: { + mode: 'logs', + label: strings('Logs'), + content + } +} + +export default logsMode diff --git a/plugins/plugin-kubectl/src/non-headless-preload.ts b/plugins/plugin-kubectl/src/non-headless-preload.ts index 6ca7f1b8ef0..b2a0390c8db 100644 --- a/plugins/plugin-kubectl/src/non-headless-preload.ts +++ b/plugins/plugin-kubectl/src/non-headless-preload.ts @@ -24,7 +24,7 @@ import crdSummaryMode from './lib/view/modes/crd-summary' import configmapSummaryMode from './lib/view/modes/configmap-summary' import namespaceSummaryMode from './lib/view/modes/namespace-summary' import conditionsMode from './lib/view/modes/conditions' -import containersMode from './lib/view/modes/containers' +import logsMode from './lib/view/modes/logs' import lastAppliedMode from './lib/view/modes/last-applied' import showOwnerButton from './lib/view/modes/ShowOwnerButton' import showNodeButton from './lib/view/modes/ShowNodeOfPodButton' @@ -46,7 +46,7 @@ export default async (registrar: PreloadRegistrar) => { configmapSummaryMode, namespaceSummaryMode, conditionsMode, - containersMode, + logsMode, lastAppliedMode, showCRDResources, showOwnerButton, diff --git a/plugins/plugin-kubectl/src/test/k8s2/get-pod.ts b/plugins/plugin-kubectl/src/test/k8s2/get-pod.ts index a4282392600..46ffa6cfad3 100644 --- a/plugins/plugin-kubectl/src/test/k8s2/get-pod.ts +++ b/plugins/plugin-kubectl/src/test/k8s2/get-pod.ts @@ -40,7 +40,7 @@ commands.forEach(command => { * Interact with the Containers tab * */ - const testContainersTab = async (click = true) => { + /* const testContainersTab = async (click = true) => { if (click) { await this.app.client.click(Selectors.SIDECAR_MODE_BUTTON('containers')) } @@ -57,24 +57,22 @@ commands.forEach(command => { // check that the message shows the final state const message = await this.app.client.getText(`${table} [data-name="nginx"] [data-key="message"]`) assert.ok(!/Initializing/i.test(message)) - } - - /* const testLogTabs = async () => { - const container = `${Selectors.SIDECAR} .bx--data-table .entity[data-name="nginx"] .entity-name` - await this.app.client.waitForVisible(container) - await this.app.client.click(container) - await SidecarExpect.open(this.app) + } */ - await this.app.client.waitForVisible(Selectors.SIDECAR_BACK_BUTTON) // make sure the back button exists - await this.app.client.waitForVisible(Selectors.SIDECAR_MODE_BUTTON_SELECTED('logs')) // Latest Tab - await this.app.client.waitForVisible(Selectors.SIDECAR_MODE_BUTTON('previous')) + const testLogTab = async (click = true) => { + if (click) { + await this.app.client.waitForVisible(Selectors.SIDECAR_MODE_BUTTON('logs')) + await this.app.client.click(Selectors.SIDECAR_MODE_BUTTON('logs')) + await this.app.client.waitForVisible(Selectors.SIDECAR_MODE_BUTTON_SELECTED('logs')) + } - await this.app.client.waitForVisible(Selectors.SIDECAR_BACK_BUTTON) // make sure the back button exists - // await new Promise(resolve => setTimeout(resolve, 2000)) - await this.app.client.click(Selectors.SIDECAR_BACK_BUTTON) // transition back to the previous view + await SidecarExpect.toolbarText({ type: 'info', text: 'Logs are live', exact: false })(this.app) - await this.app.client.waitForVisible(Selectors.SIDECAR_MODE_BUTTON_SELECTED('containers')) - } */ + return this.app.client.waitUntil(async () => { + const text = await this.app.client.getText(`${Selectors.SIDECAR} .kui--sidecar-text-content`) + return text === 'No log data' + }) + } const ns: string = createNS() const inNamespace = `-n ${ns}` @@ -189,9 +187,10 @@ commands.forEach(command => { await SidecarExpect.open(this.app) .then(SidecarExpect.mode(defaultModeForGet)) .then(SidecarExpect.showing('nginx')) + .then(SidecarExpect.toolbarText({ type: 'info', text: 'Created on', exact: false })) - await testContainersTab() - // await testLogTabs() + // await testContainersTab() + await testLogTab() // await testContainersTab(false) // testing back button, don't click the container tab // await testLogTabs() // await testContainersTab(false) // testing back button, don't click the container tab @@ -302,13 +301,13 @@ commands.forEach(command => { } }) - it(`should click on containers sidecar tab and show containers table`, testContainersTab) + // it(`should click on containers sidecar tab and show containers table`, testContainersTab) // it('should drill down to log when container is clicked', testLogTabs) // it('should transition back from log and see containers table', testContainersTab.bind(this, false)) // testing back button, do not click the Container tab // it('should drill down to log when container is clicked', testLogTabs) - it('should transition back from log and see containers table', testContainersTab.bind(this, false)) // testing back button, do not click the Container tab + // it('should transition back from log and see containers table', testContainersTab.bind(this, false)) // testing back button, do not click the Container tab it(`should be able to show table with grep`, async () => { try {