(web) open a terminal with an URL

This commit is contained in:
Alexey Kontsevoy 2017-03-05 16:00:47 -05:00
parent e76ca9ca87
commit 8a9b23e28d
25 changed files with 429 additions and 560 deletions

View file

@ -1,78 +0,0 @@
/*
Copyright 2015 Gravitational, Inc.
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.
*/
var React = require('react');
var reactor = require('app/reactor');
var {nodeHostNameByServerId} = require('app/modules/nodes/getters');
var SessionLeftPanel = require('./sessionLeftPanel');
var cfg = require('app/config');
var session = require('app/services/session');
var Terminal = require('app/common/term/terminal');
var {updateSessionFromEventStream} = require('app/modules/currentSession/actions');
var ActiveSession = React.createClass({
render() {
let {login, parties, serverId} = this.props;
let serverLabelText = '';
if(serverId){
let hostname = reactor.evaluate(nodeHostNameByServerId(serverId));
serverLabelText = `${login}@${hostname}`;
}
return (
<div className="grv-current-session">
<SessionLeftPanel parties={parties}/>
<div className="grv-current-session-server-info">
<h3>{serverLabelText}</h3>
</div>
<TtyTerminal ref="ttyCmntInstance" {...this.props} />
</div>
);
}
});
var TtyTerminal = React.createClass({
componentDidMount() {
let { serverId, siteId, login, sid } = this.props;
let {token} = session.getUserData();
let url = cfg.api.getSiteUrl(siteId);
let options = {
tty: {
serverId, login, sid, token, url
},
el: this.refs.container
}
this.terminal = new Terminal(options);
this.terminal.ttyEvents.on('data', updateSessionFromEventStream(siteId));
this.terminal.open();
},
componentWillUnmount() {
this.terminal.destroy();
},
shouldComponentUpdate() {
return false;
},
render() {
return ( <div ref="container"> </div> );
}
});
module.exports = ActiveSession;

View file

@ -1,57 +0,0 @@
/*
Copyright 2015 Gravitational, Inc.
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 React from 'react';
import reactor from 'app/reactor';
import { getters } from 'app/modules/currentSession/';
import SessionPlayer from './sessionPlayer.jsx';
import ActiveSession from './activeSession.jsx';
import cfg from 'app/config';
import { initSession } from 'app/modules/currentSession/actions';
const CurrentSessionHost = React.createClass({
mixins: [reactor.ReactMixin],
getDataBindings() {
return {
currentSession: getters.currentSession
}
},
componentDidMount() {
setTimeout(() => initSession(this.props.params.sid), 0);
},
render() {
let { currentSession } = this.state;
if(!currentSession){
return null;
}
if(currentSession.active){
return <ActiveSession {...currentSession}/>;
}
let { sid, siteId } = currentSession;
let url = cfg.api.getFetchSessionUrl({ siteId, sid });
return <SessionPlayer url={url}/>;
}
});
export default CurrentSessionHost;

View file

@ -1,51 +0,0 @@
/*
Copyright 2015 Gravitational, Inc.
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.
*/
var React = require('react');
var {actions} = require('app/modules/currentSession/');
var {UserIcon} = require('./../icons.jsx');
var ReactCSSTransitionGroup = require('react-addons-css-transition-group');
const SessionLeftPanel = ({parties}) => {
parties = parties || [];
let userIcons = parties.map((item, index)=>(
<li key={index} className="animated"><UserIcon colorIndex={index} isDark={true} name={item.user}/></li>
));
return (
<div className="grv-terminal-participans">
<ul className="nav">
<li title="Close">
<button onClick={actions.close} className="btn btn-danger btn-circle" type="button">
<span></span>
</button>
</li>
</ul>
{ userIcons.length > 0 ? <hr className="grv-divider"/> : null }
<ReactCSSTransitionGroup className="nav" component='ul'
transitionEnterTimeout={500}
transitionLeaveTimeout={500}
transitionName={{
enter: "fadeIn",
leave: "fadeOut"
}}>
{userIcons}
</ReactCSSTransitionGroup>
</div>
)
};
module.exports = SessionLeftPanel;

View file

@ -1,15 +1,33 @@
import React from 'react';
const WHEN_TO_DISPLAY = 100; // 0.2s;
class Indicator extends React.Component {
constructor(props) {
super(props);
this._timer = null;
this.state = {
canDisplay: false
}
}
componentDidMount() {
this._timer = setTimeout(() => {
this.setState({
canDisplay: true
})
}, WHEN_TO_DISPLAY);
}
componentWillUnmount() {
clearTimeout(this._timer);
}
render() {
let { enabled = true, type } = this.props;
let { type } = this.props;
if (!enabled) {
if (!this.state.canDisplay) {
return null;
}

View file

@ -15,11 +15,10 @@ limitations under the License.
*/
import React from 'react';
import actions from 'app/modules/terminal/actions';
import {UserIcon} from './../icons.jsx';
import {UserIcon} from './icons.jsx';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
const SessionLeftPanel = ({parties}) => {
const PartyListPanel = ({parties, onClose}) => {
parties = parties || [];
let userIcons = parties.map((item, index)=>(
<li key={index} className="animated"><UserIcon colorIndex={index} isDark={true} name={item.user}/></li>
@ -29,7 +28,7 @@ const SessionLeftPanel = ({parties}) => {
<div className="grv-terminal-participans">
<ul className="nav">
<li title="Close">
<button onClick={actions.close} className="btn btn-danger btn-circle" type="button">
<button onClick={onClose} className="btn btn-danger btn-circle" type="button">
<span></span>
</button>
</li>
@ -48,4 +47,4 @@ const SessionLeftPanel = ({parties}) => {
)
};
module.exports = SessionLeftPanel;
export default PartyListPanel;

View file

@ -13,45 +13,59 @@ 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 $ from 'jQuery';
import initScroll from 'perfect-scrollbar/jquery';
import React from 'react';
import { connect } from 'nuclear-js-react-addons';
import ReactSlider from 'react-slider';
import getters from 'app/modules/player/getters';
import Terminal from 'app/common/term/terminal';
import { TtyPlayer } from 'app/common/term/ttyPlayer';
import { initPlayer, close } from 'app/modules/player/actions';
import Indicator from './../indicator.jsx';
import PartyListPanel from './../partyListPanel';
var React = require('react');
var ReactSlider = require('react-slider');
var {TtyPlayer} = require('app/common/term/ttyPlayer')
var Terminal = require('app/common/term/terminal');
var SessionLeftPanel = require('./sessionLeftPanel.jsx');
var $ = require('jQuery');
require('perfect-scrollbar/jquery')($);
initScroll($);
class Term extends Terminal{
constructor(tty, el){
super({el, scrollBack: 0});
this.tty = tty;
const PlayerHost = React.createClass({
componentDidMount() {
setTimeout(() => initPlayer(this.props.params), 0);
},
render() {
let { store } = this.props;
if(store.isReady()){
let url = store.getStoredSessionUrl();
return <Player url={url}/>;
}
connect(){
this.tty.connect();
let $indicator = null;
if(store.isLoading()){
$indicator = (<Indicator type="bounce" />);
}
open() {
super.open();
$(this._el).perfectScrollbar();
if(store.isError()){
$indicator = (<ErrorIndicator text={store.getErrorText()} />);
}
resize(cols, rows) {
if(cols === this.cols && rows === this.rows){
return;
return (
<Box>{$indicator}</Box>
);
}
super.resize(cols, rows);
$(this._el).perfectScrollbar('update');
});
function mapStateToProps() {
return {
store: getters.store
}
_disconnect(){}
_requestResize(){}
}
var SessionPlayer = React.createClass({
export default connect(mapStateToProps)(PlayerHost);
const Player = React.createClass({
calculateState(){
return {
length: this.tty.length,
@ -114,8 +128,7 @@ var SessionPlayer = React.createClass({
var {isPlaying, time} = this.state;
return (
<div className="grv-current-session grv-session-player">
<SessionLeftPanel/>
<Box>
<div ref="container"/>
<div className="grv-session-player-controls">
<button className="btn" onClick={this.togglePlayStop}>
@ -133,9 +146,53 @@ var SessionPlayer = React.createClass({
className="grv-slider" />
</div>
</div>
</div>
</Box>
);
}
});
export default SessionPlayer;
class Term extends Terminal{
constructor(tty, el){
super({el, scrollBack: 0});
this.tty = tty;
}
connect(){
this.tty.connect();
}
open() {
super.open();
$(this._el).perfectScrollbar();
}
resize(cols, rows) {
if(cols === this.cols && rows === this.rows){
return;
}
super.resize(cols, rows);
$(this._el).perfectScrollbar('update');
}
_disconnect(){}
_requestResize(){}
}
const Box = props => (
<div className="grv-terminalhost grv-session-player">
<PartyListPanel onClose={close} />
{props.children}
</div>
)
const ErrorIndicator = ({ text }) => (
<div className="grv-terminalhost-indicator-error">
<i className="fa fa-exclamation-triangle fa-3x text-warning"></i>
<div className="m-l">
<strong>Error</strong>
<div className="text-center"><small>{text}</small></div>
</div>
</div>
)

View file

@ -17,21 +17,24 @@ limitations under the License.
import React from 'react';
import reactor from 'app/reactor';
import cfg from 'app/config';
import SessionLeftPanel from './terminalHostPanel';
import PartyListPanel from './../partyListPanel';
import session from 'app/services/session';
import Terminal from 'app/common/term/terminal';
import getters from 'app/modules/terminal/getters';
import { updateSessionFromEventStream } from 'app/modules/terminal/actions';
import termGetters from 'app/modules/terminal/getters';
import sessionGetters from 'app/modules/sessions/getters';
import Indicator from './../indicator.jsx';
import { initTerminal } from 'app/modules/terminal/actions';
import { initTerminal, startNew, close, updateSessionFromEventStream } from 'app/modules/terminal/actions';
import { openPlayer } from 'app/modules/player/actions';
const TerminalHost = React.createClass({
mixins: [reactor.ReactMixin],
getDataBindings() {
let { sid } = this.props.routeParams;
return {
store: getters.store
store: termGetters.store,
parties: sessionGetters.activePartiesById(sid)
}
},
@ -39,10 +42,18 @@ const TerminalHost = React.createClass({
setTimeout(() => initTerminal(this.props.routeParams), 0);
},
startNew() {
startNew(this.props.routeParams);
},
replay() {
openPlayer(this.props.routeParams);
},
render() {
let { store } = this.state;
let { store, parties } = this.state;
let serverLabel = store.getServerLabel();
let { parties = [], status, ...props } = store.toJS();
let { status, ...props } = store.toJS();
let $content = null;
@ -59,13 +70,16 @@ const TerminalHost = React.createClass({
}
if (status.isNotFound) {
$content = (<SidNotFoundError />);
$content = (
<SidNotFoundError
onReplay={this.replay}
onNew={this.startNew} />);
}
return (
<div className="grv-current-session">
<SessionLeftPanel parties={parties}/>
<div className="grv-current-session-server-info">
<div className="grv-terminalhost">
<PartyListPanel parties={parties} onClose={close}/>
<div className="grv-terminalhost-server-info">
<h3>{serverLabel}</h3>
</div>
{$content}
@ -77,7 +91,7 @@ const TerminalHost = React.createClass({
const TtyTerminal = React.createClass({
componentDidMount() {
let { serverId, siteId, login, sid } = this.props;
let {token} = session.getUserData();
let { token } = session.getUserData();
let url = cfg.api.getSiteUrl(siteId);
let options = {
@ -115,13 +129,13 @@ const ErrorIndicator = ({ text }) => (
</div>
)
const SidNotFoundError = () => (
const SidNotFoundError = ({onNew, onReplay}) => (
<div className="grv-terminalhost-indicator-error">
<div className="text-center">
<strong>The session is no longer active</strong>
<div className="m-t">
<button className="btn btn-sm btn-primary m-r"> Start New </button>
<button className="btn btn-sm btn-primary"> Replay </button>
<button onClick={onNew} className="btn btn-sm btn-primary m-r"> Start New </button>
<button onClick={onReplay} className="btn btn-sm btn-primary"> Replay </button>
</div>
</div>
</div>

View file

@ -39,7 +39,8 @@ let cfg = {
newUser: '/web/newuser/:inviteToken',
msgs: '/web/msg/:type(/:subType)',
pageNotFound: '/web/notfound',
terminal: '/web/cluster/:siteId/node/:serverId/:login(/:sid)'
terminal: '/web/cluster/:siteId/node/:serverId/:login(/:sid)',
player: '/web/player/node/:siteId/sid/:sid'
},
api: {
@ -107,7 +108,10 @@ let cfg = {
getU2fCreateUserChallengeUrl(inviteToken){
return formatPattern(cfg.api.u2fCreateUserChallengePath, {inviteToken});
}
},
getPlayerUrl({siteId, serverId, sid}) {
return formatPattern(cfg.routes.player, { siteId, serverId, sid });
},
getTerminalLoginUrl({siteId, serverId, login, sid}) {

View file

@ -25,6 +25,7 @@ import Signup from './components/user/invite.jsx';
import Nodes from './components/nodes/main.jsx';
import Sessions from './components/sessions/main.jsx';
import TerminalHost from './components/terminal/terminalHost.jsx';
import PlayerHost from './components/player/playerHost.jsx';
import { MessagePage, NotFound } from './components/msgPage.jsx';
import { ensureUser } from './modules/user/actions';
import { initApp } from './modules/app/actions';
@ -48,8 +49,8 @@ render((
<Route path={cfg.routes.app} onEnter={initApp} >
<Route path={cfg.routes.sessions} component={Sessions}/>
<Route path={cfg.routes.nodes} component={Nodes}/>
<Route path={cfg.routes.terminal}
components={{ CurrentSessionHost: TerminalHost }} />
<Route path={cfg.routes.terminal} components={{ CurrentSessionHost: TerminalHost }} />
<Route path={cfg.routes.player} components={{ CurrentSessionHost: PlayerHost }} />
</Route>
</Route>
<Route path="*" component={NotFound} />

View file

@ -1,140 +0,0 @@
/*
Copyright 2015 Gravitational, Inc.
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 reactor from 'app/reactor';
import session from 'app/services/session';
import api from 'app/services/api';
import cfg from 'app/config';
import getters from './getters';
import { fetchStoredSession, updateSession } from './../sessions/actions';
import sessionGetters from './../sessions/getters';
import {showError} from 'app/modules/notifications/actions';
const logger = require('app/common/logger').create('Current Session');
const { TLPT_CURRENT_SESSION_OPEN, TLPT_CURRENT_SESSION_CLOSE } = require('./actionTypes');
const actions = {
createNewSession(siteId, serverId, login) {
let data = {
session: {
terminal_params: {
w: 45,
h: 5
},
login
}
}
api.post(cfg.api.getSiteSessionUrl(siteId), data).then(json => {
let history = session.getHistory();
let sid = json.session.id;
let routeUrl = cfg.getCurrentSessionRouteUrl({
siteId,
sid
});
reactor.dispatch(TLPT_CURRENT_SESSION_OPEN, {
siteId,
serverId,
login,
sid,
active: true,
isNew: true
});
history.push(routeUrl);
});
},
initSession(sid) {
logger.info('attempt to open session', { sid });
// check if terminal is already open
let currentSession = reactor.evaluate(getters.currentSession);
if (currentSession) {
return;
}
// look up active session matching given sid
let activeSession = reactor.evaluate(sessionGetters.activeSessionById(sid));
if (activeSession) {
let { server_id, login, siteId, id } = activeSession;
reactor.dispatch(TLPT_CURRENT_SESSION_OPEN, {
login,
siteId,
sid: id,
serverId: server_id,
active: true
});
return;
}
// stored session then...
fetchStoredSession(sid)
.done(() => {
let storedSession = reactor.evaluate(sessionGetters.storedSessionById(sid));
if (!storedSession) {
// TODO: display not found page
showError('Cannot find archived session');
return;
}
let { siteId } = storedSession;
reactor.dispatch(TLPT_CURRENT_SESSION_OPEN, {
siteId,
sid
});
})
.fail(err => {
let msg = err.responseJSON ? err.responseJSON.message : '';
showError('Unable to fetch archived session', msg);
logger.error('open session', err);
})
},
close() {
let { isNew } = reactor.evaluate(getters.currentSession);
reactor.dispatch(TLPT_CURRENT_SESSION_CLOSE);
if (isNew) {
session.getHistory().push(cfg.routes.nodes);
} else {
session.getHistory().push(cfg.routes.sessions);
}
},
updateSessionFromEventStream(siteId) {
return data => {
data.events.forEach(item => {
if (item.event === 'session.end') {
actions.close();
}
})
updateSession({
siteId: siteId,
json: data.session
});
}
}
}
export default actions;

View file

@ -1,44 +0,0 @@
/*
Copyright 2015 Gravitational, Inc.
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.
*/
var { Store, toImmutable } = require('nuclear-js');
var { TLPT_CURRENT_SESSION_OPEN, TLPT_CURRENT_SESSION_CLOSE } = require('./actionTypes');
export default Store({
getInitialState() {
return toImmutable(null);
},
initialize() {
this.on(TLPT_CURRENT_SESSION_OPEN, setCurrentSession);
this.on(TLPT_CURRENT_SESSION_CLOSE, close);
}
})
function close(){
return toImmutable(null);
}
function setCurrentSession(state, {siteId, serverId, login, sid, active, isNew} ){
return toImmutable({
siteId,
serverId,
login,
sid,
active,
isNew
});
}

View file

@ -1,37 +0,0 @@
/*
Copyright 2015 Gravitational, Inc.
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.
*/
const currentSession = [ ['tlpt_current_session'], ['tlpt_sessions_active'],
(current, sessions) => {
if(!current){
return null;
}
let curSession = current.toJS();
// get the list of participants
if(sessions.has(curSession.sid)){
let activeSessionRec = sessions.get(curSession.sid);
curSession.parties = activeSessionRec.parties.toJS();
}
return curSession;
}
];
export default {
currentSession
}

View file

@ -16,12 +16,12 @@ limitations under the License.
import reactor from 'app/reactor';
import termStore from './terminal/store';
import playerStore from './player/store';
reactor.registerStores({
'tlpt': require('./app/appStore'),
'tlpt_current_session': require('./currentSession/currentSessionStore'),
'tlpt_terminal': termStore,
'tlpt_player': playerStore,
'tlpt_user': require('./user/userStore'),
'tlpt_sites': require('./sites/siteStore'),
'tlpt_user_invite': require('./user/userInviteStore'),

View file

@ -16,6 +16,7 @@ limitations under the License.
import keyMirror from 'keymirror'
export default keyMirror({
TLPT_CURRENT_SESSION_OPEN: null,
TLPT_CURRENT_SESSION_CLOSE: null
TLPT_PLAYER_INIT: null,
TLPT_PLAYER_CLOSE: null,
TLPT_PLAYER_SET_STATUS: null
})

View file

@ -0,0 +1,75 @@
/*
Copyright 2015 Gravitational, Inc.
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 reactor from 'app/reactor';
import session from 'app/services/session';
import api from 'app/services/api';
import cfg from 'app/config';
import { fetchStoredSession } from './../sessions/actions';
import sessionGetters from './../sessions/getters';
const logger = require('app/common/logger').create('app/flux/player');
import {
TLPT_PLAYER_INIT,
TLPT_PLAYER_CLOSE,
TLPT_PLAYER_SET_STATUS
} from './actionTypes';
const actions = {
openPlayer(routeParams) {
let routeUrl = cfg.getPlayerUrl(routeParams);
session.getHistory().push(routeUrl);
},
initPlayer(routeParams) {
logger.info('initPlayer()', routeParams);
let {sid, siteId } = routeParams;
reactor.dispatch(TLPT_PLAYER_SET_STATUS, { isLoading: true });
fetchStoredSession(sid, siteId)
.done(() => {
let storedSession = reactor.evaluate(sessionGetters.storedSessionById(sid));
if (!storedSession) {
reactor.dispatch(TLPT_PLAYER_SET_STATUS, {
isError: true,
errorText: 'Cannot find archived session'
});
} else {
let { siteId } = storedSession;
reactor.dispatch(TLPT_PLAYER_INIT, {
siteId,
sid
});
}
})
.fail(err => {
logger.error('open session', err);
let errorText = api.getErrorText(err);
reactor.dispatch(TLPT_PLAYER_SET_STATUS, {
isError: true,
errorText,
});
})
},
close() {
reactor.dispatch(TLPT_PLAYER_CLOSE);
session.getHistory().push(cfg.routes.sessions);
}
}
export default actions;

View file

@ -13,6 +13,7 @@ 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.
*/
module.exports.getters = require('./getters');
module.exports.actions = require('./actions');
module.exports.activeTermStore = require('./currentSessionStore');
export default {
store: ['tlpt_player']
}

View file

@ -0,0 +1,88 @@
/*
Copyright 2015 Gravitational, Inc.
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 { Store } from 'nuclear-js';
import { Record } from 'immutable';
import cfg from 'app/config';
import {
TLPT_PLAYER_INIT,
TLPT_PLAYER_CLOSE,
TLPT_PLAYER_SET_STATUS
} from './actionTypes';
const PlayerStatusRec = new Record({
isReady: false,
isLoading: false,
isNotFound: false,
isError: false,
errorText: undefined,
})
export class PlayerRec extends Record({
status: new PlayerStatusRec(),
siteId: undefined,
sid: undefined
}) {
makeReady() {
return this.set('status', new PlayerStatusRec({ isReady: true }));
}
isReady() {
return this.getIn(['status', 'isReady']);
}
isLoading() {
return this.getIn(['status', 'isLoading']);
}
isError() {
return this.getIn(['status', 'isError']);
}
getErrorText() {
return this.getIn(['status', 'errorText']);
}
getStoredSessionUrl() {
return cfg.api.getFetchSessionUrl({
siteId: this.siteId,
sid: this.sid
});
}
}
export default Store({
getInitialState() {
return new PlayerRec();
},
initialize() {
this.on(TLPT_PLAYER_INIT, init);
this.on(TLPT_PLAYER_CLOSE, close);
this.on(TLPT_PLAYER_SET_STATUS, changeStatus);
}
})
function close(){
return new PlayerRec();
}
function init(state, json){
return new PlayerRec(json).makeReady();
}
function changeStatus(state, status) {
return state.setIn(['status'], new PlayerStatusRec(status));
}

View file

@ -30,8 +30,8 @@ const {
const actions = {
fetchStoredSession(sid) {
let siteId = reactor.evaluate(appGetters.siteId);
fetchStoredSession(sid, siteId) {
siteId = siteId || reactor.evaluate(appGetters.siteId);
return api.get(cfg.api.getSessionEventsUrl({ siteId, sid })).then(json=>{
if(json && json.events){
reactor.dispatch(TLPT_SESSIONS_EVENTS_RECEIVE, { siteId, json: json.events });

View file

@ -33,6 +33,16 @@ const storedSessionList = [['tlpt_sessions_archived'], ['tlpt', 'siteId'], (sess
const activeSessionById = sid => ['tlpt_sessions_active', sid];
const storedSessionById = sid => ['tlpt_sessions_archived', sid];
const activePartiesById = sid => [['tlpt_sessions_active', sid, 'parties'], parties => {
if (parties) {
return parties.toJS();
}
return [];
}];
const nodeIpById = sid => ['tlpt_sessions_events', sid, EventTypeEnum.START, 'addr.local'];
function createStoredListItem(session){
@ -42,7 +52,7 @@ function createStoredListItem(session){
let nodeDisplayText = getNodeIpDisplayText(server_id, nodeIp);
let createdDisplayText = getCreatedDisplayText(created);
let sessionUrl = cfg.getCurrentSessionRouteUrl({
let sessionUrl = cfg.getPlayerUrl({
sid,
siteId
});
@ -65,15 +75,17 @@ function createActiveListItem(session){
let sid = session.get('id');
let parties = createParties(session.parties);
let { siteId, created, last_active, server_id } = session;
let { siteId, created, login, last_active, server_id } = session;
let duration = moment(last_active).diff(created);
let nodeIp = reactor.evaluate(nodeIpById(sid));
let nodeDisplayText = getNodeIpDisplayText(server_id, nodeIp);
let createdDisplayText = getCreatedDisplayText(created);
let sessionUrl = cfg.getCurrentSessionRouteUrl({
let sessionUrl = cfg.getTerminalLoginUrl({
sid,
siteId
siteId,
login,
serverId: server_id
});
return {
@ -121,6 +133,7 @@ export default {
storedSessionList,
activeSessionList,
activeSessionById,
activePartiesById,
storedSessionById,
createStoredListItem
}

View file

@ -26,8 +26,23 @@ const logger = require('app/common/logger').create('Current Session');
const { TLPT_TERMINAL_OPEN, TLPT_TERMINAL_CLOSE, TLPT_TERMINAL_SET_STATUS } = require('./actionTypes');
const changeBrowserUrl = newRouteParams => {
let routeUrl = cfg.getTerminalLoginUrl(newRouteParams);
session.getHistory().push(routeUrl);
}
const actions = {
startNew(routeParams) {
let newRouteParams = {
...routeParams,
sid: undefined
}
changeBrowserUrl(newRouteParams);
actions.initTerminal(newRouteParams);
},
createNewSession(routeParams) {
let { login, siteId } = routeParams;
let data = {
@ -43,15 +58,14 @@ const actions = {
return api.post(cfg.api.getSiteSessionUrl(siteId), data)
.then(json => {
let sid = json.session.id;
let newRoutParams = {
let newRouteParams = {
...routeParams,
sid
};
let routeUrl = cfg.getTerminalLoginUrl(newRoutParams);
reactor.dispatch(TLPT_TERMINAL_OPEN, newRoutParams);
reactor.dispatch(TLPT_TERMINAL_OPEN, newRouteParams);
reactor.dispatch(TLPT_TERMINAL_SET_STATUS, { isReady: true });
session.getHistory().push(routeUrl);
changeBrowserUrl(newRouteParams);
})
.fail(err => {
let errorText = api.getErrorText(err);

View file

@ -36,12 +36,19 @@ export class TermRec extends Record({
status: TermStatusRec(),
login: undefined,
siteId: undefined,
serverId: undefined
serverId: undefined,
sid: undefined
}) {
getServerLabel() {
let hostname = reactor.evaluate(nodeHostNameByServerId(this.serverId));
return `${this.login} @${hostname}`;
if (hostname && this.login) {
return `${this.login}@${hostname}`;
}
return ''
}
}
export default Store({

View file

@ -1,59 +0,0 @@
/*
Copyright 2015 Gravitational, Inc.
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.
*/
.grv-current-session{
padding: 40px 25px 10px 90px;
position: fixed;
height: 100%;
width: 100%;
display: block;
background-color: $grv-color-bg-term;
z-index: 1;
}
.grv-terminal-participans{
margin-left: -70px;
width: 50px;
position: absolute;
.grv-divider{
background-color: #3B3838;
border-color: #3B3838;
}
.nav {
text-align: center;
> li{
margin: 15px 0;
}
}
}
.grv-current-session-server-info{
height: 30px;
margin-top: -35px;
text-align: center;
overflow: hidden;
span{
margin-left: 5px
}
.btn{
padding: 0 10px;
margin-bottom: 5px;
}
}

View file

@ -61,7 +61,7 @@ limitations under the License.
position: relative;
.grv-sessions-stored-details{
max-width: 300px;
max-width: 320px;
}
}

View file

@ -6,3 +6,47 @@
height: 100%;
}
.grv-terminalhost{
padding: 40px 25px 10px 90px;
position: fixed;
height: 100%;
width: 100%;
display: block;
background-color: $grv-color-bg-term;
z-index: 1;
}
.grv-terminal-participans{
margin-left: -70px;
width: 50px;
position: absolute;
.grv-divider{
background-color: #3B3838;
border-color: #3B3838;
}
.nav {
text-align: center;
> li{
margin: 15px 0;
}
}
}
.grv-terminalhost-server-info{
height: 30px;
margin-top: -35px;
text-align: center;
overflow: hidden;
span{
margin-left: 5px
}
.btn{
padding: 0 10px;
margin-bottom: 5px;
}
}

View file

@ -30,7 +30,6 @@ limitations under the License.
@import "grv-nodes";
@import "grv-sessions";
@import "grv-nav";
@import "grv-current-session";
@import "grv-terminalhost";
@import "grv-msg-page";
@import "grv-slider";