diff --git a/package-lock.json b/package-lock.json
index 4fffdb46cd..b93ae45534 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14149,28 +14149,19 @@
}
},
"reactstrap": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-5.0.0.tgz",
- "integrity": "sha512-y0eju/LAK7gbEaTFfq2iW92MF7/5Qh0tc1LgYr2mg92IX8NodGc03a+I+cp7bJ0VXHAiLy0bFL9UP89oSm4cBg==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-8.0.0.tgz",
+ "integrity": "sha512-Rd7MyaFnZZXe1lbeBlgkxzyv+u7Z/ots0yYoO6ijT7/0uGsNwB1ch2h2t7Tf6u05jpbTFlLD+lyBEER0eibcQw==",
"requires": {
+ "@babel/runtime": "^7.2.0",
"classnames": "^2.2.3",
"lodash.isfunction": "^3.0.9",
"lodash.isobject": "^3.0.2",
"lodash.tonumber": "^4.0.3",
"prop-types": "^15.5.8",
- "react-popper": "^0.8.3",
- "react-transition-group": "^2.2.1"
- },
- "dependencies": {
- "react-popper": {
- "version": "0.8.3",
- "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-0.8.3.tgz",
- "integrity": "sha1-D3MzMTfJ+wr27EB00tBYWgoEYeE=",
- "requires": {
- "popper.js": "^1.12.9",
- "prop-types": "^15.6.0"
- }
- }
+ "react-lifecycles-compat": "^3.0.4",
+ "react-popper": "^1.3.3",
+ "react-transition-group": "^2.3.1"
}
},
"read-pkg": {
diff --git a/package.json b/package.json
index 2dcf48050e..102e9fb524 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
"react-motion": "^0.5.2",
"react-redux": "^5.0.6",
"react-router-dom": "^4.2.2",
- "reactstrap": "^5.0.0-alpha.4",
+ "reactstrap": "^8.0.0",
"redux": "^3.7.2",
"redux-thunk": "^2.2.0",
"styled-jsx": "^2.2.6",
diff --git a/src/api-client/notebook-servers.js b/src/api-client/notebook-servers.js
index 5385fafc43..5377159c4f 100644
--- a/src/api-client/notebook-servers.js
+++ b/src/api-client/notebook-servers.js
@@ -43,37 +43,67 @@ function cleanAnnotations(annotations, domain) {
}
function addNotebookServersMethods(client) {
- client.getNotebookServers = (id) => {
- let headers = client.getBasicHeaders();
+ client.getNotebookServers = (id, branch, commit) => {
+ // TODO: add filtering logic here, remove from Notebook
+ const headers = client.getBasicHeaders();
const url = `${client.baseUrl}/notebooks/servers`;
return client.clientFetch(url, {
method: 'GET',
- headers: headers
+ headers
}).then(resp => {
- const { servers } = resp.data;
+ let { servers } = resp.data;
if (id) {
// TODO: remove this filter when this API will support projectId filtering
- const filteredServers = Object.keys(servers)
+ servers = Object.keys(servers)
.filter(server => servers[server].annotations["renku.io/projectId"] === id)
.reduce((obj, key) => {obj[key] = servers[key]; return obj}, {});
- return { "data": filteredServers };
}
return { "data": servers };
});
}
client.stopNotebookServer = (serverName) => {
- let headers = client.getBasicHeaders();
+ const headers = client.getBasicHeaders();
const url = `${client.baseUrl}/notebooks/servers/${serverName}`;
return client.clientFetch(url, {
method: 'DELETE',
- headers: headers
+ headers
}, "text")
.then(resp => {
return true;
});
}
+
+ client.getNotebookServerOptions = (projectUrl, commitId) => {
+ const headers = client.getBasicHeaders();
+ const url = `${client.baseUrl}/notebooks/${projectUrl}/${commitId}/server_options`;
+
+ return client.clientFetch(url, {
+ method: 'GET',
+ headers
+ }).then((resp) => {
+ let { data } = resp;
+ Object.keys(data).forEach(key => {
+ data[key].selected = data[key].default;
+ })
+ return data;
+ });
+ }
+
+ client.startNotebook = (projectUrl, branchName, commitId, options) => {
+ const headers = client.getBasicHeaders();
+ const url = `${client.baseUrl}/notebooks/${projectUrl}/${commitId}`;
+
+ return client.clientFetch(url, {
+ method: 'POST',
+ headers,
+ queryParams: { branch: branchName },
+ body: JSON.stringify(options)
+ }).then((resp) => {
+ return resp.data;
+ });
+ }
}
export { cleanAnnotations, ExpectedAnnotations };
diff --git a/src/model/Model.js b/src/model/Model.js
index b39cb09ca4..a886578337 100644
--- a/src/model/Model.js
+++ b/src/model/Model.js
@@ -65,6 +65,12 @@ const SpecialPropVal = {
UPDATING: 'is_updating'
};
+const StatusHelper = {
+ isUpdating: (value) => {
+ return value === SpecialPropVal.UPDATING ? true : false;
+ }
+}
+
class FieldSpec {
constructor(spec) {
Object.keys(spec).forEach((prop) => {
@@ -271,6 +277,11 @@ class SubModel {
this.baseModel.setUpdating(fullOptions);
}
+ isUpdating(propAccessorString) {
+ const fullPropAccessorString = this.baseModelPath + (propAccessorString ? '.' + propAccessorString : '');
+ return StatusHelper.isUpdating(this.baseModel.get(fullPropAccessorString));
+ }
+
mapStateToProps = _mapStateToProps.bind(this);
subModel = (path) => new SubModel(this.baseModel, this.baseModelPath + '.' + path);
@@ -460,4 +471,4 @@ function _mapStateToProps(state, ownProps) {
}
-export { Schema, StateModel, StateKind , SubModel, SpecialPropVal}
+export { Schema, StateModel, StateKind , SubModel, SpecialPropVal, StatusHelper }
diff --git a/src/notebooks/Notebooks.container.js b/src/notebooks/Notebooks.container.js
index f32eda2822..62cd94c031 100644
--- a/src/notebooks/Notebooks.container.js
+++ b/src/notebooks/Notebooks.container.js
@@ -18,163 +18,279 @@
import React, { Component } from 'react';
import { connect } from 'react-redux'
-import { Col } from 'reactstrap';
-import { NotebookServerOptions, NotebookServers, LogOutUser } from './Notebooks.present';
+import { NotebookServers } from './Notebooks.present';
+import { StartNotebookServer as StartNotebookServerPresent } from './Notebooks.present';
import NotebooksModel from './Notebooks.state';
import { Notebooks as NotebooksPresent } from './Notebooks.present';
-
-class LaunchNotebookServer extends Component {
+import { StatusHelper } from '../model/Model'
+
+/**
+ * Displays a start page for new Jupiterlab servers.
+ *
+ * @param {Object[]} branches Branches as redurnet by gitlab "/branches" API
+ * @param {function} refreshBranches Function to invoke to refresh the list of branches
+ * @param {number} projectId id of the reference project
+ * @param {number} projectPath path of the reference project
+ * @param {Object[]} client api-client used to query the gateway
+ * @param {string} successUrl Optional: url to redirect when then notebook is succesfully started
+ * @param {Object} history Optional: used with successUrl to properly set the new url without reloading the page
+ */
+class StartNotebookServer extends Component {
constructor(props) {
- super(props)
- this.state = {
- serverOptions: {},
- serverRunning: false,
- serverStarting: false,
- doLogOut: false
+ super(props);
+ this.model = new NotebooksModel(props.client);
+
+ this.handlers = {
+ refreshBranches: this.refreshBranches.bind(this),
+ refreshCommits: this.refreshCommits.bind(this),
+ setBranch: this.setBranchFromName.bind(this),
+ setCommit: this.setCommitFromId.bind(this),
+ toggleMergedBranches: this.toggleMergedBranches.bind(this),
+ setDisplayedCommits: this.setDisplayedCommits.bind(this),
+ setServerOption: this.setServerOptionFromEvent.bind(this),
+ startServer: this.startServer.bind(this)
+ }
+
+ this.state = {
+ first: true,
+ startring: false
};
- this._unmounting = false;
+ }
+
+ componentDidMount() {
+ this._isMounted = true;
+ this.startNotebookPolling();
+ this.refreshBranches();
}
componentWillUnmount() {
- this._unmounting = true;
+ this.stopNotebookPolling();
+ this._isMounted = false;
}
- componentDidMount() {
- if (this.state.doLogOut === false)
- this.componentDidUpdate()
+ componentDidUpdate(previousProps) {
+ // TODO: this is a temporary fix, remove it once the component won't be
+ // rerendered multiple times at the first url load
+ if (this.state.first &&
+ StatusHelper.isUpdating(previousProps.branches) &&
+ !StatusHelper.isUpdating(this.props.branches)) {
+ this.autoselectBranch(this.props.branches);
+ this.setState({ first: false });
+ }
}
- componentDidUpdate() {
- if (this.state.doLogOut === true) return; // We are about to refresh anyway...
+ refreshBranches() {
+ const { branches } = this.props;
+ if (StatusHelper.isUpdating(branches)) return;
+ this.props.refreshBranches().then((branches) => {
+ this.autoselectBranch(branches);
+ });
+ }
- if (this.props.core.notebookServerAPI !== this.previousNotebookServerAPI) {
- this.serverStatusSet = false;
- this.serverOptionsSet = false;
+ setBranch(branch) {
+ const oldBranch = this.model.get("filters.branch");
+ if (!branch.name || branch.name === oldBranch.name)
+ return;
+ this.model.setBranch(branch);
+ this.refreshCommits(branch);
+ }
+
+ setBranchFromName(eventOrName) {
+ const { branches } = this.props;
+ const branchName = eventOrName.target ?
+ eventOrName.target.value :
+ eventOrName;
+ for (let branchCurrent of branches) {
+ if (branchName === branchCurrent.name) {
+ this.setBranch(branchCurrent);
+ }
}
- this.previousNotebookServerAPI = this.props.core.notebookServerAPI;
- this.setServerStatus();
- this.setServerOptions();
- }
-
-
- setServerStatus() {
- if (this.serverStatusSet) return;
- if (!this.props.core.notebookServerAPI) return;
- if (!this.props.client) return;
- // Check for already running servers
- const headers = this.props.client.getBasicHeaders();
- this.props.client.clientFetch(this.props.core.notebookServerAPI, {headers})
- .then(response => {
- const serverStatus = !(!response.data.pending && !response.data.ready);
- if (!this._unmounting) {
- this.setState({serverRunning: serverStatus});
- this.serverStatusSet = true;
- }
- }).catch(e => {
- if (e.case === 'UNAUTHORIZED') {
- this.setState({ doLogOut: true });
- }
- });
}
- setServerOptions() {
- if (this.serverOptionsSet) return;
- if (!this.props.core.notebookServerAPI) return;
- if (!this.props.client) return;
- // Load options and save them to state,
- // set intial selection values to defaults.
- const headers = this.props.client.getBasicHeaders();
-
- // TODO: Move this code to a method getServerOptions in api client library.
- this.props.client.clientFetch(`${this.props.core.notebookServerAPI}/server_options`, {
- cache: 'no-store',
- headers
- })
- .then(response => {
- const data = response.data;
- Object.keys(data).forEach(key => {
- data[key].selected = data[key].default;
- })
- if (!this._unmounting) {
- this.setState({serverOptions: data});
- this.serverOptionsSet = true;
- }
- })
- .catch(e => {
- if (e.case === 'UNAUTHORIZED'){
- this.setState({ doLogOut: true });
- }
- });
- }
+ validateBranch(updatedBranches, updatedBranch) {
+ const branch = updatedBranch ?
+ updatedBranch :
+ this.model.get("filters.branch");
+ if (branch.name === undefined)
+ return true;
+
+ const branches = updatedBranches ?
+ updatedBranches :
+ this.props.branches;
+ const filterBranches = !this.model.get("filters.includeMergedBranches");
+ const filteredBranches = filterBranches ?
+ branches.filter(branch => !branch.merged ? branch : null ) :
+ branches;
+ for (let branchCurrent of filteredBranches) {
+ if (branch.name === branchCurrent.name) {
+ return true;
+ }
+ }
- getChangeHandlers() {
- const handlers = {};
- // Add all resource change handlers.
- Object.keys(this.state.serverOptions).forEach(key => {
- handlers[key] = (e) => {
- const newValue = this.state.serverOptions[key].type === 'boolean' ?
- e.target.checked : e.target.value;
+ this.setBranch({});
+ this.setCommit({});
+ return false;
+ }
- this.setState((prevState) => {
- const newState = {...prevState};
- newState.serverOptions[key].selected = newValue;
- return newState;
- })
+ autoselectBranch(branches, defaultBranch = "master") {
+ if (this._isMounted) {
+ const branch = this.model.get("filters.branch");
+ if (!branch.name) {
+ const autoSelect = branches.filter(branch => branch.name === defaultBranch);
+ if (autoSelect.length !== 1) return; // improve this logic if necessary when defaultBranch will be dynamic
+ this.setBranch(autoSelect[0]);
}
+ else {
+ this.validateBranch(branches);
+ }
+ }
+ }
+
+ refreshCommits(updatedBranch) {
+ const commits = this.model.get("data.commits");
+ if (StatusHelper.isUpdating(commits)) return;
+ const { projectId } = this.props;
+ const branch = updatedBranch && typeof updatedBranch === "string" ?
+ updatedBranch :
+ this.model.get("filters.branch");
+ this.model.fetchCommits(projectId, branch.name).then((commits) => {
+ this.autoselectCommit(commits);
});
- return handlers;
}
+ setCommit(commit) {
+ const oldCommit = this.model.get("filters.commit");
+ if (commit.id === oldCommit.id) {
+ return;
+ }
+ this.model.setCommit(commit);
+ this.model.verifyIfRunning(this.props.projectId, this.props.projectPath);
+ }
- onSubmit(event) {
- event.preventDefault();
+ setCommitFromId(eventOrId) {
+ const commits = this.model.get("data.commits");
+ const commitId = eventOrId.target ?
+ eventOrId.target.value :
+ eventOrId;
+ for (let commitCurrent of commits) {
+ if (commitId === commitCurrent.id) {
+ this.setCommit(commitCurrent);
+ }
+ }
+ }
- const postData = {
- serverOptions: {}
- };
- Object.keys(this.state.serverOptions).forEach(key => {
- postData.serverOptions[key] = this.state.serverOptions[key].selected
- });
+ validateCommit(updatedCommits, updatedCommit) {
+ const commit = updatedCommit ?
+ updatedCommit :
+ this.model.get("filters.commit");
+ if (commit.id === undefined) {
+ return true;
+ }
- this.setState({serverStarting: true});
- const headers = this.props.client.getBasicHeaders();
- headers.set('Content-Type', 'application/json');
- this.props.client.clientFetch(this.props.core.notebookServerAPI, {
- method: 'POST',
- headers: headers,
- body: JSON.stringify(postData)
- })
- .then(() => {
- this.props.onSuccess()
- })
+ const commits = updatedCommits ?
+ updatedCommits :
+ this.model.get("data.commits");
+
+ const maxCommits = this.model.get("filters.displayedCommits");
+ const filteredCommits = maxCommits && maxCommits > 0 ?
+ commits.slice(0, maxCommits) :
+ commits;
+ for (let commitCurrent of filteredCommits) {
+ if (commit.id === commitCurrent.id) {
+ // necessary for istant refresh in the UI
+ this.model.verifyIfRunning(this.props.projectId, this.props.projectPath);
+ return true;
+ }
+ }
- // Note that the opening of the new tab must happen
- // on click and can not be delayed (pop-up blocking)
- // window.open(this.props.core.notebookServerUrl);
+ this.setCommit({});
+ return false;
+ }
+
+ autoselectCommit(commits, defaultCommit = 0) {
+ if (this._isMounted) {
+ if (this._isMounted) {
+ const commit = this.model.get("filters.commit");
+ if (!commit.id) {
+ const autoSelect = commits[defaultCommit];
+ if (!autoSelect) return; // improve this logic if "latest" won't be the default anymore
+ this.setCommit(autoSelect);
+ }
+ else {
+ this.validateCommit(commits);
+ }
+ }
+ }
}
+ setServerOptionFromEvent(option, event) {
+ const value = event.target.checked !== undefined ?
+ event.target.checked:
+ event.target.value;
+ this.model.setNotebookOptions(option, value);
+ }
- render() {
- if (!this.props.client) return null;
-
- if (this.state.doLogOut) {
- return
- } else if (this.state.serverRunning) {
- return
- You already have a server running.
-
+ startServer() {
+ // Data from notebooks/servers endpoint needs some time to update propery.
+ // To avoid flickering UI, just set a temporary state and display a loading wheel.
+ // TODO: change this when the notebook service will be updated.
+ const { successUrl } = this.props;
+ if (!successUrl) {
+ this.model.startServer(this.props.projectPath);
}
else {
- return
+ this.setState({ "starting": true });
+ this.model.startServer(this.props.projectPath).then((data) => {
+ this.props.history.push(successUrl);
+ });
+ }
+ }
+
+ toggleMergedBranches(event) {
+ const currentSetting = this.model.get("filters.includeMergedBranches");
+ this.model.setMergedBranches(!currentSetting);
+ this.validateBranch();
+ }
+
+ setDisplayedCommits(event) {
+ this.model.setDisplayedCommits(event.target.value);
+ this.validateCommit();
+ }
+
+ startNotebookPolling() {
+ this.model.startNotebookPolling(this.props.projectId, this.props.projectPath);
+ }
+
+ stopNotebookPolling() {
+ this.model.stopNotebookPolling();
+ }
+
+ mapStateToProps(state, ownProps) {
+ const augmentedState = { ...state,
+ data: {...state.data,
+ branches: ownProps.inherited.branches
+ } // add "branches" to data
+ };
+ return {
+ handlers: this.handlers,
+ store: ownProps.store, // adds store and other props manually added to
+ ...augmentedState
}
}
+
+ render() {
+ const ConnectedStartNotebookServer = connect(this.mapStateToProps.bind(this))(StartNotebookServerPresent);
+
+ return ;
+ }
}
class Notebooks extends Component {
@@ -223,4 +339,4 @@ class Notebooks extends Component {
}
-export { LaunchNotebookServer, NotebookServers, Notebooks };
+export { NotebookServers, Notebooks, StartNotebookServer };
diff --git a/src/notebooks/Notebooks.present.js b/src/notebooks/Notebooks.present.js
index 2375a10037..1708de194d 100644
--- a/src/notebooks/Notebooks.present.js
+++ b/src/notebooks/Notebooks.present.js
@@ -20,17 +20,19 @@ import React, { Component } from 'react';
import Media from 'react-media';
import { Link } from 'react-router-dom';
-import { Form, FormGroup, Label, Input, Button, Row, Col, Table} from 'reactstrap';
+import { Form, FormGroup, FormText, Label, Input, Button, Row, Col, Table} from 'reactstrap';
import { UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem} from 'reactstrap';
+import { UncontrolledTooltip, UncontrolledPopover, PopoverHeader, PopoverBody } from 'reactstrap';
+// temporary issue with UncontrolledTooltip --> https://github.com/reactstrap/reactstrap/issues/1255
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
-import { faStopCircle, faExternalLinkAlt, faInfoCircle } from '@fortawesome/fontawesome-free-solid';
+import { faStopCircle, faExternalLinkAlt, faInfoCircle, faSyncAlt, faCogs } from '@fortawesome/fontawesome-free-solid';
-import { SpecialPropVal } from '../model/Model';
-import { Loader, InfoAlert } from '../utils/UIComponents';
+import { SpecialPropVal, StatusHelper } from '../model/Model';
+import { Loader, InfoAlert, ExternalLink } from '../utils/UIComponents';
+import Time from '../utils/Time';
import Sizes from '../utils/Media';
import { cleanAnnotations } from '../api-client/notebook-servers';
-
const Columns = {
large: {
default: ["Project", "Commit", "Action"],
@@ -42,148 +44,6 @@ const Columns = {
}
};
-class LogOutUser extends Component {
-
- constructor(props) {
- super(props);
- this.state = {
- timer: null,
- logedout: false
- }
- }
-
- componentWillUnmount() {
- this.state.timer.clear()
- }
-
- componentDidMount() {
- this.setState({
- timer: setTimeout(() => {
- this.setState({ logedout: true });
- this.props.client.doLogout();
- }, 6000)
- });
- }
-
- render() {
- return (
- this.state.logedout ?
- We logged you out.
- :
-
- You will be logged out because your JupyterLab token expired.
-
Please log in again to continue working with Renku.
-
-
-
- )
- }
-}
-
-class RenderedServerOptions extends Component {
- render() {
- if (this.props.loader) {
- return
- }
- const renderedServerOptions = Object.keys(this.props.serverOptions).map(key => {
- const serverOption = this.props.serverOptions[key];
- const onChange = this.props.changeHandlers[key];
-
- switch (serverOption.type) {
- case 'enum':
- return
-
-
- ;
-
- case 'int':
- return
-
-
- ;
-
- case 'float':
- return
-
-
- ;
-
- case 'boolean':
- return
-
-
- ;
-
- default:
- return null;
- }
- });
- return
- }
-}
-
-class NotebookServerOptions extends Component {
- render() {
- return (
-
-
- Launch new JupyterLab server
-
-
-
-
-
-
- );
- }
-}
-
-class EnumOption extends Component {
- render() {
- return (
-
- {this.props.options.map((optionName, i) => {
- return
- })}
-
- );
- }
-}
-
-class BooleanOption extends Component {
- render() {
- return (
-
- );
- }
-}
-
-class RangeOption extends Component {
- render() {
- return (
-
- );
- }
-}
-
class NotebookServerRowAction extends Component {
render() {
const {status, name} = this.props;
@@ -477,4 +337,375 @@ class Notebooks extends Component {
}
}
-export { NotebookServerOptions, NotebookServers, LogOutUser, Notebooks }
+class StartNotebookServer extends Component {
+ render() {
+ return (
+
+
+ Start new Jupyterlab server
+
+
+
+ )
+ }
+}
+
+class StartNotebookBranches extends Component {
+ render() {
+ const { branches } = this.props.data;
+ let content;
+ if (StatusHelper.isUpdating(branches) || branches.length === 0) {
+ content = (
+
+ )
+ }
+ else {
+ if (branches.length === 1) {
+ content = (
+
+
+
+
+
+ )
+ }
+ else {
+ const filter = !this.props.filters.includeMergedBranches;
+ const filteredBranches = filter ?
+ branches.filter(branch => !branch.merged ? branch : null ) :
+ branches;
+ let branchOptions = filteredBranches.map((branch, index) => {
+ return
+ });
+ content = (
+
+
+
+
+ {branchOptions}
+
+
+ )
+ }
+ }
+ return (
+
+ {content}
+
+ )
+ }
+}
+
+class StartNotebookBranchesUpdate extends Component {
+ render() {
+ return [
+ ,
+
+ Refresh branches
+
+ ]
+ }
+}
+
+class StartNotebookBranchesOptions extends Component {
+ render() {
+ return [
+ ,
+
+ Branch options
+ ,
+
+ Branch options
+
+
+
+
+
+
+ ]
+ }
+}
+
+class StartNotebookCommits extends Component {
+ render() {
+ const { branch } = this.props.filters;
+ const { branches, commits } = this.props.data;
+ if (!branch.name || StatusHelper.isUpdating(branches)) {
+ return null;
+ }
+ let content;
+ if (StatusHelper.isUpdating(commits)) {
+ content = (
+
+ )
+ }
+ else {
+ const maxCommits = this.props.filters.displayedCommits;
+ const filteredCommits = maxCommits && maxCommits > 0 ?
+ commits.slice(0, maxCommits) :
+ commits;
+ const commitOptions = filteredCommits.map((commit) => {
+ return
+ });
+ content = (
+
+
+
+
+ {commitOptions}
+
+
+ )
+ }
+
+ return (
+ content
+ )
+ }
+}
+
+class StartNotebookCommitsUpdate extends Component {
+ render() {
+ return [
+ ,
+
+ Refresh commits
+
+ ]
+ }
+}
+
+class StartNotebookCommitsOptions extends Component {
+ render() {
+ return [
+ ,
+
+ Commit options
+ ,
+
+ Commit options
+
+
+
+
+ 1-100, 0 for unlimited
+
+
+
+ ]
+ }
+}
+
+class StartNotebookOptions extends Component {
+ render() {
+ const { commit } = this.props.filters;
+ const { branches, commits } = this.props.data;
+ if (!commit.id || StatusHelper.isUpdating(branches) || StatusHelper.isUpdating(commits)) {
+ return null;
+ }
+ const { justStarted } = this.props;
+ if (justStarted) {
+ return
+ }
+ const { status, url } = this.props.notebooks;
+ let content;
+ if (status == null) {
+ content = (
+
+ );
+ }
+ else if (status === false) {
+ const { notebookOptions } = this.props.data;
+ if (!notebookOptions.commitId || notebookOptions.commitId !== commit.id) {
+ content = (
+
+ );
+ }
+ else {
+ content = [
+ ,
+
+ ];
+ }
+ }
+ else {
+ if (status === "running") {
+ content = (
+
+
+
+
+
+ );
+ }
+ else if (status === "spawn") {
+ content = (
+
+
+
+ );
+ }
+ else if (status === "stop") {
+ content = (
+
+
+
+ );
+ }
+ else {
+ content = (
+
+
+
+ );
+ }
+ }
+ return content;
+ }
+}
+
+class StartNotebookServerOptions extends Component {
+ render() {
+ const { notebookOptions, selectedOptions } = this.props.data;
+ const renderedServerOptions = Object.keys(notebookOptions)
+ .filter(key => key !== "commitId")
+ .map(key => {
+ const serverOption = { ...notebookOptions[key], selected: selectedOptions[key] };
+ const onChange = (event) => {
+ this.props.handlers.setServerOption(key, event);
+ };
+
+ switch (serverOption.type) {
+ case 'enum':
+ return
+
+
+ ;
+
+ case 'int':
+ return
+
+
+ ;
+
+ case 'float':
+ return
+
+
+ ;
+
+ case 'boolean':
+ return
+
+
+ ;
+
+ default:
+ return null;
+ }
+ });
+ return renderedServerOptions.length ?
+ renderedServerOptions :
+ ;
+ }
+}
+
+class ServerOptionEnum extends Component {
+ render() {
+ return (
+
+ {this.props.options.map((optionName, i) => {
+ return
+ })}
+
+ );
+ }
+}
+
+class ServerOptionBoolean extends Component {
+ render() {
+ return (
+
+ );
+ }
+}
+
+class ServerOptionRange extends Component {
+ render() {
+ return (
+
+ );
+ }
+}
+
+class ServerOptionLaunch extends Component {
+ render() {
+ return (
+
+ );
+ }
+}
+
+export { NotebookServers, Notebooks, StartNotebookServer }
diff --git a/src/notebooks/Notebooks.state.js b/src/notebooks/Notebooks.state.js
index 4959cf1c34..2551737f55 100644
--- a/src/notebooks/Notebooks.state.js
+++ b/src/notebooks/Notebooks.state.js
@@ -24,12 +24,33 @@
*/
import { Schema, StateKind, StateModel } from '../model/Model';
+import { cleanAnnotations } from '../api-client/notebook-servers';
const notebooksSchema = new Schema({
notebooks: {
schema: {
polling: {initial: null},
- all: {initial: {}}
+ all: {initial: {}},
+ status: {initial: null},
+ url: {initial: null}
+ }
+ },
+ filters: {
+ schema: {
+ branch: {initial: {}},
+ commit: {initial: {}},
+ includeMergedBranches: {initial:false},
+ displayedCommits: {initial:10},
+ displayCommitAuthor: {initial:true},
+ displayCommitTimestamp: {initial:true}
+ }
+ },
+ data: {
+ schema: {
+ commits: {initial:[]}, // is this the proper place? should this logic moved to Project?
+ error: {initial:null},
+ notebookOptions: {initial:{}},
+ selectedOptions: {initial:{}}
}
}
});
@@ -40,6 +61,37 @@ class NotebooksModel extends StateModel {
this.client = client;
}
+ setMergedBranches(value) {
+ this.set('filters.includeMergedBranches', value);
+ }
+
+ setDisplayedCommits(value) {
+ this.set('filters.displayedCommits', value);
+ }
+
+ setBranch(branch) {
+ this.set('filters.branch', branch);
+ }
+
+ setCommit(commit) {
+ this.set('filters.commit', commit);
+ }
+
+ async fetchCommits(projectId, branchName) {
+ if (branchName == null) {
+ this.set('data.commits', []);
+ return [];
+ }
+ else {
+ this.setUpdating({data: {commits: true}});
+ return this.client.getCommits(projectId, branchName)
+ .then(resp => {
+ this.set('data.commits', resp.data);
+ return resp.data;
+ });
+ }
+ }
+
fetchNotebooks(first) {
if (first) {
this.setUpdating({notebooks: {all: true}});
@@ -47,20 +99,124 @@ class NotebooksModel extends StateModel {
return this.client.getNotebookServers()
.then(resp => {
this.set('notebooks.all', resp.data);
+ return resp.data;
});
}
- startNotebookPolling() {
+ verifyIfRunning(projectId, projectPath, servers) {
+ const notebooks = servers ?
+ servers :
+ this.get('notebooks.all');
+ const branch = this.get('filters.branch');
+ const commit = this.get('filters.commit');
+ for (let notebookName of Object.keys(notebooks)) {
+ const notebook = notebooks[notebookName];
+ const annotations = cleanAnnotations(notebook["annotations"], "renku.io");
+ if (parseInt(annotations.projectId) !== projectId) continue;
+ if (annotations["branch"] === branch.name && annotations["commit-sha"] === commit.id) {
+ this.setObject({ notebooks: {
+ status: notebook.ready ?
+ "running" :
+ notebook.pending,
+ url: notebook.url
+ }});
+ return true;
+ }
+ }
+ this.setObject({notebooks: {
+ status: false,
+ url: null
+ }});
+
+ // fetch notebook options
+ this.fetchNotebookOptions(projectPath, commit.id);
+ return false;
+ }
+
+ notebookPollingIteration(verifyRunningId, projectPath) {
+ const fetchPromise = this.fetchNotebooks();
+ if (verifyRunningId) {
+ return fetchPromise.then((servers) => {
+ return this.verifyIfRunning(verifyRunningId, projectPath, servers);
+ });
+ }
+ else {
+ return fetchPromise;
+ }
+ }
+
+ startNotebookPolling(verifyRunningId, projectPath) {
+ if (verifyRunningId) {
+ this.setObject({notebooks: {
+ status: false,
+ url: null
+ }});
+ }
const oldPoller = this.get('notebooks.polling');
if (oldPoller == null) {
const newPoller = setInterval(() => {
- return this.fetchNotebooks();
+ this.notebookPollingIteration(verifyRunningId, projectPath);
}, 3000);
this.set('notebooks.polling', newPoller);
// fetch immediatly
- return this.fetchNotebooks(true);
+ this.notebookPollingIteration(verifyRunningId, projectPath);
+ }
+ }
+
+ fetchNotebookOptions(projectPath, commitId) {
+ const oldData = this.get('data.notebookOptions');
+ if (oldData.commitId === commitId)
+ return;
+ this.set('data.notebookOptions', {});
+ return this.client.getNotebookServerOptions(projectPath, commitId).then((resp)=> {
+ const notebookOptions = { ...resp, commitId };
+ let selectedOptions = {};
+ Object.keys(resp).forEach(option => {
+ selectedOptions[option] = resp[option].default;
+ })
+ const options = {
+ data: {
+ notebookOptions,
+ selectedOptions
+ }
+ };
+ this.setObject(options);
+ return options;
+ });
+ }
+
+ setNotebookOptions(option, value) {
+ this.set(`data.selectedOptions.${option}`, value);
+ }
+
+ startServer(projectPath, branchName, commitId) {
+ const options = {
+ serverOptions: this.get('data.selectedOptions')
+ };
+ let branch = branchName;
+ if (!branchName) {
+ const selctedBranch = this.get('filters.branch');
+ if (selctedBranch.name) {
+ branch = selctedBranch.name;
+ }
+ else {
+ branch = "master";
+ }
}
+ let commit = commitId;
+ if (!commitId) {
+ const selctedCommit = this.get('filters.commit');
+ if (selctedCommit.id) {
+ commit = selctedCommit.id;
+ }
+ else {
+ commit = "latest";
+ }
+ }
+
+ this.set('notebooks.status', 'spawn');
+ return this.client.startNotebook(projectPath, branch, commit, options);
}
stopNotebookPolling() {
diff --git a/src/notebooks/Notebooks.test.js b/src/notebooks/Notebooks.test.js
index c749df7e87..2f631dfd5b 100644
--- a/src/notebooks/Notebooks.test.js
+++ b/src/notebooks/Notebooks.test.js
@@ -26,7 +26,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { MemoryRouter } from 'react-router-dom';
-import { Notebooks } from './Notebooks.container';
+import { Notebooks, StartNotebookServer } from './Notebooks.container';
import { cleanAnnotations, ExpectedAnnotations } from '../api-client/notebook-servers';
import { testClient as client } from '../api-client'
import { generateFakeUser } from '../app-state/UserState.test';
@@ -41,6 +41,17 @@ describe('rendering', () => {
, div);
});
+
+ it('renders launch notebook page without crashing', () => {
+ const div = document.createElement('div');
+ const refreshBranches = async function() {
+ return [];
+ };
+ ReactDOM.render(
+
+
+ , div);
+ });
});
describe('notebook server clean annotation', () => {
diff --git a/src/notebooks/index.js b/src/notebooks/index.js
index 45cfe2cee2..46ff30d11e 100644
--- a/src/notebooks/index.js
+++ b/src/notebooks/index.js
@@ -23,7 +23,6 @@
*
*/
-import { LaunchNotebookServer, NotebookServers } from './Notebooks.container';
-import { Notebooks } from './Notebooks.container';
+import { Notebooks, NotebookServers, StartNotebookServer } from './Notebooks.container';
-export { LaunchNotebookServer, NotebookServers, Notebooks }
+export { NotebookServers, Notebooks, StartNotebookServer }
diff --git a/src/project/Project.js b/src/project/Project.js
index efcd162089..a26b5a386a 100644
--- a/src/project/Project.js
+++ b/src/project/Project.js
@@ -35,7 +35,6 @@ import { FileLineage } from '../file'
import { ACCESS_LEVELS } from '../api-client';
import { alertError } from '../utils/Errors';
import { MergeRequest, MergeRequestList } from '../merge-request';
-import { LaunchNotebookServer } from '../notebooks';
import List from './list';
import New from './new';
@@ -74,7 +73,7 @@ class View extends Component {
return this.projectState.startNotebookServersPolling(this.props.client, this.props.id,PollingInterval.START);
}
async stopNotebookServersPolling() { return this.projectState.stopNotebookServersPolling(); }
- async stopNotebookServer(serverName) { return this.projectState.stopNotebookServer(this.props.client, serverName); }
+ async stopNotebookServer(serverName) { return this.projectState.stopNotebookServer(this.props.client, serverName, this.props.id); }
async createGraphWebhook() { return this.projectState.createGraphWebhook(this.props.client, this.props.id); }
async stopCheckingWebhook() { this.projectState.stopCheckingWebhook(); }
async fetchGraphWebhook() { this.projectState.fetchGraphWebhook(this.props.client, this.props.id, this.props.user); }
@@ -224,7 +223,6 @@ class View extends Component {
};
const ConnectedMergeRequestList = connect(mapStateToProps)(MergeRequestList);
- const ConnectedLaunchNotebookServer = connect(this.projectState.mapStateToProps)(LaunchNotebookServer);
return {
kuList: ,
@@ -260,14 +258,7 @@ class View extends Component {
mrView: (p) => ,
-
- launchNotebookServer: (p) => this.props.history.push(`/projects/${this.projectState.get('core.id')}/notebookServers`)}
- />
+ updateProjectState={this.fetchAll.bind(this)}/>
}
}
@@ -341,6 +332,9 @@ class View extends Component {
},
fetchNotebookServerUrl: () => {
return this.fetchNotebookServerUrl();
+ },
+ fetchBranches: () => {
+ return this.fetchBranches();
}
};
diff --git a/src/project/Project.present.js b/src/project/Project.present.js
index 671e59ad58..4217e49342 100644
--- a/src/project/Project.present.js
+++ b/src/project/Project.present.js
@@ -47,7 +47,7 @@ import { ExternalLink, Loader, RenkuNavLink, TimeCaption} from '../utils/UICompo
import { InfoAlert, SuccessAlert, WarnAlert, ErrorAlert } from '../utils/UIComponents'
import { SpecialPropVal } from '../model/Model'
import { ProjectTags, ProjectTagList } from './shared'
-import { NotebookServers } from '../notebooks'
+import { NotebookServers, StartNotebookServer } from '../notebooks'
import FilesTreeView from './filestreeview/FilesTreeView';
import { ACCESS_LEVELS } from '../api-client';
@@ -611,6 +611,24 @@ class ProjectNotebookServers extends Component {
}
}
+class ProjectStartNotebookServer extends Component {
+ render() {
+ return (
+
+
+
+ )
+ }
+}
+
class RepositoryUrls extends Component {
render() {
return [
@@ -769,7 +787,7 @@ class ProjectView extends Component {
} />
+ render={props => }/>
]
diff --git a/src/project/Project.state.js b/src/project/Project.state.js
index f56f7f9df5..a680ac672f 100644
--- a/src/project/Project.state.js
+++ b/src/project/Project.state.js
@@ -274,7 +274,7 @@ class ProjectModel extends StateModel {
});
}
- stopNotebookServer(client, serverName) {
+ stopNotebookServer(client, serverName, id) {
// manually set the state instead of waiting for the promise to resolve
const updatedState = {
notebooks: {
@@ -286,6 +286,11 @@ class ProjectModel extends StateModel {
}
}
}
+
+ const oldInterval = this.get('notebooks.pollingInterval');
+ if (oldInterval !== PollingInterval.START) {
+ this.changeNotebookServerPollingInterval(client, id, PollingInterval.START);
+ }
this.setObject(updatedState);
return client.stopNotebookServer(serverName);
}
@@ -317,10 +322,11 @@ class ProjectModel extends StateModel {
fetchBranches(client, id) {
this.setUpdating({system: {branches: true}});
- client.getBranches(id)
+ return client.getBranches(id)
.then(resp => resp.data)
.then(d => {
this.set('system.branches', d)
+ return d;
})
}
diff --git a/src/utils/Time.js b/src/utils/Time.js
index 7535f07064..5f7e909f6c 100644
--- a/src/utils/Time.js
+++ b/src/utils/Time.js
@@ -38,7 +38,7 @@ class Time {
if (this.isDate(convertedDate)) {
return convertedDate;
}
- throw("Invalid date");
+ throw(new Error("Invalid date"));
}
static toISOString(inputDate, type="datetime") {
@@ -54,7 +54,7 @@ class Time {
return readableDate.substring(11);
}
else {
- throw(`Uknown type "${type}"`);
+ throw(new Error(`Uknown type "${type}"`));
}
}
}