Skip to content
This repository has been archived by the owner on May 21, 2019. It is now read-only.

Commit

Permalink
Job menu (#999)
Browse files Browse the repository at this point in the history
* Add job menu with sigkill button

* Complete job menu button
  • Loading branch information
drew-gross authored Mar 16, 2017
1 parent 4028ef3 commit 264ff76
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 15 deletions.
6 changes: 6 additions & 0 deletions src/shell/Job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ export class Job extends EmitterWithUniqueID implements TerminalLikeDevice {
}
}

sendSignal(signal: string): void {
if (this.pty) {
this.pty.kill(signal);
}
}

winch(): void {
if (this.pty && this.status === Status.InProgress) {
this.pty.dimensions = this.dimensions;
Expand Down
40 changes: 33 additions & 7 deletions src/views/4_PromptComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import * as _ from "lodash";
import * as e from "../Enums";
import * as React from "react";
import {AutocompleteComponent} from "./AutocompleteComponent";
import {FloatingMenu} from "./FloatingMenu";
import DecorationToggleComponent from "./DecorationToggleComponent";
import {History} from "../shell/History";
import {stopBubblingUp, getCaretPosition, setCaretPosition} from "./ViewUtils";
import {Prompt} from "../shell/Prompt";
import {Job} from "../shell/Job";
import {Suggestion} from "../plugins/autocompletion_utils/Common";
import Button from "../plugins/autocompletion_utils/Button";
import {KeyCode} from "../Enums";
import {getSuggestions} from "../Autocompletion";
import * as css from "./css/main";
Expand All @@ -31,6 +33,7 @@ interface State {
caretPositionFromPreviousFocus: number;
suggestions: Suggestion[];
isSticky: boolean;
showJobMenu: boolean;
}


Expand Down Expand Up @@ -64,6 +67,7 @@ export class PromptComponent extends React.Component<Props, State> {
caretPositionFromPreviousFocus: 0,
suggestions: [],
isSticky: false,
showJobMenu: false,
};
}

Expand Down Expand Up @@ -95,15 +99,18 @@ export class PromptComponent extends React.Component<Props, State> {
let autocompletedPreview: any;
let decorationToggle: any;
let scrollToTop: any;
let jobMenuButton: any;

if (this.showAutocomplete()) {
autocomplete = <AutocompleteComponent suggestions={this.state.suggestions}
offsetTop={this.state.offsetTop}
caretPosition={getCaretPosition(this.commandNode)}
onSuggestionHover={index => this.setState({...this.state, highlightedSuggestionIndex: index})}
onSuggestionClick={this.applySuggestion.bind(this)}
highlightedIndex={this.state.highlightedSuggestionIndex}
ref="autocomplete"/>;
autocomplete = <AutocompleteComponent
suggestions={this.state.suggestions}
offsetTop={this.state.offsetTop}
caretPosition={getCaretPosition(this.commandNode)}
onSuggestionHover={index => this.setState({...this.state, highlightedSuggestionIndex: index})}
onSuggestionClick={this.applySuggestion.bind(this)}
highlightedIndex={this.state.highlightedSuggestionIndex}
ref="autocomplete"
/>;
const completed = this.valueWithCurrentSuggestion;
if (completed.trim() !== this.prompt.value && completed.startsWith(this.prompt.value)) {
autocompletedPreview = <div style={css.autocompletedPreview}>{completed}</div>;
Expand All @@ -127,6 +134,12 @@ export class PromptComponent extends React.Component<Props, State> {
</span>;
}

jobMenuButton = <span style={{transform: "translateY(-1px)"}} className="jobMenu">
<Button
onClick={() => this.setState({showJobMenu: !this.state.showJobMenu} as State)}
>•••</Button>
</span>;

return <div ref="placeholder" id={this.props.job.id.toString()} style={css.promptPlaceholder}>
<div style={css.promptWrapper(this.props.status, this.state.isSticky)}>
<div style={css.arrow(this.props.status)}>
Expand Down Expand Up @@ -154,7 +167,20 @@ export class PromptComponent extends React.Component<Props, State> {
<div style={css.actions}>
{decorationToggle}
{scrollToTop}
{this.props.job.isInProgress() ? jobMenuButton : undefined}
</div>
{this.state.showJobMenu ? <FloatingMenu
highlightedIndex={0}
menuItems={[{
text: "Send SIGKILL",
action: () => this.props.job.sendSignal("SIGKILL"),
}, {
text: "Send SIGTERM",
action: () => this.props.job.sendSignal("SIGTERM"),
}]}
hide={() => this.setState({ showJobMenu: false } as any)}
offsetTop={this.state.offsetTop}
/> : undefined}
</div>
</div>;
}
Expand Down
14 changes: 8 additions & 6 deletions src/views/AutocompleteComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const SuggestionComponent = ({suggestion, onHover, onClick, isHighlighted}: Sugg
onMouseOver={onHover}
onClick={onClick}>

<i style={{...css.suggestionIcon, ...suggestion.style.css} as any} dangerouslySetInnerHTML={{__html: suggestion.style.value}}/>
<i style={{...css.suggestionIcon, ...suggestion.style.css} as any}>{suggestion.style.value}</i>
<span style={css.autocomplete.value}>{suggestion.displayValue}</span>
<span style={css.autocomplete.synopsis}>{suggestion.synopsis}</span>
</li>;
Expand All @@ -32,11 +32,13 @@ interface AutocompleteProps {
export class AutocompleteComponent extends React.Component<AutocompleteProps, {}> {
render() {
const suggestionViews = this.props.suggestions.map((suggestion, index) =>
<SuggestionComponent suggestion={suggestion}
onHover={() => this.props.onSuggestionHover(index)}
onClick={this.props.onSuggestionClick}
key={index}
isHighlighted={index === this.props.highlightedIndex}/>,
<SuggestionComponent
suggestion={suggestion}
onHover={() => this.props.onSuggestionHover(index)}
onClick={this.props.onSuggestionClick}
key={index}
isHighlighted={index === this.props.highlightedIndex}
/>,
);

const suggestionDescription = this.props.suggestions[this.props.highlightedIndex].description;
Expand Down
64 changes: 64 additions & 0 deletions src/views/FloatingMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as React from "react";
import * as css from "./css/main";
import {fontAwesome} from "./css/FontAwesome";

interface MenuItemProps {
suggestion: string;
onHover: () => void;
onClick: () => void;
isHighlighted: boolean;
}

const MenuItem = ({suggestion, onHover, onClick, isHighlighted}: MenuItemProps) =>
<li
style={css.autocomplete.item(isHighlighted)}
onMouseOver={onHover}
onClick={onClick}
className="floatingMenuItem"
>
<i style={css.suggestionIcon as any}>{fontAwesome.times}</i>
<span style={css.autocomplete.value}>{suggestion}</span>
</li>;

export interface MenuItemData {
text: string;
action: () => void;
};

interface Props {
offsetTop: number;
menuItems: MenuItemData[];
highlightedIndex: number;
hide: () => void;
}

interface State {
highlightedIndex: number;
}

export class FloatingMenu extends React.Component<Props, State> {
constructor(props: Props) {
super(props);

this.state = {
highlightedIndex: -1,
};
}

render() {
const suggestionViews = this.props.menuItems.map((item, index) => <MenuItem
suggestion={item.text}
onHover={() => this.setState({highlightedIndex: index})}
onClick={() => {
item.action();
this.props.hide();
}}
key={index}
isHighlighted={index === this.state.highlightedIndex}
/>);

return <div style={css.floatingMenu.box(this.props.offsetTop)}>
<ul style={css.autocomplete.suggestionsList}>{suggestionViews}</ul>
</div>;
}
}
23 changes: 23 additions & 0 deletions src/views/css/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,25 @@ export const suggestionIcon = {
backgroundColor: "rgba(0, 0, 0, 0.15)",
};

export const floatingMenu = {
box: (offsetTop: number) => {
// TODO: Make this be less magic. Use a computation
// that is based on the number of items in the menu.
// Also, should unify this with AutoocompleteMenu
const shouldDisplayAbove = offsetTop + 100 > window.innerHeight;
return {
position: "absolute",
top: shouldDisplayAbove ? "auto" : promptWrapperHeight,
bottom: shouldDisplayAbove ? suggestionSize : "auto",
minWidth: 300,
right: "20px",
boxShadow: defaultShadow,
backgroundColor: colors.black,
zIndex: 3,
};
},
};

export const autocomplete = {
box: (offsetTop: number, caretPosition: number, hasDescription: boolean) => {
const shouldDisplayAbove = offsetTop + (suggestionsLimit * suggestionSize) > window.innerHeight;
Expand Down Expand Up @@ -595,3 +614,7 @@ export const image = {
maxHeight: "90vh",
maxWidth: "100vh",
};

export const menuButton = {
color: "blue",
};
2 changes: 1 addition & 1 deletion test/autocompletion_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe("Autocompletion suggestions", () => {
});

it("wraps file names in quotes if necessary", async() => {
expect(await anyFilesSuggestions("fil", join(__dirname, "test_files"))).to.eql([{
expect(await anyFilesSuggestions("fil", join(__dirname, "test_files", "file_names_test"))).to.eql([{
attributes: {
displayValue: "file\\ with\\ brackets\\(\\)",
promptSerializer: noEscapeSpacesPromptSerializer,
Expand Down
22 changes: 21 additions & 1 deletion test/e2e.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const {Application} = require("spectron");
const {expect} = require("chai");
const {join} = require("path");

const timeout = 50000;

Expand Down Expand Up @@ -30,5 +31,24 @@ describe("application launch", function () {
then((output) => {
expect(output[0]).to.contain("expected-text");
});
})
});

it("send signals via button", function () {
return app.client.
waitUntilWindowLoaded().
waitForExist(".prompt", timeout).
setValue(".prompt", `node ${join(__dirname, "test_files", "print_on_sigterm.js")}\n`).
waitForExist(".jobMenu", timeout).
click(".jobMenu").
waitForExist(".floatingMenuItem").
click(".floatingMenuItem:last-of-type").
click(".jobMenu").
waitForExist(".floatingMenuItem").
click(".floatingMenuItem:first-of-type").
waitForExist(".prompt[contenteditable=true]").
getText(".job .output").
then(output => {
expect(output[0]).to.eql('Received SIGTERM');
});
});
});
2 changes: 2 additions & 0 deletions test/test_files/print_on_sigterm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
process.on('SIGTERM', () => console.log('Received SIGTERM'));
setInterval(() => {}, 1000)

0 comments on commit 264ff76

Please sign in to comment.