Skip to content

Commit

Permalink
feat: edit project sessions settings
Browse files Browse the repository at this point in the history
* Create new sessions sub-navigation in the settings tab
* Uset the config APIs to get and set project options

fix #1114
  • Loading branch information
lorenzo-cavazzi committed Jun 9, 2021
1 parent c7b4662 commit 6196170
Show file tree
Hide file tree
Showing 15 changed files with 1,131 additions and 264 deletions.
14 changes: 14 additions & 0 deletions client/src/api-client/notebook-servers.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ function addNotebookServersMethods(client) {
anonymous
).then((resp) => {
let { data } = resp;

// ? rename defaultUrl to default_url to prevent conflicts later with project options
if (data && "defaultUrl" in data) {
data.default_url = data.defaultUrl;
delete data.defaultUrl;
}

Object.keys(data).forEach(key => {
data[key].selected = data[key].default;
});
Expand All @@ -80,6 +87,13 @@ function addNotebookServersMethods(client) {
const headers = client.getBasicHeaders();
headers.append("Content-Type", "application/json");
const url = `${client.baseUrl}/notebooks/servers`;

// ? rename default_url to legacy defaultUrl
if (options && options.serverOptions && "default_url" in options.serverOptions) {
options.serverOptions.defaultUrl = options.serverOptions.default_url;
delete options.serverOptions.default_url;
}

let parameters = {
namespace: decodeURIComponent(namespacePath),
project: projectPath,
Expand Down
39 changes: 39 additions & 0 deletions client/src/api-client/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,45 @@ function addProjectMethods(client) {
return Promise.resolve(datasetsPromise)
.then(datasetsContent => Promise.all(datasetsContent));
};

/**
* Get project config file data
* @see {@link https://github.com/SwissDataScienceCenter/renku-python/blob/master/renku/service/views/config.py}
* @param {string} projectRepositoryUrl - external repository full url.
*/
client.getProjectConfig = async (projectRepositoryUrl) => {
const url = `${client.baseUrl}/renku/config.show`;
const queryParams = { git_url: projectRepositoryUrl };
let headers = client.getBasicHeaders();
headers.append("Content-Type", "application/json");
headers.append("X-Requested-With", "XMLHttpRequest");

return client.clientFetch(url, {
method: "GET",
headers,
queryParams
});
};

/**
* Set project config data
* @see {@link https://github.com/SwissDataScienceCenter/renku-python/blob/master/renku/service/views/config.py}
* @param {string} projectRepositoryUrl - external repository full url.
* @param {object} config - config object in the form {key: value}. A null value removes the key.
*/
client.setProjectConfig = async (projectRepositoryUrl, config) => {
const url = `${client.baseUrl}/renku/config.set`;
const body = { git_url: projectRepositoryUrl, config };
let headers = client.getBasicHeaders();
headers.append("Content-Type", "application/json");
headers.append("X-Requested-With", "XMLHttpRequest");

return client.clientFetch(url, {
method: "POST",
headers,
body: JSON.stringify(body)
});
};
}


Expand Down
11 changes: 11 additions & 0 deletions client/src/model/RenkuModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,17 @@ const projectGlobalSchema = new Schema({
branch: { [Prop.INITIAL]: { name: "master" }, [Prop.MANDATORY]: true },
commit: { [Prop.INITIAL]: { id: "latest" }, [Prop.MANDATORY]: true },
})
},
config: {
[Prop.SCHEMA]: new Schema({
data: { [Prop.INITIAL]: {}, [Prop.MANDATORY]: true },
error: { [Prop.INITIAL]: {}, [Prop.MANDATORY]: true },
fetched: { [Prop.INITIAL]: null },
fetching: { [Prop.INITIAL]: false },

initial: { [Prop.INITIAL]: {} },
input: { [Prop.INITIAL]: {} }
})
}
});

Expand Down
54 changes: 32 additions & 22 deletions client/src/notebooks/Notebooks.present.js
Original file line number Diff line number Diff line change
Expand Up @@ -1591,8 +1591,8 @@ class StartNotebookOptionsRunning extends Component {
*/
function mergeEnumOptions(globalOptions, projectOptions, key) {
let options = globalOptions[key].options;
// defaultUrl can extend the existing options, but not the other ones
if (key === "defaultUrl"
// default_url can extend the existing options, but not the other ones
if (key === "default_url"
&& Object.keys(projectOptions).indexOf(key) >= 0
&& globalOptions[key].options.indexOf(projectOptions[key]) === -1)
options = [...globalOptions[key].options, projectOptions[key]];
Expand Down Expand Up @@ -1635,20 +1635,20 @@ class StartNotebookServerOptions extends Component {
const options = mergeEnumOptions(globalOptions, projectOptions, key);
serverOption["options"] = options;
return <FormGroup key={key} className={serverOption.options.length === 1 ? "mb-0" : ""}>
<Label>{serverOption.displayName}</Label>
<Label className="me-2">{serverOption.displayName}</Label>
<ServerOptionEnum {...serverOption} onChange={onChange} />
{warning}
</FormGroup>;
}
case "int":
return <FormGroup key={key}>
<Label>{`${serverOption.displayName}: ${serverOption.selected}`}</Label>
<Label className="me-2">{`${serverOption.displayName}: ${serverOption.selected}`}</Label>
<ServerOptionRange step={1} {...serverOption} onChange={onChange} />
</FormGroup>;

case "float":
return <FormGroup key={key}>
<Label>{`${serverOption.displayName}: ${serverOption.selected}`}</Label>
<Label className="me-2">{`${serverOption.displayName}: ${serverOption.selected}`}</Label>
<ServerOptionRange step={0.01} {...serverOption} onChange={onChange} />
</FormGroup>;

Expand Down Expand Up @@ -1696,23 +1696,30 @@ class ServerOptionEnum extends Component {

if (selected && options && options.length && !options.includes(selected))
options = options.concat(selected);
if (options.length === 1)
return (<label>: {this.props.selected}</label>);
if (options.length === 1) {
const color = this.props.selected ?
"primary" :
"light";
return (<Badge color={color}>{this.props.options[0]}</Badge>);
}

return (
<div>
<ButtonGroup>
{options.map((optionName, i) => {
const color = optionName === selected ? "primary" : "outline-primary";
return (
<Button
color={color}
key={optionName}
onClick={event => this.props.onChange(event, optionName)}>{optionName}</Button>
);
})}
</ButtonGroup>
</div>
<ButtonGroup>
{options.map((optionName, i) => {
let color = "outline-primary";
if (optionName === selected) {
color = this.props.warning != null && this.props.warning === optionName ?
"danger" :
"primary";
}
const size = this.props.size ? this.props.size : "sm";
return (
<Button
key={optionName} color={color} size={size}
onClick={event => this.props.onChange(event, optionName)}>{optionName}</Button>
);
})}
</ButtonGroup>
);
}
}
Expand All @@ -1722,7 +1729,7 @@ class ServerOptionBoolean extends Component {
// The double negation solves an annoying problem happening when checked=undefined
// https://stackoverflow.com/a/39709700/1303090
const selected = !!this.props.selected;
return (<div className="form-check form-switch">
return (<div className="form-check form-switch d-inline-block">
<Input type="switch" id={this.props.id} label={this.props.displayName}
checked={selected} onChange={this.props.onChange} className="form-check-input rounded-pill"/>
<Label check htmlFor={this.props.id}>{this.props.displayName}</Label>
Expand Down Expand Up @@ -1906,4 +1913,7 @@ class CheckNotebookIcon extends Component {
}
}

export { CheckNotebookIcon, Notebooks, NotebooksDisabled, ShowSession, StartNotebookServer, mergeEnumOptions };
export {
CheckNotebookIcon, Notebooks, NotebooksDisabled, ServerOptionBoolean, ServerOptionEnum, ServerOptionRange,
ShowSession, StartNotebookServer, mergeEnumOptions
};
103 changes: 95 additions & 8 deletions client/src/notebooks/Notebooks.state.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ const VALID_SETTINGS = [
"image"
];

const SESSIONS_PREFIX = "interactive.";

const ExpectedAnnotations = {
domain: "renku.io",
"renku.io": {
Expand Down Expand Up @@ -109,11 +111,93 @@ const NotebooksHelper = {
return { ...cleaned };
},

/**
* Get options and default values from notebooks and project options
*
* @param {object} notebooksOptions - notebook options as return by the "GET server_options" API.
* @param {object} projectOptions - project options as returned by the "GET config.show" API.
* @param {string} [projectPrefix] - project option key prefix used by config to identify session options.
*/
getProjectDefault: (notebooksOptions, projectOptions, projectPrefix = SESSIONS_PREFIX) => {
let returnObject = {};
if (!notebooksOptions || !projectOptions)
return returnObject;

// Conversion helper
const convert = (value) => {
// return boolean
if (value === true || value === false)
return value;

// convert stringy boolean
if (value && value.toLowerCase() === "true")
return true;

else if (value && value.toLowerCase() === "false")
return false;

// convert stringy number
else if (!isNaN(value))
return parseFloat(value);

return value;
};

// Define options
const sortedOptions = Object.keys(notebooksOptions)
.sort((a, b) => parseInt(notebooksOptions[a].order) - parseInt(notebooksOptions[b].order));
let unknownOptions = [];

// Get RenkuLab defaults
let globalDefaults = [...sortedOptions].reduce((defaults, option) => {
if ("default" in notebooksOptions[option])
defaults[option] = notebooksOptions[option].default;
return defaults;
}, {});

// Overwrite renku defaults
if (projectOptions && projectOptions.default && Object.keys(projectOptions.default)) {
for (const [key, value] of Object.entries(projectOptions.default)) {
if (key.startsWith(projectPrefix)) {
const option = key.substring(projectPrefix.length);
if (!sortedOptions.includes(option))
unknownOptions.push(option);
globalDefaults[option] = convert(value);
}
}
}

// Get project defaults
let projectDefaults = [];
if (projectOptions && projectOptions.config && Object.keys(projectOptions.config)) {
for (const [key, value] of Object.entries(projectOptions.config)) {
if (key.startsWith(projectPrefix)) {
const option = key.substring(projectPrefix.length);
if (!sortedOptions.includes(option))
unknownOptions.push(option);
projectDefaults[option] = convert(value);
}
}
}

return {
defaults: {
global: globalDefaults,
project: projectDefaults
},
options: {
known: sortedOptions,
unknown: unknownOptions
}
};
},

/**
* Parse project options raw data from the .ini file to a JS object
*
* @param {string} data - raw file content
*/
// TODO: use the getProjectDefault function after switching to the "GET config.show" API
parseProjectOptions: (data) => {
let projectOptions = {};

Expand All @@ -132,10 +216,10 @@ const NotebooksHelper = {
parsedOptions = parsedData[RENKU_INI_SECTION_LEGACY];
if (parsedOptions) {
Object.keys(parsedOptions).forEach(parsedOption => {
// treat "default_url" as "defaultUrl" to allow name consistency in the the .ini file
// treat "defaultUrl" as "default_url" to allow name consistency in the the .ini file
let option = parsedOption;
if (parsedOption === "default_url")
option = "defaultUrl";
if (parsedOption === "defaultUrl")
option = "default_url";

// convert boolean and numbers
let value = parsedOptions[parsedOption];
Expand Down Expand Up @@ -165,8 +249,8 @@ const NotebooksHelper = {
* @param {string} currentValue - current project option value
*/
checkOptionValidity: (globalOptions, currentOption, currentValue) => {
// defaultUrl is a special case and any string will fit
if (currentOption === "defaultUrl") {
// default_url is a special case and any string will fit
if (currentOption === "default_url") {
if (typeof currentValue === "string")
return true;
return false;
Expand Down Expand Up @@ -217,7 +301,8 @@ const NotebooksHelper = {
},

pipelineTypes: PIPELINE_TYPES,
validSettings: VALID_SETTINGS
validSettings: VALID_SETTINGS,
sessionConfigPrefix: SESSIONS_PREFIX
};

class NotebooksCoordinator {
Expand Down Expand Up @@ -361,8 +446,9 @@ class NotebooksCoordinator {
});
}

async fetchNotebookOptions() {
await this.fetchProjectOptions();
async fetchNotebookOptions(skip = false) {
if (!skip)
await this.fetchProjectOptions();
const oldData = this.model.get("options.global");
if (Object.keys(oldData).length !== 0)
return;
Expand All @@ -381,6 +467,7 @@ class NotebooksCoordinator {
});
}

// TODO: switch to the "GET config.show" API. Adapt (or remove) also the following setDefaultOptions function
async fetchProjectOptions() {
// prepare query data and reset warnings
const filters = this.getQueryFilters();
Expand Down
Loading

0 comments on commit 6196170

Please sign in to comment.