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

UI: add exec terminal #6697

Merged
merged 154 commits into from
Mar 24, 2020
Merged

UI: add exec terminal #6697

merged 154 commits into from
Mar 24, 2020

Conversation

backspace
Copy link
Contributor

This is but a prototype, but it works locally! At the moment it won’t work in Mirage and hence in the Netlify deployments, but maybe we can add a minimal toy faux backend for the socket.

Many things remain to be explored, like:

This is adapted from helpful posts in this thread:
ember-cli/ember-cli#2508

Without this, the origin header wasn’t being rewritten
so I was unable to open a websocket to the API running
on a different port.
The communication between the socket and xterm.js terminal
instance might better be extracted, I’m not sure whether
the component integration test even makes sense. I originally
tried an acceptance test, but that seemed worse.
@henrikjohansen
Copy link

Hard-coding the command as /bin/bash is problematic for many containers ... some only provide /bin/sh or similar.

@backspace
Copy link
Contributor Author

Understood, @henrikjohansen, this is in very early stages and we plan to make it configurable, thanks.

# Conflicts:
#	ui/yarn.lock
This seems quite fiddly, hopefully not Too Much™ 😳
It’s not that much of a navbar even… just not sure about
how much sharing is even worth it here 🤔
This approach… not sure about it:
• how to test the contents of the terminal without
  storing on window?
• passing the terminal in seems a bit weird but
  otherwise was considering some event interface,
  but that seems like overkill…?!?
This isn’t responsive to resize events etc yet but at least
looks better.
This seems a bit strange but…? Maybe a component would
make sense to wrap everything in but then would it be
rerendering as you navigate deeper into the hierarchy…?

Maybe there’s some breakage if you navigate between
task groups and something closes that you opened yourself
but since I don’t anticipate actually changing the route
when you click a task group (vs a task, which will), it
seems safe to ignore.
I haven’t started to include allocations in all of this yet
so this is pretty unrealistic.
This is becoming quite sprawling but I believe having good
acceptance tests will help me clean up when I’ve covered
the myriad approaches.
</div>
<div>
{{#if model.isRunning}}
<div class="two-step-button">
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I considered having a duplication of this class called something like .like-two-step-button because this isn’t an actual two-step button but using this class made it align properly with the other buttons in this section. It seemed maybe unnecessary…?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, it's fine for now. I think the entire way we handle buttons in titles needs a larger rethink.

@backspace
Copy link
Contributor Author

This is ready for review! I’ve extracted the nice-to-haves from that list above into issues and added links to them to the list.

@backspace backspace marked this pull request as ready for review March 24, 2020 15:58
@backspace backspace requested review from DingoEatingFuzz and a team March 24, 2020 15:58
@DingoEatingFuzz
Copy link
Contributor

image

I think at least a couple of these scrollbar placeholders can be removed by setting overflow-y: auto. (yes I know there's a macos setting to make these go away, but in windows and linux environments they might still be present).

@backspace
Copy link
Contributor Author

ah good catch, thank you! It is indeed a problem on Windows:

image

Copy link
Contributor

@DingoEatingFuzz DingoEatingFuzz left a comment

Choose a reason for hiding this comment

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

This looks really great! Definitely good enough for the beta.

I had a few little things scattered throughout, but the only big thing that jumped out is the mock server. I'm not entirely sure what it's doing or why it was added.

But we can chat about that later, this is good to be merged now 😄

@@ -5,6 +5,5 @@

Setting `disableAnalytics` to true will prevent any data from being sent.
*/
"disableAnalytics": false,
"proxy": "http://127.0.0.1:4646"
"disableAnalytics": false
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this to prevent the errors and socket pool saturation when nomad isn't running in the background? I have also ran into that, but I dunno if I'm ready to remove the proxy from here.

Thoughts on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

eep sorry, I meant to explain this. Just as I was starting to prototype this, I found that I was completely unable to connect to a websocket through that proxy because the Origin didn’t match and the request was rejected by the API. The Javascript socket interface doesn’t give you any control over that for security reasons, so I switched to the technique described here because it gives you fine-grained control. The crux for this feature is this section where the origin gets rewritten in the proxy outside the browser. It’s a shame to lose this simplicity but it seems unavoidable :////////

Copy link
Contributor

Choose a reason for hiding this comment

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

Wow. TIL. Definitely a shame when things don't just work, but given the amount of oddities in our use of HTTP, I'm surprised the default proxy even got us this far.

ui/app/components/exec-terminal.js Show resolved Hide resolved
return activeStateTaskNames;
},
[]
);
Copy link
Contributor

Choose a reason for hiding this comment

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

I see what you mean now about how hairy this traversal gets.

It doesn't make it any better, and in fact I think this is technically (but insignificant at this scale), but this approach with more method chaining might be more readable?

const activeStateTaskNames = this.taskGroup.allocations
  .mapBy('states')
  .reduce((allStates, states) => allStates.concat(states), [])
  .filterBy('isActive')
  .filter(taskState => taskState.task.taskGroup.name === this.taskGroup.name)
  .mapBy('name');

Copy link
Contributor Author

@backspace backspace Mar 24, 2020

Choose a reason for hiding this comment

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

yeah, I conceptually like reduce but I agree that this is Too Much. I tried replacing with your suggestion and found it was rejecting everything, I don’t understand it:

this.taskGroup.allocations
  .mapBy('states')
  .reduce((allStates, states) => allStates.concat(states), [])
  .mapBy('name')

// ["states", "states", "states", "states", "states", "states"]

this.taskGroup.allocations
  .mapBy('states')
  .reduce((allStates, states) => allStates.concat(states), [])
  .filterBy('isActive')

// [undefined, undefined, undefined, undefined, undefined, undefined]

I don’t know what’s different about it but the collected and flattened objects don’t seem like the true task states…? I would like to do something like what you suggested though, so I opened this issue to help remember, thanks.

this.terminal.writeln('');
}

this.terminal.writeln('Customize your command, then hit ‘return’ to run.');
Copy link
Contributor

Choose a reason for hiding this comment

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

Not super important but return is one of those OS specific languages at this point. Not sure what the best way tot distinguish enter vs. return or if it's even important enough to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wondered about it and decided to follow what the design had but I don’t love the lack of general term 😐

return this.store
.findRecord('job', fullId, { reload: true })
.then(job => {
return RSVP.all([job.get('allocations'), job.get('evaluations')]).then(() => job);
Copy link
Contributor

Choose a reason for hiding this comment

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

Are evaluations used for exec?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hah they indeed are not, the perils of copypaste! I’ve removed it, thank you.

function encodeString(string) {
let encoded = new TextEncoderLite('utf-8').encode(string);
return base64js.fromByteArray(encoded);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🎉 We're gonna need to use this for log and fs streaming as well. We have emoji issues over there.

@@ -0,0 +1,3 @@
export default function openExecUrl(url) {
window.open(url, '_blank', 'width=973,height=490,location=1');
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if down the road we can do something fancier here by detecting native resolution or something.

</g>
</g>
</g>
</svg>
Copy link
Contributor

Choose a reason for hiding this comment

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

Ember Inline SVG will do this at build time, but I think this should be run through svgo and saved that way. Just be mindful of preserving the viewBox.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, done, thanks!


mocks.forEach(route => route(app, options));
proxies.forEach(route => route(app, options));
};
Copy link
Contributor

Choose a reason for hiding this comment

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

You'll have to walk me through what's going on here. I'm assuming this is all in an effort to mock web sockets?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, this is boilerplate that I think just replicates what was previous set up in .ember-cli. I created #7465 so I can revisit this and maybe remove some parts. For instance, I suspect mocks are superfluous.

test('it generates an exec job URL', function(assert) {
generateExecUrl(this.router, { job: 'job-name' });

assert.ok(this.urlForSpy.calledWith('exec', 'job-name', emptyOptions));
Copy link
Contributor

Choose a reason for hiding this comment

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

Cool use of a spy.

@backspace backspace merged commit 27df92a into master Mar 24, 2020
@backspace backspace deleted the f-ui/exec branch March 24, 2020 23:22
@github-actions
Copy link

I'm going to lock this pull request because it has been closed for 120 days ⏳. This helps our maintainers find and focus on the active contributions.
If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jan 14, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants