Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Live Stream Lambda Output During Execution #406

Merged
merged 19 commits into from
Oct 1, 2019
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 0 additions & 40 deletions front-end/src/components/Common/Loading.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,43 +29,3 @@ export default class LoadingContainer extends Vue implements LoadingContainerPro
@Prop() classes?: string;
}
</script>

<style scoped lang="scss">
.loading-helper__container {
position: relative;
margin: 0;
max-width: unset;
}

.loading-helper__overlay {
background-color: #f0f0f0;
color: #000000;
z-index: 9998;
position: absolute;
top: 0;
left: 0;
margin: 0;
border: none;
width: 100%;
height: 100%;
opacity: 0.9;
&--dark {
background-color: #333;
color: #fff;
opacity: 0.7;
}
}

.loading-helper__loading-text {
text-align: center;
z-index: 9999;
top: 50%;
transform: translateY(-50%);
left: 0;
bottom: 0;
margin: auto;
position: relative;
font-weight: bold;
font-size: 16px;
}
</style>
10 changes: 8 additions & 2 deletions front-end/src/components/Common/RefineryCodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default class RefineryCodeEditor extends Vue implements EditorProps {
@Prop() onChange?: (s: string) => void;
@Prop() fullscreenToggled?: () => void;
@Prop({ default: false }) disableFullscreen!: boolean;
@Prop({ default: false }) tailOutput!: boolean;

// Ace Props
@Prop() readOnly?: boolean;
Expand Down Expand Up @@ -67,12 +68,17 @@ export default class RefineryCodeEditor extends Vue implements EditorProps {
wordWrap: this.wrapText,
theme: this.theme || 'vs-dark',
automaticLayout: true,
onChange: this.onChange
onChange: this.onChange,
tailOutput: this.tailOutput
};

return (
// @ts-ignore
<MonacoEditor key={`${languageToAceLangMap[this.lang]}${this.readOnly ? '-read-only' : ''}`} ref="editor" props={monacoProps} />
<MonacoEditor
key={`${languageToAceLangMap[this.lang]}${this.readOnly ? '-read-only' : ''}`}
ref="editor"
props={monacoProps}
/>
);
}

Expand Down
65 changes: 46 additions & 19 deletions front-end/src/components/RunLambda.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,35 @@ export default class RunLambda extends Vue implements RunLambdaProps {
}

// If we have valid output for a Lambda based on it's ID.
if (this.lambdaIdOrArn && this.runResultOutputId === this.lambdaIdOrArn) {
if (this.lambdaIdOrArn) {
return true;
}

// Otherwise, false. This is not valid output.
return false;
}

public getRunLambdaReturnFieldValue(runResultOutput: RunLambdaResult | null) {
// Check if the Lambda is running, we'll show a different return value if it is
// to indicate to the user that the Code Block is currently still executing.
if (this.isCurrentlyRunning) {
return 'The Code Block has not finished executing yet, please wait...';
}

if (runResultOutput && runResultOutput.returned_data && typeof runResultOutput.returned_data === 'string') {
return runResultOutput.returned_data;
}

return 'Click Execute button for run output.';
}

public getRunLambdaOutput(hasValidOutput: boolean) {
// Check if the Lambda is running, we'll show a different return value if it is
// to indicate to the user that the Code Block is currently still executing.
if (this.isCurrentlyRunning && !this.runResultOutput) {
return 'No output from Code Block received yet, please wait...';
}

// Need to check this because Ace will shit the bed if given a *gasp* null value!
if (!hasValidOutput || !this.runResultOutput) {
return 'No return data to display.';
Expand Down Expand Up @@ -126,6 +146,25 @@ export default class RunLambda extends Vue implements RunLambdaProps {
);
}

/*
Disables the Execute With Data button if the Lambda is running
*/
getExecuteWithDataButton() {
if (this.isCurrentlyRunning) {
return (
<b-button variant="primary" disabled={true}>
<b-spinner small /> Code Block is executing, please wait...
</b-button>
);
}

return (
<b-button variant="primary" on={{ click: () => this.onRunLambda() }}>
Execute With Data
</b-button>
);
}

public renderEditors() {
const hasValidOutput = this.checkIfValidRunLambdaOutput();

Expand All @@ -151,7 +190,7 @@ export default class RunLambda extends Vue implements RunLambdaProps {
name: `result-data-${this.getNameSuffix()}`,
// This is very nice for rendering non-programming text
lang: hasResultData ? 'json' : 'text',
content: hasResultData || 'Click Execute button for run output.',
content: this.getRunLambdaReturnFieldValue(this.runResultOutput),
wrapText: true,
readOnly: true
};
Expand All @@ -163,7 +202,8 @@ export default class RunLambda extends Vue implements RunLambdaProps {
lang: 'text',
content: this.getRunLambdaOutput(hasValidOutput),
wrapText: true,
readOnly: true
readOnly: true,
tailOutput: true
};

const saveInputDataButton = (
Expand All @@ -176,9 +216,7 @@ export default class RunLambda extends Vue implements RunLambdaProps {
<div class="m-2">
<b-button-group class="width--100percent">
{this.displayLocation === RunLambdaDisplayLocation.editor && this.onSaveInputData && saveInputDataButton}
<b-button variant="primary" on={{ click: () => this.onRunLambda() }}>
Execute With Data
</b-button>
{this.getExecuteWithDataButton()}
</b-button-group>
</div>
);
Expand Down Expand Up @@ -213,22 +251,11 @@ export default class RunLambda extends Vue implements RunLambdaProps {
}

public render(h: CreateElement): VNode {
const loadingProps: LoadingContainerProps = {
show: this.isCurrentlyRunning,
dark: true,
label: this.loadingText,
classes: 'height--100percent width--100percent'
};

const classes = {
'run-lambda-container display--flex flex-direction--column': true,
'run-lambda-container display--flex flex-direction--column width--100percent': true,
[`run-lambda-container__${this.displayMode}`]: true
};

return (
<Loading props={loadingProps}>
<div class={classes}>{this.renderEditors()}</div>
</Loading>
);
return <div class={classes}>{this.renderEditors()}</div>;
}
}
57 changes: 57 additions & 0 deletions front-end/src/lib/MonacoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Prop, Watch } from 'vue-property-decorator';
import elementResizeDetector from 'element-resize-detector';
import IModelContentChangedEvent = monaco.editor.IModelContentChangedEvent;
import { timeout } from '@/utils/async-utils';
import { IScrollEvent } from 'monaco-editor';

export interface MonacoEditorProps {
readOnly?: boolean;
Expand All @@ -17,13 +18,17 @@ export interface MonacoEditorProps {
diffEditor?: boolean;
wordWrap?: boolean;
automaticLayout?: boolean;
tailOutput?: boolean;

onChange?: (s: string) => void;
}

@Component
export default class MonacoEditor extends Vue implements MonacoEditorProps {
editor?: any;
lastEditorHeight?: number;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not actionable feedback on this code and solely an anecdote about this:

I'm not crazy stoked about keeping track of this state in this component, but at the same time I understand why this makes sense to do. Longer term, we'll have to think about how we want to hold state for the editors. Would be nice to have this in the store (especially for debugging). But that's not easy to solve. So I'm fine with this living here for now until we have a better story around how to keep track of Monaco state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth making an issue for maybe?

lastEditorContentsLength?: number;
tailingEnabled?: boolean;

@Prop() public readOnly?: boolean;
@Prop() public original?: string;
Expand All @@ -34,6 +39,7 @@ export default class MonacoEditor extends Vue implements MonacoEditorProps {
@Prop({ default: false }) public diffEditor!: boolean;
@Prop({ default: false }) public wordWrap!: boolean;
@Prop({ default: false }) public automaticLayout!: boolean;
@Prop({ default: false }) public tailOutput!: boolean;

@Prop() onChange?: (s: string) => void;

Expand All @@ -52,6 +58,8 @@ export default class MonacoEditor extends Vue implements MonacoEditorProps {
if (newValue !== editor.getValue()) {
editor.setValue(newValue);
}

this.onTailedContentRefreshed();
}
}

Expand All @@ -70,6 +78,34 @@ export default class MonacoEditor extends Vue implements MonacoEditorProps {
}
}

onTailedContentRefreshed() {
const editor = this.getModifiedEditor();
if (this.tailOutput) {
const characterCount = editor.getValue().length;
const editorPreviouslyHadContent = this.lastEditorContentsLength !== undefined;
const newContentIsShorter =
this.lastEditorContentsLength !== undefined && characterCount < this.lastEditorContentsLength;

// If we detected that we suddenly have less content in the editor
// than we did previously that means that we are almost certainly
// starting a new run and can re-enable tailing!
if (editorPreviouslyHadContent && newContentIsShorter) {
this.tailingEnabled = true;
}

this.lastEditorContentsLength = characterCount;
}

// We use tailingEnabled instead of the prop because
// we want to disable tailing if the user has attempted to scroll
// up. Vue doesn't like you mutating props so we pull the state internally.
if (this.tailingEnabled) {
// Auto-scroll to the bottom
const lineCount = editor.getModel().getLineCount();
editor.revealLine(lineCount);
}
}

relayoutEditor() {
this.editor.layout();
}
Expand Down Expand Up @@ -122,6 +158,27 @@ export default class MonacoEditor extends Vue implements MonacoEditorProps {
}
});

// Enable tailing if it was set
this.lastEditorHeight = 0;
if (this.tailOutput) {
this.tailingEnabled = true;
}

// This is used with the tailing of output functionality to calculate if
// a user scrolled while the output was being tailed. If they have and it
// wasn't programmatically-caused then we need to stop tailing!
editor.onDidScrollChange((event: IScrollEvent) => {
if (this.lastEditorHeight === undefined) {
return;
}

if (this.lastEditorHeight > event.scrollTop) {
this.tailingEnabled = false;
}

this.lastEditorHeight = event.scrollTop;
});

editor.updateOptions({
insertSpaces: true,
readOnly: this.readOnly
Expand Down
15 changes: 5 additions & 10 deletions front-end/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,16 @@ import App from './App';
import router from './router';
import store from './store/index';
import './registerServiceWorker';
import { VueNativeSock } from 'vue-native-websocket';
import VueNativeSock from 'vue-native-websocket';
import { setupWebsocketVuePlugin } from '@/setup-websocket';

Vue.use(BootstrapVue);
Vue.use(VueIntercom, { appId: 'sjaaunj7' });

const websocketEndpoint =
`${process.env.VUE_APP_API_HOST}`.replace('https://', 'wss://').replace('http://', 'ws://') +
'/ws/v1/lambdas/livedebug';

Vue.use(VueNativeSock, websocketEndpoint, {
store: store
});

Vue.config.productionTip = false;

setupWebsocketVuePlugin(Vue, store);

// If, in the future, we need to unsync the router we can use this function.
const unsync = sync(store, router);

Expand All @@ -57,7 +52,7 @@ window.onbeforeunload = function(e: Event) {
}
};

new Vue({
const vm = new Vue({
router,
store,
render: h => h(App)
Expand Down
25 changes: 25 additions & 0 deletions front-end/src/setup-websocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import VueNativeSock from 'vue-native-websocket';
import { RootState } from '@/store/store-types';
import { Vue, VueConstructor } from 'vue/types/vue';
import { Store } from 'vuex';

export function setupWebsocketVuePlugin(Vue: VueConstructor, store: Store<RootState>) {
const websocketEndpoint =
`${process.env.VUE_APP_API_HOST}`.replace('https://', 'wss://').replace('http://', 'ws://') +
'/ws/v1/lambdas/livedebug';

const mutations = {
SOCKET_ONOPEN: 'runLambda/SOCKET_ONOPEN',
SOCKET_ONCLOSE: 'runLambda/SOCKET_ONCLOSE',
SOCKET_ONERROR: 'runLambda/SOCKET_ONERROR',
SOCKET_ONMESSAGE: 'runLambda/SOCKET_ONMESSAGE',
SOCKET_RECONNECT: 'runLambda/SOCKET_RECONNECT',
SOCKET_RECONNECT_ERROR: 'runLambda/SOCKET_RECONNECT_ERROR'
};

Vue.use(VueNativeSock, websocketEndpoint, {
store: store,
reconnection: true,
mutations: mutations
});
}
Loading