toolbar.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { Text } from '@jupyterlab/coreutils';
  4. import {
  5. Button,
  6. circleEmptyIcon,
  7. circleIcon,
  8. classes,
  9. JLIcon,
  10. refreshIcon,
  11. stopIcon
  12. } from '@jupyterlab/ui-components';
  13. import { IIterator, find, map, some } from '@lumino/algorithm';
  14. import { CommandRegistry } from '@lumino/commands';
  15. import { ReadonlyJSONObject } from '@lumino/coreutils';
  16. import { Message, MessageLoop } from '@lumino/messaging';
  17. import { AttachedProperty } from '@lumino/properties';
  18. import { PanelLayout, Widget } from '@lumino/widgets';
  19. import * as React from 'react';
  20. import { ISessionContext, sessionContextDialogs } from './sessioncontext';
  21. import { UseSignal, ReactWidget } from './vdom';
  22. /**
  23. * The class name added to toolbars.
  24. */
  25. const TOOLBAR_CLASS = 'jp-Toolbar';
  26. /**
  27. * The class name added to toolbar items.
  28. */
  29. const TOOLBAR_ITEM_CLASS = 'jp-Toolbar-item';
  30. /**
  31. * The class name added to toolbar kernel name text.
  32. */
  33. const TOOLBAR_KERNEL_NAME_CLASS = 'jp-Toolbar-kernelName';
  34. /**
  35. * The class name added to toolbar spacer.
  36. */
  37. const TOOLBAR_SPACER_CLASS = 'jp-Toolbar-spacer';
  38. /**
  39. * The class name added to toolbar kernel status icon.
  40. */
  41. const TOOLBAR_KERNEL_STATUS_CLASS = 'jp-Toolbar-kernelStatus';
  42. /**
  43. * A layout for toolbars.
  44. *
  45. * #### Notes
  46. * This layout automatically collapses its height if there are no visible
  47. * toolbar widgets, and expands to the standard toolbar height if there are
  48. * visible toolbar widgets.
  49. */
  50. class ToolbarLayout extends PanelLayout {
  51. /**
  52. * A message handler invoked on a `'fit-request'` message.
  53. *
  54. * If any child widget is visible, expand the toolbar height to the normal
  55. * toolbar height.
  56. */
  57. protected onFitRequest(msg: Message): void {
  58. super.onFitRequest(msg);
  59. if (this.parent!.isAttached) {
  60. // If there are any widgets not explicitly hidden, expand the toolbar to
  61. // accommodate them.
  62. if (some(this.widgets, w => !w.isHidden)) {
  63. this.parent!.node.style.minHeight = 'var(--jp-private-toolbar-height)';
  64. } else {
  65. this.parent!.node.style.minHeight = '';
  66. }
  67. }
  68. // Set the dirty flag to ensure only a single update occurs.
  69. this._dirty = true;
  70. // Notify the ancestor that it should fit immediately. This may
  71. // cause a resize of the parent, fulfilling the required update.
  72. if (this.parent!.parent) {
  73. MessageLoop.sendMessage(this.parent!.parent!, Widget.Msg.FitRequest);
  74. }
  75. // If the dirty flag is still set, the parent was not resized.
  76. // Trigger the required update on the parent widget immediately.
  77. if (this._dirty) {
  78. MessageLoop.sendMessage(this.parent!, Widget.Msg.UpdateRequest);
  79. }
  80. }
  81. /**
  82. * A message handler invoked on an `'update-request'` message.
  83. */
  84. protected onUpdateRequest(msg: Message): void {
  85. super.onUpdateRequest(msg);
  86. if (this.parent!.isVisible) {
  87. this._dirty = false;
  88. }
  89. }
  90. /**
  91. * A message handler invoked on a `'child-shown'` message.
  92. */
  93. protected onChildShown(msg: Widget.ChildMessage): void {
  94. super.onChildShown(msg);
  95. // Post a fit request for the parent widget.
  96. this.parent!.fit();
  97. }
  98. /**
  99. * A message handler invoked on a `'child-hidden'` message.
  100. */
  101. protected onChildHidden(msg: Widget.ChildMessage): void {
  102. super.onChildHidden(msg);
  103. // Post a fit request for the parent widget.
  104. this.parent!.fit();
  105. }
  106. /**
  107. * A message handler invoked on a `'before-attach'` message.
  108. */
  109. protected onBeforeAttach(msg: Message): void {
  110. super.onBeforeAttach(msg);
  111. // Post a fit request for the parent widget.
  112. this.parent!.fit();
  113. }
  114. /**
  115. * Attach a widget to the parent's DOM node.
  116. *
  117. * @param index - The current index of the widget in the layout.
  118. *
  119. * @param widget - The widget to attach to the parent.
  120. *
  121. * #### Notes
  122. * This is a reimplementation of the superclass method.
  123. */
  124. protected attachWidget(index: number, widget: Widget): void {
  125. super.attachWidget(index, widget);
  126. // Post a fit request for the parent widget.
  127. this.parent!.fit();
  128. }
  129. /**
  130. * Detach a widget from the parent's DOM node.
  131. *
  132. * @param index - The previous index of the widget in the layout.
  133. *
  134. * @param widget - The widget to detach from the parent.
  135. *
  136. * #### Notes
  137. * This is a reimplementation of the superclass method.
  138. */
  139. protected detachWidget(index: number, widget: Widget): void {
  140. super.detachWidget(index, widget);
  141. // Post a fit request for the parent widget.
  142. this.parent!.fit();
  143. }
  144. private _dirty = false;
  145. }
  146. /**
  147. * A class which provides a toolbar widget.
  148. */
  149. export class Toolbar<T extends Widget = Widget> extends Widget {
  150. /**
  151. * Construct a new toolbar widget.
  152. */
  153. constructor() {
  154. super();
  155. this.addClass(TOOLBAR_CLASS);
  156. this.layout = new ToolbarLayout();
  157. }
  158. /**
  159. * Get an iterator over the ordered toolbar item names.
  160. *
  161. * @returns An iterator over the toolbar item names.
  162. */
  163. names(): IIterator<string> {
  164. let layout = this.layout as ToolbarLayout;
  165. return map(layout.widgets, widget => {
  166. return Private.nameProperty.get(widget);
  167. });
  168. }
  169. /**
  170. * Add an item to the end of the toolbar.
  171. *
  172. * @param name - The name of the widget to add to the toolbar.
  173. *
  174. * @param widget - The widget to add to the toolbar.
  175. *
  176. * @param index - The optional name of the item to insert after.
  177. *
  178. * @returns Whether the item was added to toolbar. Returns false if
  179. * an item of the same name is already in the toolbar.
  180. *
  181. * #### Notes
  182. * The item can be removed from the toolbar by setting its parent to `null`.
  183. */
  184. addItem(name: string, widget: T): boolean {
  185. let layout = this.layout as ToolbarLayout;
  186. return this.insertItem(layout.widgets.length, name, widget);
  187. }
  188. /**
  189. * Insert an item into the toolbar at the specified index.
  190. *
  191. * @param index - The index at which to insert the item.
  192. *
  193. * @param name - The name of the item.
  194. *
  195. * @param widget - The widget to add.
  196. *
  197. * @returns Whether the item was added to the toolbar. Returns false if
  198. * an item of the same name is already in the toolbar.
  199. *
  200. * #### Notes
  201. * The index will be clamped to the bounds of the items.
  202. * The item can be removed from the toolbar by setting its parent to `null`.
  203. */
  204. insertItem(index: number, name: string, widget: T): boolean {
  205. let existing = find(this.names(), value => value === name);
  206. if (existing) {
  207. return false;
  208. }
  209. widget.addClass(TOOLBAR_ITEM_CLASS);
  210. let layout = this.layout as ToolbarLayout;
  211. layout.insertWidget(index, widget);
  212. Private.nameProperty.set(widget, name);
  213. return true;
  214. }
  215. /**
  216. * Insert an item into the toolbar at the after a target item.
  217. *
  218. * @param at - The target item to insert after.
  219. *
  220. * @param name - The name of the item.
  221. *
  222. * @param widget - The widget to add.
  223. *
  224. * @returns Whether the item was added to the toolbar. Returns false if
  225. * an item of the same name is already in the toolbar.
  226. *
  227. * #### Notes
  228. * The index will be clamped to the bounds of the items.
  229. * The item can be removed from the toolbar by setting its parent to `null`.
  230. */
  231. insertAfter(at: string, name: string, widget: T): boolean {
  232. return this._insertRelative(at, 1, name, widget);
  233. }
  234. /**
  235. * Insert an item into the toolbar at the before a target item.
  236. *
  237. * @param at - The target item to insert before.
  238. *
  239. * @param name - The name of the item.
  240. *
  241. * @param widget - The widget to add.
  242. *
  243. * @returns Whether the item was added to the toolbar. Returns false if
  244. * an item of the same name is already in the toolbar.
  245. *
  246. * #### Notes
  247. * The index will be clamped to the bounds of the items.
  248. * The item can be removed from the toolbar by setting its parent to `null`.
  249. */
  250. insertBefore(at: string, name: string, widget: T): boolean {
  251. return this._insertRelative(at, 0, name, widget);
  252. }
  253. private _insertRelative(
  254. at: string,
  255. offset: number,
  256. name: string,
  257. widget: T
  258. ): boolean {
  259. let nameWithIndex = map(this.names(), (name, i) => {
  260. return { name: name, index: i };
  261. });
  262. let target = find(nameWithIndex, x => x.name === at);
  263. if (target) {
  264. return this.insertItem(target.index + offset, name, widget);
  265. }
  266. return false;
  267. }
  268. /**
  269. * Handle the DOM events for the widget.
  270. *
  271. * @param event - The DOM event sent to the widget.
  272. *
  273. * #### Notes
  274. * This method implements the DOM `EventListener` interface and is
  275. * called in response to events on the dock panel's node. It should
  276. * not be called directly by user code.
  277. */
  278. handleEvent(event: Event): void {
  279. switch (event.type) {
  280. case 'click':
  281. this.handleClick(event);
  282. break;
  283. default:
  284. break;
  285. }
  286. }
  287. /**
  288. * Handle a DOM click event.
  289. */
  290. protected handleClick(event: Event) {
  291. // Clicking a label focuses the corresponding control
  292. // that is linked with `for` attribute, so let it be.
  293. if (event.target instanceof HTMLLabelElement) {
  294. const forId = event.target.getAttribute('for');
  295. if (forId && this.node.querySelector(`#${forId}`)) {
  296. return;
  297. }
  298. }
  299. // If this click already focused a control, let it be.
  300. if (this.node.contains(document.activeElement)) {
  301. return;
  302. }
  303. // Otherwise, activate the parent widget, which may take focus if desired.
  304. if (this.parent) {
  305. this.parent.activate();
  306. }
  307. }
  308. /**
  309. * Handle `after-attach` messages for the widget.
  310. */
  311. protected onAfterAttach(msg: Message): void {
  312. this.node.addEventListener('click', this);
  313. }
  314. /**
  315. * Handle `before-detach` messages for the widget.
  316. */
  317. protected onBeforeDetach(msg: Message): void {
  318. this.node.removeEventListener('click', this);
  319. }
  320. }
  321. /**
  322. * The namespace for Toolbar class statics.
  323. */
  324. export namespace Toolbar {
  325. /**
  326. * Create an interrupt toolbar item.
  327. */
  328. export function createInterruptButton(
  329. sessionContext: ISessionContext
  330. ): Widget {
  331. return new ToolbarButton({
  332. icon: stopIcon,
  333. onClick: () => {
  334. void sessionContext.session?.kernel?.interrupt();
  335. },
  336. tooltip: 'Interrupt the kernel'
  337. });
  338. }
  339. /**
  340. * Create a restart toolbar item.
  341. */
  342. export function createRestartButton(
  343. sessionContext: ISessionContext,
  344. dialogs?: ISessionContext.IDialogs
  345. ): Widget {
  346. return new ToolbarButton({
  347. icon: refreshIcon,
  348. onClick: () => {
  349. void (dialogs ?? sessionContextDialogs).restart(sessionContext);
  350. },
  351. tooltip: 'Restart the kernel'
  352. });
  353. }
  354. /**
  355. * Create a toolbar spacer item.
  356. *
  357. * #### Notes
  358. * It is a flex spacer that separates the left toolbar items
  359. * from the right toolbar items.
  360. */
  361. export function createSpacerItem(): Widget {
  362. return new Private.Spacer();
  363. }
  364. /**
  365. * Create a kernel name indicator item.
  366. *
  367. * #### Notes
  368. * It will display the `'display_name`' of the session context. It can
  369. * handle a change in context or kernel.
  370. */
  371. export function createKernelNameItem(
  372. sessionContext: ISessionContext,
  373. dialogs?: ISessionContext.IDialogs
  374. ): Widget {
  375. const el = ReactWidget.create(
  376. <Private.KernelNameComponent
  377. sessionContext={sessionContext}
  378. dialogs={dialogs ?? sessionContextDialogs}
  379. />
  380. );
  381. el.addClass('jp-KernelName');
  382. return el;
  383. }
  384. /**
  385. * Create a kernel status indicator item.
  386. *
  387. * #### Notes
  388. * It will show a busy status if the kernel status is busy.
  389. * It will show the current status in the node title.
  390. * It can handle a change to the context or the kernel.
  391. */
  392. export function createKernelStatusItem(
  393. sessionContext: ISessionContext
  394. ): Widget {
  395. return new Private.KernelStatus(sessionContext);
  396. }
  397. }
  398. /**
  399. * Namespace for ToolbarButtonComponent.
  400. */
  401. export namespace ToolbarButtonComponent {
  402. /**
  403. * Interface for ToolbarButttonComponent props.
  404. */
  405. export interface IProps {
  406. className?: string;
  407. label?: string;
  408. iconClass?: string;
  409. iconLabel?: string;
  410. icon?: JLIcon;
  411. tooltip?: string;
  412. onClick?: () => void;
  413. enabled?: boolean;
  414. }
  415. }
  416. /**
  417. * React component for a toolbar button.
  418. *
  419. * @param props - The props for ToolbarButtonComponent.
  420. */
  421. export function ToolbarButtonComponent(props: ToolbarButtonComponent.IProps) {
  422. // In some browsers, a button click event moves the focus from the main
  423. // content to the button (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus).
  424. // We avoid a click event by calling preventDefault in mousedown, and
  425. // we bind the button action to `mousedown`.
  426. const handleMouseDown = (event: React.MouseEvent) => {
  427. // Fire action only when left button is pressed.
  428. if (event.button === 0) {
  429. event.preventDefault();
  430. props.onClick?.();
  431. }
  432. };
  433. const handleKeyDown = (event: React.KeyboardEvent) => {
  434. const { key } = event;
  435. if (key === 'Enter' || key === ' ') {
  436. props.onClick?.();
  437. }
  438. };
  439. const Icon = () => {
  440. if (props.icon) {
  441. return (
  442. <props.icon.react
  443. className={classes(props.iconClass, 'jp-ToolbarButtonComponent-icon')}
  444. tag="span"
  445. justify="center"
  446. kind="toolbarButton"
  447. />
  448. );
  449. } else if (props.iconClass) {
  450. return (
  451. <JLIcon.getReact
  452. name={classes(props.iconClass, 'jp-Icon', 'jp-Icon-16')}
  453. className="jp-ToolbarButtonComponent-icon"
  454. tag="span"
  455. justify="center"
  456. kind="toolbarButton"
  457. />
  458. );
  459. } else {
  460. return <></>;
  461. }
  462. };
  463. return (
  464. <Button
  465. className={
  466. props.className
  467. ? props.className + ' jp-ToolbarButtonComponent'
  468. : 'jp-ToolbarButtonComponent'
  469. }
  470. disabled={props.enabled === false}
  471. onMouseDown={handleMouseDown}
  472. onKeyDown={handleKeyDown}
  473. title={props.tooltip || props.iconLabel}
  474. minimal
  475. >
  476. <Icon />
  477. {props.label && (
  478. <span className="jp-ToolbarButtonComponent-label">{props.label}</span>
  479. )}
  480. </Button>
  481. );
  482. }
  483. /**
  484. * Adds the toolbar button class to the toolbar widget.
  485. * @param w Toolbar button widget.
  486. */
  487. export function addToolbarButtonClass(w: Widget): Widget {
  488. w.addClass('jp-ToolbarButton');
  489. return w;
  490. }
  491. /**
  492. * Phosphor Widget version of static ToolbarButtonComponent.
  493. */
  494. export class ToolbarButton extends ReactWidget {
  495. /**
  496. * Creates a toolbar button
  497. * @param props props for underlying `ToolbarButton` componenent
  498. */
  499. constructor(private props: ToolbarButtonComponent.IProps = {}) {
  500. super();
  501. addToolbarButtonClass(this);
  502. }
  503. render() {
  504. return <ToolbarButtonComponent {...this.props} />;
  505. }
  506. }
  507. /**
  508. * Namespace for CommandToolbarButtonComponent.
  509. */
  510. export namespace CommandToolbarButtonComponent {
  511. /**
  512. * Interface for CommandToolbarButtonComponent props.
  513. */
  514. export interface IProps {
  515. commands: CommandRegistry;
  516. id: string;
  517. args?: ReadonlyJSONObject;
  518. }
  519. }
  520. /**
  521. * React component for a toolbar button that wraps a command.
  522. *
  523. * This wraps the ToolbarButtonComponent and watches the command registry
  524. * for changes to the command.
  525. */
  526. export function CommandToolbarButtonComponent(
  527. props: CommandToolbarButtonComponent.IProps
  528. ) {
  529. return (
  530. <UseSignal
  531. signal={props.commands.commandChanged}
  532. shouldUpdate={(sender, args) =>
  533. (args.id === props.id && args.type === 'changed') ||
  534. args.type === 'many-changed'
  535. }
  536. >
  537. {() => <ToolbarButtonComponent {...Private.propsFromCommand(props)} />}
  538. </UseSignal>
  539. );
  540. }
  541. /*
  542. * Adds the command toolbar button class to the command toolbar widget.
  543. * @param w Command toolbar button widget.
  544. */
  545. export function addCommandToolbarButtonClass(w: Widget): Widget {
  546. w.addClass('jp-CommandToolbarButton');
  547. return w;
  548. }
  549. /**
  550. * Phosphor Widget version of CommandToolbarButtonComponent.
  551. */
  552. export class CommandToolbarButton extends ReactWidget {
  553. /**
  554. * Creates a command toolbar button
  555. * @param props props for underlying `CommandToolbarButtonComponent` componenent
  556. */
  557. constructor(private props: CommandToolbarButtonComponent.IProps) {
  558. super();
  559. addCommandToolbarButtonClass(this);
  560. }
  561. render() {
  562. return <CommandToolbarButtonComponent {...this.props} />;
  563. }
  564. }
  565. /**
  566. * A namespace for private data.
  567. */
  568. namespace Private {
  569. export function propsFromCommand(
  570. options: CommandToolbarButtonComponent.IProps
  571. ): ToolbarButtonComponent.IProps {
  572. let { commands, id, args } = options;
  573. const iconClass = commands.iconClass(id, args);
  574. const iconLabel = commands.iconLabel(id, args);
  575. const label = commands.label(id, args);
  576. let className = commands.className(id, args);
  577. // Add the boolean state classes.
  578. if (commands.isToggled(id, args)) {
  579. className += ' lm-mod-toggled';
  580. }
  581. if (!commands.isVisible(id, args)) {
  582. className += ' lm-mod-hidden';
  583. }
  584. const tooltip = commands.caption(id, args) || label || iconLabel;
  585. const onClick = () => {
  586. void commands.execute(id, args);
  587. };
  588. const enabled = commands.isEnabled(id, args);
  589. return { className, iconClass, tooltip, onClick, enabled, label };
  590. }
  591. /**
  592. * An attached property for the name of a toolbar item.
  593. */
  594. export const nameProperty = new AttachedProperty<Widget, string>({
  595. name: 'name',
  596. create: () => ''
  597. });
  598. /**
  599. * A no-op function.
  600. */
  601. export function noOp() {
  602. /* no-op */
  603. }
  604. /**
  605. * A spacer widget.
  606. */
  607. export class Spacer extends Widget {
  608. /**
  609. * Construct a new spacer widget.
  610. */
  611. constructor() {
  612. super();
  613. this.addClass(TOOLBAR_SPACER_CLASS);
  614. }
  615. }
  616. /**
  617. * Namespace for KernelNameComponent.
  618. */
  619. export namespace KernelNameComponent {
  620. /**
  621. * Interface for KernelNameComponent props.
  622. */
  623. export interface IProps {
  624. sessionContext: ISessionContext;
  625. dialogs: ISessionContext.IDialogs;
  626. }
  627. }
  628. /**
  629. * React component for a kernel name button.
  630. *
  631. * This wraps the ToolbarButtonComponent and watches the kernel
  632. * session for changes.
  633. */
  634. export function KernelNameComponent(props: KernelNameComponent.IProps) {
  635. const callback = () => {
  636. void props.dialogs.selectKernel(props.sessionContext);
  637. };
  638. return (
  639. <UseSignal
  640. signal={props.sessionContext.kernelChanged}
  641. initialSender={props.sessionContext}
  642. >
  643. {sessionContext => (
  644. <ToolbarButtonComponent
  645. className={TOOLBAR_KERNEL_NAME_CLASS}
  646. onClick={callback}
  647. tooltip={'Switch kernel'}
  648. label={sessionContext?.kernelDisplayName}
  649. />
  650. )}
  651. </UseSignal>
  652. );
  653. }
  654. /**
  655. * A toolbar item that displays kernel status.
  656. */
  657. export class KernelStatus extends Widget {
  658. /**
  659. * Construct a new kernel status widget.
  660. */
  661. constructor(sessionContext: ISessionContext) {
  662. super();
  663. this.addClass(TOOLBAR_KERNEL_STATUS_CLASS);
  664. this._onStatusChanged(sessionContext);
  665. sessionContext.statusChanged.connect(this._onStatusChanged, this);
  666. }
  667. /**
  668. * Handle a status on a kernel.
  669. */
  670. private _onStatusChanged(sessionContext: ISessionContext) {
  671. if (this.isDisposed) {
  672. return;
  673. }
  674. let status = sessionContext.kernelDisplayStatus;
  675. // set the icon
  676. if (this._isBusy(status)) {
  677. circleIcon.element({
  678. container: this.node,
  679. title: `Kernel ${Text.titleCase(status)}`,
  680. justify: 'center',
  681. kind: 'toolbarButton'
  682. });
  683. } else {
  684. circleEmptyIcon.element({
  685. container: this.node,
  686. title: `Kernel ${Text.titleCase(status)}`,
  687. justify: 'center',
  688. kind: 'toolbarButton'
  689. });
  690. }
  691. }
  692. /**
  693. * Check if status should be shown as busy.
  694. */
  695. private _isBusy(status: ISessionContext.KernelDisplayStatus): boolean {
  696. return (
  697. status === 'busy' || status === 'starting' || status === 'restarting'
  698. );
  699. }
  700. }
  701. }