From 5512165a9f76fa251d6406eed85b5a13253ff03e Mon Sep 17 00:00:00 2001 From: Chandrasekhar Ramakrishnan Date: Tue, 16 Nov 2021 10:34:30 +0100 Subject: [PATCH] refactor: hoist project state to global model (#1183) (#1568) Fix #1183 --- client/src/App.js | 3 +- .../file/KnowledgeGraphStatus.container.js | 4 +- client/src/index.js | 24 +- client/src/model/RenkuModels.js | 143 ++-- client/src/model/RenkuModels.test.js | 14 +- client/src/notebooks/Notebooks.container.js | 167 +++-- client/src/notebooks/Notebooks.present.js | 143 ++-- client/src/notebooks/Notebooks.test.js | 9 + client/src/project/Project.js | 132 ++-- client/src/project/Project.present.js | 134 ++-- client/src/project/Project.state.js | 678 ++++++++++-------- client/src/project/Project.test.js | 83 ++- .../src/project/datasets/DatasetsListView.js | 4 +- .../project/overview/ProjectOverview.test.js | 17 +- .../settings/ProjectSettings.present.js | 14 +- .../project/settings/ProjectSettings.test.js | 3 +- .../project/shared/ProjectTag.container.js | 2 +- client/src/utils/EnhancedState.js | 3 +- client/src/utils/UIComponents.js | 5 +- .../formgenerator/FormGenerator.container.js | 37 +- 20 files changed, 857 insertions(+), 762 deletions(-) diff --git a/client/src/App.js b/client/src/App.js index 5772634ee5..b2db6ccf7a 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -99,8 +99,7 @@ function CentralContentContainer(props) { /> { - this.setState({ error: err }); + .catch((error) => { + this.setState({ error }); this.stopPollingProgress(); }); } diff --git a/client/src/index.js b/client/src/index.js index f514dc6272..eadb80fe56 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -1,6 +1,6 @@ import React from "react"; import ReactDOM from "react-dom"; -import { connect } from "react-redux"; +import { connect, Provider } from "react-redux"; import { BrowserRouter as Router, Route } from "react-router-dom"; import "bootstrap"; import "jquery"; @@ -83,16 +83,18 @@ Promise.all([configFetch, privacyFetch]).then(valuesRead => { const VisibleApp = connect(mapStateToProps)(App); ReactDOM.render( - - { - LoginHelper.handleLoginParams(props.history); - return ( - - ); - }} /> - , + + + { + LoginHelper.handleLoginParams(props.history); + return ( + + ); + }} /> + + , document.getElementById("root") ); }); diff --git a/client/src/model/RenkuModels.js b/client/src/model/RenkuModels.js index 84629e25cb..1fe4c7c5e0 100644 --- a/client/src/model/RenkuModels.js +++ b/client/src/model/RenkuModels.js @@ -90,14 +90,8 @@ const projectSchema = new Schema({ core: { schema: { available: { initial: null }, - created_at: { initial: null, }, - last_activity_at: { initial: null, }, id: { initial: null, }, description: { initial: "no description", mandatory: true }, - displayId: { initial: "", }, - title: { initial: "no title", mandatory: true }, - external_url: { initial: "", }, - path_with_namespace: { initial: null }, owner: { initial: null }, } }, @@ -119,48 +113,20 @@ const projectSchema = new Schema({ schema: { files: { schema: [] } } - }, - readme: { - schema: { - text: { initial: "", mandatory: false } - } } }, }, system: { schema: { - tag_list: { schema: [] }, - star_count: { initial: 0, mandatory: true }, - forks_count: { initial: 0, mandatory: true }, - forked_from_project: { initial: {} }, - ssh_url: { initial: "", }, - http_url: { initial: "", }, - merge_requests: { schema: [], initial: [] }, branches: { schema: [], initial: [] }, autosaved: { schema: [], initial: [] }, } }, - files: { - schema: { - notebooks: { schema: [] }, - data: { schema: [] }, - modifiedFiles: { initial: {}, mandatory: true } - } - }, transient: { schema: { requests: { initial: {} }, } }, - webhook: { - schema: { - status: { initial: null }, - created: { initial: null }, - possible: { initial: null }, - stop: { initial: null }, - progress: { initial: null } - } - }, migration: { schema: { migration_required: { initial: null }, @@ -285,37 +251,60 @@ const projectStatisticsSchema = new Schema({ }); const projectGlobalSchema = new Schema({ - metadata: { + branches: { [Prop.SCHEMA]: new Schema({ - exists: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true }, - - id: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true }, // id - namespace: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true }, // namespace.full_path - path: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true }, // path - pathWithNamespace: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true }, // path_with_namespace - repositoryUrl: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true }, // web_url - starCount: { [Prop.INITIAL]: null }, // star_count - forksCount: { [Prop.INITIAL]: null }, // forks_count - + standard: { [Prop.INITIAL]: [], [Prop.MANDATORY]: true }, + autosaved: { [Prop.INITIAL]: [], [Prop.MANDATORY]: true }, + error: { [Prop.INITIAL]: null }, fetched: { [Prop.INITIAL]: null }, fetching: { [Prop.INITIAL]: false }, }) }, - statistics: { + commits: { [Prop.SCHEMA]: new Schema({ - data: { schema: projectStatisticsSchema }, + list: { [Prop.INITIAL]: [], [Prop.MANDATORY]: true }, + error: { [Prop.INITIAL]: null }, fetched: { [Prop.INITIAL]: null }, fetching: { [Prop.INITIAL]: false }, }) }, - commits: { + config: { [Prop.SCHEMA]: new Schema({ - list: { [Prop.INITIAL]: [], [Prop.MANDATORY]: true }, - error: { [Prop.INITIAL]: null }, - + 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]: {} } + }) + }, + data: { + [Prop.SCHEMA]: new Schema({ + readme: { [Prop.INITIAL]: {} } + }) + }, + datasets: { + [Prop.SCHEMA]: new Schema({ + datasets_kg: { [Prop.INITIAL]: [] }, + core: { [Prop.INITIAL]: { + datasets: null, + error: null + } } + }) + }, + files: { + [Prop.SCHEMA]: new Schema({ + notebooks: { [Prop.INITIAL]: [] }, + data: { [Prop.INITIAL]: [] }, + modifiedFiles: { [Prop.INITIAL]: {}, [Prop.MANDATORY]: true } + }) + }, + filesTree: { + [Prop.SCHEMA]: new Schema({ + hash: { [Prop.INITIAL]: {} }, + loaded: { [Prop.INITIAL]: false, [Prop.MANDATORY]: true } }) }, filters: { @@ -324,17 +313,57 @@ const projectGlobalSchema = new Schema({ commit: { [Prop.INITIAL]: { id: "latest" }, [Prop.MANDATORY]: true }, }) }, - config: { + forkedFromProject: { [Prop.INITIAL]: {} }, + metadata: { [Prop.SCHEMA]: new Schema({ - data: { [Prop.INITIAL]: {}, [Prop.MANDATORY]: true }, - error: { [Prop.INITIAL]: {}, [Prop.MANDATORY]: true }, + avatarUrl: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true }, // avatar_url + accessLevel: { [Prop.INITIAL]: 0, [Prop.MANDATORY]: true }, // visibility.access_level + createdAt: { [Prop.INITIAL]: "", [Prop.MANDATORY]: true }, // created_at + defaultBranch: { [Prop.INITIAL]: null }, // default_branch + description: { [Prop.INITIAL]: "" }, + exists: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true }, + externalUrl: { [Prop.INITIAL]: "" }, // external_url + forksCount: { [Prop.INITIAL]: null }, // forks_count + httpUrl: { [Prop.INITIAL]: "", }, // http_url + id: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true }, // id + lastActivityAt: { [Prop.INITIAL]: "", [Prop.MANDATORY]: true }, // last_activity_at + namespace: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true }, // namespace.full_path + owner: { [Prop.INITIAL]: null }, + path: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true }, + pathWithNamespace: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true }, // path_with_namespace + repositoryUrl: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true }, // web_url + sshUrl: { [Prop.INITIAL]: "", }, // ssh_url + starCount: { [Prop.INITIAL]: null }, // star_count + tagList: { [Prop.INITIAL]: [] }, // tag_list + title: { [Prop.INITIAL]: "" }, + visibility: { [Prop.INITIAL]: "private", [Prop.MANDATORY]: true }, // visibility.level + fetched: { [Prop.INITIAL]: null }, fetching: { [Prop.INITIAL]: false }, + }) + }, + statistics: { + [Prop.SCHEMA]: new Schema({ + data: { schema: projectStatisticsSchema }, - initial: { [Prop.INITIAL]: {} }, - input: { [Prop.INITIAL]: {} } + fetched: { [Prop.INITIAL]: null }, + fetching: { [Prop.INITIAL]: false }, }) - } + }, + transient: { + [Prop.SCHEMA]: new Schema({ + requests: { [Prop.INITIAL]: {} } + }) + }, + webhook: { + [Prop.SCHEMA]: { + status: { [Prop.INITIAL]: null }, + created: { [Prop.INITIAL]: null }, + possible: { [Prop.INITIAL]: null }, + stop: { [Prop.INITIAL]: null }, + progress: { [Prop.INITIAL]: null } + } + }, }); const notebooksSchema = new Schema({ diff --git a/client/src/model/RenkuModels.test.js b/client/src/model/RenkuModels.test.js index fcfe8d85af..0df75a56fa 100644 --- a/client/src/model/RenkuModels.test.js +++ b/client/src/model/RenkuModels.test.js @@ -1,17 +1,19 @@ /* eslint-disable */ import { testClient as client } from "../api-client"; -import { StateKind, StateModel } from "../model/Model"; +import { StateModel, globalSchema } from "../model"; // import { Project, projectSchema } from "./RenkuModels"; -import { ProjectModel } from "../project/Project.state"; +import { ProjectCoordinator } from "../project"; + +const model = new StateModel(globalSchema); describe("fetch project", () => { it("fetches project", () => { const projectId = 3; - const project = new ProjectModel(StateKind.REDUX); - project.fetchProject(client, projectId).then(() => { - expect(project.get("core.id")).toEqual(projectId); - expect(project.get("core.title")).toEqual("A-first-project"); + const projectCoordinator = new ProjectCoordinator(client, model.subModel("project")); + projectCoordinator.fetchProject(client, projectId).then(() => { + expect(projectCoordinator.get("metadata.id")).toEqual(projectId); + expect(projectCoordinator.get("metadata.title")).toEqual("A-first-project"); }); }); }); diff --git a/client/src/notebooks/Notebooks.container.js b/client/src/notebooks/Notebooks.container.js index 78114cbee3..8a497690d2 100644 --- a/client/src/notebooks/Notebooks.container.js +++ b/client/src/notebooks/Notebooks.container.js @@ -45,6 +45,30 @@ import { Url } from "../utils/url"; * @param {Object} [location] - react location object * @param {Object} [history] - react history object */ + +function mapSessionStateToProps(state, ownProps) { + const notebooks = state.notebooks.notebooks; + const available = notebooks.all[ownProps.target] ? + true : + false; + const notebook = { + available, + fetched: notebooks.fetched, + fetching: notebooks.fetching, + data: available ? + notebooks.all[ownProps.target] : + {}, + logs: state.notebooks.logs + }; + return { + handlers: ownProps.handlers, + target: ownProps.target, + filters: state.notebooks.filters, + notebook + }; +} + +const ShowSessionMapped = connect(mapSessionStateToProps)(ShowSessionPresent); class ShowSession extends Component { constructor(props) { super(props); @@ -96,42 +120,30 @@ class ShowSession extends Component { return this.coordinator.fetchLogs(serverName, full); } - mapStateToProps(state, ownProps) { - const notebooks = state.notebooks.notebooks; - const available = notebooks.all[this.target] ? - true : - false; - const notebook = { - available, - fetched: notebooks.fetched, - fetching: notebooks.fetching, - data: available ? - notebooks.all[this.target] : - {}, - logs: state.notebooks.logs - }; - return { - handlers: this.handlers, - target: this.target, - filters: state.notebooks.filters, - notebook - }; - } - - render() { if (this.props.blockAnonymous) return ; - const ShowSessionMapped = connect(this.mapStateToProps.bind(this))(ShowSessionPresent); return ( ); } } +function mapSessionListStateToProps(state, ownProps) { + return { + handlers: ownProps.handlers, + ...state.notebooks, + logs: { ...state.notebooks.logs, show: ownProps.showingLogs } + }; +} + +const VisibleNotebooks = connect(mapSessionListStateToProps)(NotebooksPresent); + /** * Display the list of Notebook servers * @@ -212,27 +224,19 @@ class Notebooks extends Component { this.fetchLogs(serverName); } - mapStateToProps(state, ownProps) { - return { - handlers: this.handlers, - ...state.notebooks, - logs: { ...state.notebooks.logs, show: this.state.showingLogs } - }; - } - render() { if (this.props.blockAnonymous) return ; - const VisibleNotebooks = connect(this.mapStateToProps.bind(this))(NotebooksPresent); - return ; } } @@ -243,6 +247,7 @@ class Notebooks extends Component { * @param {Object} client - api-client used to query the gateway * @param {Object} model - global model for the ui * @param {Object[]} branches - Branches as returned by gitlab "/branches" API - no autosaved branches + * @param {Object[]} commits - Commits as stored in the ProjectCoordinator * @param {Object[]} autosaved - Autosaved branches * @param {function} refreshBranches - Function to invoke to refresh the list of branches * @param {Object} scope - object containing filtering parameters @@ -279,7 +284,12 @@ class StartNotebookServer extends Component { false; this.state = { autostartReady: false, - autostartTried: false + autostartTried: false, + first: true, + ignorePipeline: null, + launchError: null, + showAdvanced: false, + starting: false }; this.handlers = { @@ -288,16 +298,12 @@ class StartNotebookServer extends Component { reTriggerPipeline: this.reTriggerPipeline.bind(this), setBranch: this.selectBranch.bind(this), setCommit: this.selectCommit.bind(this), - toggleMergedBranches: this.toggleMergedBranches.bind(this), + setIgnorePipeline: this.setIgnorePipeline.bind(this), setDisplayedCommits: this.setDisplayedCommits.bind(this), setServerOption: this.setServerOptionFromEvent.bind(this), - startServer: this.startServer.bind(this) - }; - - this.state = { - first: true, - starting: false, - launchError: null + startServer: this.startServer.bind(this), + toggleMergedBranches: this.toggleMergedBranches.bind(this), + toggleShowAdvanced: this.toggleShowAdvanced.bind(this) }; } @@ -318,23 +324,32 @@ class StartNotebookServer extends Component { componentDidUpdate(previousProps) { // TODO: temporary fix to prevent issue with component rerendered multiple times at the first url load if (this.state.first && - StatusHelper.isUpdating(previousProps.branches) && - !StatusHelper.isUpdating(this.props.branches)) { + StatusHelper.isUpdating(previousProps.fetchingBranches) && + !StatusHelper.isUpdating(this.props.fetchingBranches)) { this.setState({ first: false }); if (this._isMounted) - this.selectBranch(); + this.refreshBranches(); + this.selectBranch(); + } } + toggleShowAdvanced() { + this.setState({ showAdvanced: !this.state.showAdvanced }); + } + + setIgnorePipeline(value) { + this.setState({ ignorePipeline: value }); + } + async refreshBranches() { if (this._isMounted) { - if (StatusHelper.isUpdating(this.props.branches)) + if (StatusHelper.isUpdating(this.props.fetchingBranches)) return; await this.props.refreshBranches(); if (this._isMounted && this.state.first) this.selectBranch(); - } } @@ -458,6 +473,7 @@ class StartNotebookServer extends Component { errorMessage: `The session could not start because the image is still building. Please wait for the build to finish, or start the session with the base image.` }, + showAdvanced: true, starting: false }); } @@ -474,6 +490,7 @@ class StartNotebookServer extends Component { errorMessage: `The session could not start because no image is available. Please select a different commit or start the session with the base image.` }, + showAdvanced: true, starting: false }); } @@ -576,25 +593,24 @@ class StartNotebookServer extends Component { return branches.filter(branch => branch.autosave.username === username); } - mapStateToProps(state, ownProps) { - const username = state.user.logged ? - state.user.data.username : + propsToChildProps() { + const username = this.props.user.logged ? + this.props.user.data.username : null; - const ownAutosaved = this.filterAutosavedBranches([...ownProps.inherited.autosaved], username); + const ownAutosaved = this.filterAutosavedBranches([...this.props.autosaved], username); const augmentedState = { - ...state.notebooks, + ...this.props.notebooks, data: { - fetched: state.project.commits.fetched, - fetching: state.project.commits.fetching, - commits: state.project.commits.list, - branches: ownProps.inherited.branches, + fetched: this.props.commits.fetched, + fetching: this.props.commits.fetching, + commits: this.props.commits.list, + branches: this.props.branches, autosaved: ownAutosaved }, - externalUrl: ownProps.inherited.externalUrl + externalUrl: this.props.externalUrl }; return { handlers: this.handlers, - store: ownProps.store, // adds store and other props manually added to ...augmentedState }; } @@ -603,19 +619,36 @@ class StartNotebookServer extends Component { if (this.props.blockAnonymous) return ; - const ConnectedStartNotebookServer = connect(this.mapStateToProps.bind(this))(StartNotebookServerPresent); - - return ; } } +function mapNotebookStatusStateToProps(state, ownProps) { + const subState = state.notebooks; + + const notebookKeys = Object.keys(subState.notebooks.all); + const notebook = notebookKeys.length > 0 ? + subState.notebooks.all[notebookKeys] : + null; + + return { + fetched: subState.notebooks.fetched, + fetching: subState.notebooks.fetching, + notebook + }; +} + +const VisibleNotebookIcon = connect(mapNotebookStatusStateToProps)(CheckNotebookIcon); + /** * Display the connect to Jupyter icon * @@ -686,8 +719,6 @@ class CheckNotebookStatus extends Component { } render() { - const VisibleNotebookIcon = connect(this.mapStateToProps.bind(this))(CheckNotebookIcon); - return (); } } diff --git a/client/src/notebooks/Notebooks.present.js b/client/src/notebooks/Notebooks.present.js index 17de0f5596..620cdd1108 100644 --- a/client/src/notebooks/Notebooks.present.js +++ b/client/src/notebooks/Notebooks.present.js @@ -1031,93 +1031,74 @@ class EnvironmentLogs extends Component { // * StartNotebookServer code * // -class StartNotebookServer extends Component { - constructor(props) { - super(props); - // show advanced if there was an error - const initialShowAdvanced = props.launchError != null; - this.state = { - ignorePipeline: null, - showAdvanced: initialShowAdvanced - }; - } - - toggleShowAdvanced() { - this.setState({ showAdvanced: !this.state.showAdvanced }); - } - - setIgnorePipeline(value) { - this.setState({ ignorePipeline: value }); - } - - render() { - const { branch, commit } = this.props.filters; - const { branches } = this.props.data; - const { autoStarting, pipelines, message } = this.props; +function StartNotebookServer(props) { + const toggleShowAdvanced = props.handlers.toggleShowAdvanced; + const setIgnorePipeline = props.handlers.setIgnorePipeline; + const { branch, commit } = props.filters; + const { autoStarting, pipelines, message } = props; - if (autoStarting) - return (); + if (autoStarting) + return (); - const fetching = { - branches: StatusHelper.isUpdating(branches) ? true : false, - pipelines: pipelines.fetching, - commits: this.props.data.fetching - }; + const fetching = { + branches: StatusHelper.isUpdating(props.fetchingBranches) ? true : false, + pipelines: pipelines.fetching, + commits: props.data.fetching + }; - let show = {}; - show.commits = !fetching.branches && branch.name ? true : false; - show.pipelines = show.commits && !fetching.commits && commit && commit.id; - show.options = show.pipelines && pipelines.fetched; + let show = {}; + show.commits = !fetching.branches && branch.name ? true : false; + show.pipelines = show.commits && !fetching.commits && commit && commit.id; + show.options = show.pipelines && pipelines.fetched; - const messageOutput = message ? - (
{message}
) : - null; - const disabled = fetching.branches || fetching.commits; + const messageOutput = message ? + (
{message}
) : + null; + const disabled = fetching.branches || fetching.commits; - const buttonMessage = this.state.showAdvanced ? - "Hide branch, commit, and image settings" : - "Do you want to select the branch, commit, or image?"; + const buttonMessage = props.showAdvanced ? + "Hide branch, commit, and image settings" : + "Do you want to select the branch, commit, or image?"; - return ( - - -

Start a new session

- - {messageOutput} -
- - - {show.commits ? : null} - {show.pipelines ? : null} - - - {show.options ? - ( - - ) : + return ( + + +

Start a new session

+ + {messageOutput} + + + + {show.commits ? : null} + {show.pipelines ? : null} + + + {show.options ? + ( + + ) : + null + } + {show.options ? + : + !props.showAdvanced ? + : null - } - {show.options ? - : - !this.state.showAdvanced ? - : - null - } - - -
- ); - } + } + + +
+ ); } function StartNotebookAutostart(props) { @@ -1161,7 +1142,7 @@ class StartNotebookBranches extends Component { const { branches } = this.props.data; const { disabled } = this.props; let content; - if (StatusHelper.isUpdating(branches)) { + if (StatusHelper.isUpdating(this.props.fetchingBranches)) { content = ( ); diff --git a/client/src/notebooks/Notebooks.test.js b/client/src/notebooks/Notebooks.test.js index 3aea2b2e52..a08730d51e 100644 --- a/client/src/notebooks/Notebooks.test.js +++ b/client/src/notebooks/Notebooks.test.js @@ -33,6 +33,7 @@ import { import { mergeEnumOptions } from "./Notebooks.present"; import { ExpectedAnnotations } from "./Notebooks.state"; import { StateModel, globalSchema } from "../model"; +import { ProjectCoordinator } from "../project"; import { testClient as client } from "../api-client"; @@ -365,9 +366,17 @@ describe("rendering", () => { }); it("renders StartNotebookServer without crashing", async () => { + const projectCoordinator = new ProjectCoordinator(client, model.subModel("project")); + await act(async () => { + await projectCoordinator.fetchProject(client, "test"); + await projectCoordinator.fetchCommits(); + }); const props = { client, model, + commits: projectCoordinator.get("commits"), + notebooks: model.get("notebooks"), + user: { logged: true, data: { username: "test" } }, branches: [], autosaved: [], location: fakeLocation, diff --git a/client/src/project/Project.js b/client/src/project/Project.js index e9fc7c544e..4dccc783f7 100644 --- a/client/src/project/Project.js +++ b/client/src/project/Project.js @@ -211,37 +211,36 @@ class View extends Component { async fetchProject() { const pathComponents = splitProjectSubRoute(this.props.match.url); - const projectData = this.projectState.fetchProject(this.props.client, pathComponents.projectPathWithNamespace); - // TODO: gradually move queries from local store projectState to shared store projectCoordinator + const projectData = + this.projectCoordinator.fetchProject(this.props.client, pathComponents.projectPathWithNamespace); projectData.then(data => { - this.projectCoordinator.setProjectData(data, true); - this.projectCoordinator.fetchCommits(); - // TODO: move fetchBranches to projectCoordinator. We should fetch commits after we know the defaul branch + this.projectState.setProjectData(data, true); + // TODO: We should fetch commits after we know the default branch this.fetchBranches(); + this.projectCoordinator.fetchCommits(); }); + return projectData; } - async fetchReadme() { return this.projectState.fetchReadme(this.props.client); } - async fetchMergeRequests() { return this.projectState.fetchMergeRequests(this.props.client); } - async fetchModifiedFiles() { return this.projectState.fetchModifiedFiles(this.props.client); } - async fetchBranches() { return this.projectState.fetchBranches(this.props.client); } - async createGraphWebhook() { return this.projectState.createGraphWebhook(this.props.client); } - async stopCheckingWebhook() { this.projectState.stopCheckingWebhook(); } - async fetchGraphWebhook() { this.projectState.fetchGraphWebhook(this.props.client, this.props.user); } + async fetchReadme() { return this.projectCoordinator.fetchReadme(this.props.client); } + async fetchModifiedFiles() { return this.projectCoordinator.fetchModifiedFiles(this.props.client); } + async fetchBranches() { return this.projectCoordinator.fetchBranches(); } + async createGraphWebhook() { return this.projectCoordinator.createGraphWebhook(this.props.client); } + async fetchGraphWebhook() { this.projectCoordinator.fetchGraphWebhook(this.props.client, this.props.user); } async fetchProjectFilesTree() { - return this.projectState.fetchProjectFilesTree(this.props.client, this.cleanCurrentURL()); + return this.projectCoordinator.fetchProjectFilesTree(this.props.client, this.cleanCurrentURL()); } async setProjectOpenFolder(filePath) { - this.projectState.setProjectOpenFolder(this.props.client, filePath); + this.projectCoordinator.setProjectOpenFolder(this.props.client, filePath); } async fetchProjectDatasets(forceReFetch) { - return this.projectState.fetchProjectDatasets(this.props.client, forceReFetch); + return this.projectCoordinator.fetchProjectDatasets(this.props.client, forceReFetch); } async fetchProjectDatasetsFromKg() { - return this.projectState.fetchProjectDatasetsFromKg(this.props.client); + return this.projectCoordinator.fetchProjectDatasetsFromKg(this.props.client); } - async fetchGraphStatus() { return this.projectState.fetchGraphStatus(this.props.client); } - saveProjectLastNode(nodeData) { this.projectState.saveProjectLastNode(nodeData); } + async fetchGraphStatus() { return this.projectCoordinator.fetchGraphStatus(this.props.client); } + saveProjectLastNode(nodeData) { this.projectCoordinator.saveProjectLastNode(nodeData); } async fetchMigrationCheck() { this.projectState.fetchMigrationCheck(this.props.client); } @@ -283,25 +282,10 @@ class View extends Component { .replace(subUrls.fileContentUrl, ""); } - // TODO: move all .set actions to Project.state.js - checkGraphWebhook() { - // check if data are available -- may remove this? - if (this.projectState.get("core.available") !== true) { - this.projectState.set("webhook.possible", false); - return; - } - // check user permissions and fetch webhook status - const webhookCreator = this.projectState.get("visibility.accessLevel") >= ACCESS_LEVELS.MAINTAINER ? - true : - false; - this.projectState.set("webhook.possible", webhookCreator); - if (webhookCreator) - this.projectState.fetchGraphWebhookStatus(this.props.client); - - } + checkGraphWebhook() { this.projectCoordinator.checkGraphWebhook(this.props.client); } isGraphReady() { - const webhookStatus = this.projectState.get("webhook"); + const webhookStatus = this.projectCoordinator.get("webhook"); return webhookStatus.status || (webhookStatus.created && webhookStatus.stop) || webhookStatus.progress === GraphIndexingStatus.MAX_VALUE; } @@ -311,7 +295,7 @@ class View extends Component { // return false until data are available if (!featured.fetched) return false; - return featured.starred.map((project) => project.id).indexOf(this.projectState.get("core.id")) >= 0; + return featured.starred.map((project) => project.id).indexOf(this.projectCoordinator.get("metadata.id")) >= 0; } getSubUrls() { @@ -364,23 +348,22 @@ class View extends Component { } subComponents(projectId, ownProps) { - const visibility = this.projectState.get("visibility"); - const isPrivate = visibility && visibility.level === "private"; - const accessLevel = visibility.accessLevel; - const externalUrl = this.projectState.get("core.external_url"); - const httpProjectUrl = this.projectState.get("system.http_url"); + const isPrivate = this.projectCoordinator.get("metadata.visibility") === "private"; + const accessLevel = this.projectCoordinator.get("metadata.accessLevel"); + const externalUrl = this.projectCoordinator.get("metadata.externalUrl"); + const httpProjectUrl = this.projectCoordinator.get("metadata.httpUrl"); const updateProjectView = this.forceUpdate.bind(this); - const filesTree = this.projectState.get("filesTree"); - const datasets = this.projectState.get("core.datasets"); - const graphProgress = this.projectState.get("webhook.progress"); - const maintainer = visibility.accessLevel >= ACCESS_LEVELS.MAINTAINER ? + const filesTree = this.projectCoordinator.get("filesTree"); + const datasets = this.projectCoordinator.get("datasets.core.datasets"); + const graphProgress = this.projectCoordinator.get("webhook.progress"); + const maintainer = accessLevel >= ACCESS_LEVELS.MAINTAINER ? true : false; - const forkedData = this.projectState.get("system.forked_from_project"); + const forkedData = this.projectCoordinator.get("forkedFromProject"); const forked = (forkedData != null && Object.keys(forkedData).length > 0) ? true : false; - const projectPathWithNamespace = this.projectState.get("core.path_with_namespace"); + const projectPathWithNamespace = this.projectCoordinator.get("metadata.pathWithNamespace"); // Access to the project state could be given to the subComponents by connecting them here to // the projectStore. This is not yet necessary. const subUrls = this.getSubUrls(); @@ -388,8 +371,8 @@ class View extends Component { ...ownProps, projectId, accessLevel, externalUrl, filesTree, projectPathWithNamespace, datasets }; const branches = { - all: this.projectState.get("system.branches"), - fetch: () => { this.fetchBranches(); } + all: this.projectCoordinator.get("branches"), + fetch: () => { this.projectCoordinator.fetchBranches(); } }; const pathComponents = splitProjectSubRoute(this.props.match.url); @@ -427,31 +410,31 @@ class View extends Component { maintainer={maintainer} forked={forked} launchNotebookUrl={subUrls.launchNotebookUrl} - projectNamespace={this.projectState.get("core.namespace_path")} - projectPathOnly={this.projectState.get("core.project_path")} + projectNamespace={this.projectCoordinator.get("metadata.namespace")} + projectPathOnly={this.projectCoordinator.get("metadata.path")} branches={branches} hashElement={filesTree !== undefined ? filesTree.hash[p.match.params.filePath] : undefined} gitFilePath={p.location.pathname.replace(pathComponents.baseUrl + "/files/lineage/", "")} history={this.props.history} - branch={this.projectState.get("core.default_branch")} />, + branch={this.projectCoordinator.get("metadata.defaultBranch")} />, fileView: (p) => , + branch={this.projectCoordinator.get("metadata.defaultBranch")} //this can be changed + defaultBranch={this.projectCoordinator.get("metadata.defaultBranch")} />, datasetView: (p, projectInsideKg) => , editDataset: (p) => , importDataset: (p) => { - this.projectState.setTags(this.props.client, tags); + this.projectCoordinator.setTags(this.props.client, tags); }, onProjectDescriptionChange: (description) => { - this.projectState.setDescription(this.props.client, description); + this.projectCoordinator.setDescription(this.props.client, description); }, onAvatarChange: (avatarFile) => { - return this.projectState.setAvatar(this.props.client, avatarFile); + return this.projectCoordinator.setAvatar(this.props.client, avatarFile); }, onStar: () => { const starred = this.getStarred(); - return this.projectState.star(this.props.client, starred).then((project) => { + return this.projectCoordinator.star(this.props.client, starred).then((project) => { // we know it worked, we can manually change star status without querying APIs if (project && project.star_count != null) { // first update the list of starred project, otherwise this.getStarred returns wrong this.projectsCoordinator.updateStarred(project, !starred); - this.projectState.setStars(project.star_count); + this.projectCoordinator.setStars(project.star_count); } return true; }); @@ -588,13 +571,8 @@ class View extends Component { fetchOverviewData: () => { return this.fetchReadme(); }, - fetchMrSuggestions: async () => { - await this.fetchMergeRequests(); - this.fetchBranches(); - }, fetchFiles: () => { this.fetchProjectFilesTree(); - //this.fetchModifiedFiles(); }, fetchDatasets: (forceReFetch) => { this.fetchProjectDatasetsFromKg(); @@ -614,7 +592,7 @@ class View extends Component { return this.fetchGraphStatus(); }, fetchBranches: () => { - return this.fetchBranches(); + return this.projectCoordinator.fetchBranches(); }, onMigrateProject: (params) => { return this.migrateProject(params); @@ -623,15 +601,17 @@ class View extends Component { mapStateToProps(state, ownProps) { const pathComponents = splitProjectSubRoute(ownProps.match.url); - const internalId = this.projectState.get("core.id") || parseInt(ownProps.match.params.id, 10); + const internalId = this.projectCoordinator.get("metadata.id") || parseInt(ownProps.match.params.id, 10); const starred = this.getStarred(); - const settingsReadOnly = state.visibility.accessLevel < ACCESS_LEVELS.MAINTAINER; - const externalUrl = this.projectState.get("core.external_url"); - const canCreateMR = state.visibility.accessLevel >= ACCESS_LEVELS.DEVELOPER; + const accessLevel = this.projectCoordinator.get("metadata.accessLevel"); + const settingsReadOnly = accessLevel < ACCESS_LEVELS.MAINTAINER; + const externalUrl = this.projectCoordinator.get("metadata.externalUrl"); + const canCreateMR = accessLevel >= ACCESS_LEVELS.DEVELOPER; const isGraphReady = this.isGraphReady(); return { ...this.projectState.get(), + ...this.projectCoordinator.get(), ...ownProps, projectPathWithNamespace: pathComponents.projectPathWithNamespace, projectId: pathComponents.projectId, @@ -652,7 +632,6 @@ class View extends Component { const props = { ...this.props, ...this.eventHandlers, - store: this.projectState.reduxStore, projectCoordinator: this.projectCoordinator }; return ; @@ -715,10 +694,9 @@ function withProjectMapped(MappingComponent, features = [], passProps = true) { const projectCoordinator = this.props.projectCoordinator; const mapFunction = mapProjectFeatures(projectCoordinator, features); const MappedComponent = connect(mapFunction.bind(this))(MappingComponent); - const store = projectCoordinator.model.reduxStore; const props = passProps ? this.props : {}; - return (); + return (); } }; } diff --git a/client/src/project/Project.present.js b/client/src/project/Project.present.js index 5c368fb79d..1f4895bcd8 100644 --- a/client/src/project/Project.present.js +++ b/client/src/project/Project.present.js @@ -209,14 +209,14 @@ class ForkProjectModal extends Component { } function ProjectIdentifier(props) { - const forkedFromText = (props.forkedFromLink == null) ? - null : - [" ", forked, " from ", props.forkedFromLink]; + const forkedFromText = (isForkedFromProject(props.forkedFromProject)) ? + {" "}forked{" from "} {props.forkedFromLink} : + null; const forkedFrom = (forkedFromText) ? {forkedFromText}
: null; - const projectId = props.core.path_with_namespace; - const projectTitle = props.core.title; + const projectId = props.metadata.pathWithNamespace; + const projectTitle = props.metadata.title; return ( @@ -230,7 +230,7 @@ function ProjectIdentifier(props) { template_update_possible={props.migration.template_update_possible} docker_update_possible={props.migration.docker_update_possible} />{projectTitle} - + {projectId} {forkedFrom} @@ -281,7 +281,7 @@ class ProjectViewHeaderOverview extends Component { } render() { - const system = this.props.system; + const metadata = this.props.metadata; let starElement; let starText; @@ -306,7 +306,7 @@ class ProjectViewHeaderOverview extends Component { const gitlabIDEUrl = this.props.externalUrl !== "" && this.props.externalUrl.includes("/gitlab/") ? this.props.externalUrl.replace("/gitlab/", "/gitlab/-/ide/project/") : null; const description = ; const forkProjectDisabled = this.props.visibility.accessLevel < ACCESS_LEVELS.REPORTER @@ -317,9 +317,9 @@ class ProjectViewHeaderOverview extends Component { - { this.props.core.avatar_url ? + { this.props.metadata.avatarUrl ?
-
: null } @@ -334,8 +334,8 @@ class ProjectViewHeaderOverview extends Component { history={this.props.history} model={this.props.model} notifications={this.props.notifications} - title={this.props.core && this.props.core.title ? this.props.core.title : ""} - id={this.props.core && this.props.core.id ? this.props.core.id : 0} + title={this.props.metadata && this.props.metadata.title ? this.props.metadata.title : ""} + id={this.props.metadata && this.props.metadata.id ? this.props.metadata.id : 0} forkProjectDisabled={forkProjectDisabled} /> @@ -356,7 +356,7 @@ class ProjectViewHeaderOverview extends Component { + style={{ cursor: "default" }}>{metadata.starCount} - { this.props.system.tag_list.length > 0 ? + { this.props.metadata.tagList.length > 0 ?
- +
: null }
- +
@@ -397,18 +397,22 @@ function StartSessionButton(props) { ); } +function isForkedFromProject(forkedFromProject) { + return forkedFromProject && Object.keys(forkedFromProject).length > 0; +} + +function ForkedFromLink({ forkedFromProject, projectsUrl }) { + if (!isForkedFromProject(forkedFromProject)) return null; + return + {forkedFromProject.pathWithNamespace || "no title"} + ; +} + class ProjectViewHeader extends Component { render() { - let forkedFromLink = null; - if (this.props.system.forked_from_project != null && - Object.keys(this.props.system.forked_from_project).length > 0) { - const forkedFrom = this.props.system.forked_from_project; - const projectsUrl = this.props.projectsUrl; - forkedFromLink = - {forkedFrom.metadata.core.path_with_namespace || "no title"} - ; - } - + const forkedFromLink = ; return ; } } @@ -482,13 +486,13 @@ class ProjectViewReadme extends Component { README.md @@ -497,15 +501,7 @@ class ProjectViewReadme extends Component { } function ProjectViewGeneral(props) { - let forkedFromLink = null; - if (props.system.forked_from_project != null && - Object.keys(props.system.forked_from_project).length > 0) { - const forkedFrom = props.system.forked_from_project; - const projectsUrl = props.projectsUrl; - forkedFromLink = - {forkedFrom.metadata.core.path_with_namespace || "no title"} - ; - } + const forkedFromLink = ; return } /> @@ -606,17 +602,17 @@ class ProjectViewOverview extends Component { class ProjectDatasetsNav extends Component { render() { - const allDatasets = this.props.core.datasets || []; + const allDatasets = this.props.datasets.core.datasets || []; if (allDatasets.length === 0) return null; return ; } @@ -756,18 +752,18 @@ function ProjectViewDatasets(props) { />; useEffect(()=>{ - const loading = props.core.datasets === SpecialPropVal.UPDATING; + const loading = props.datasets.core === SpecialPropVal.UPDATING; if (loading) return; props.fetchDatasets(props.location.state && props.location.state.reload); props.fetchGraphStatus(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const loading = props.core.datasets === SpecialPropVal.UPDATING || props.core.datasets === undefined; + const loading = props.datasets.core === SpecialPropVal.UPDATING || props.datasets.core === undefined; if (loading) return ; - if (props.core.datasets.error) { + if (props.datasets.core.error) { return There was an error fetching the datasets, please try : ; return
@@ -202,8 +202,8 @@ class RepositoryUrls extends Component { - - + +
diff --git a/client/src/project/settings/ProjectSettings.test.js b/client/src/project/settings/ProjectSettings.test.js index 62cfd44bac..f894f8d3a2 100644 --- a/client/src/project/settings/ProjectSettings.test.js +++ b/client/src/project/settings/ProjectSettings.test.js @@ -54,8 +54,7 @@ describe("rendering", () => { it("renders ProjectSettingsGeneral", async () => { const props = { - core: {}, - system: {} + metadata: {} }; const div = document.createElement("div"); diff --git a/client/src/project/shared/ProjectTag.container.js b/client/src/project/shared/ProjectTag.container.js index 437625f24f..4f572d5ead 100644 --- a/client/src/project/shared/ProjectTag.container.js +++ b/client/src/project/shared/ProjectTag.container.js @@ -58,7 +58,7 @@ class ProjectTags extends Component { } static tagListString(props) { - const tagList = sortedTagList(props.tag_list); + const tagList = sortedTagList(props.tagList); return tagList.join(", "); } diff --git a/client/src/utils/EnhancedState.js b/client/src/utils/EnhancedState.js index baa5978a5a..4ec561c104 100644 --- a/client/src/utils/EnhancedState.js +++ b/client/src/utils/EnhancedState.js @@ -33,7 +33,8 @@ function createStore(reducer, name = "renku") { window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize... - name + name, + trace: true, traceLimit: 25 }) : compose; const enhancer = composeEnhancers( diff --git a/client/src/utils/UIComponents.js b/client/src/utils/UIComponents.js index 9de893eb28..385239d89d 100644 --- a/client/src/utils/UIComponents.js +++ b/client/src/utils/UIComponents.js @@ -823,8 +823,11 @@ function RefreshButton(props) { * @param {string} props.label text next to the arrow */ function GoBackButton(props) { + const linkClasses = (props.className) ? + props.className + " link-rk-text text-decoration-none" : + "link-rk-text text-decoration-none"; return - + {props.label} ; diff --git a/client/src/utils/formgenerator/FormGenerator.container.js b/client/src/utils/formgenerator/FormGenerator.container.js index 7a7dc8c3a4..43b75a74be 100644 --- a/client/src/utils/formgenerator/FormGenerator.container.js +++ b/client/src/utils/formgenerator/FormGenerator.container.js @@ -34,13 +34,24 @@ import { FormGeneratorCoordinator } from "./FormGenerator.state"; import FormPanel from "./FormGenerator.present"; import _ from "lodash"; +function locationToLocationHash(loc) { return "uid_" + simpleHash(loc); } + +function mapStateToProps(state, props) { + const currentDraft = state.formGenerator.formDrafts[props.locationHash]; + return { + draft: currentDraft, + ...props + }; +} + +const VisibleFormGenerator = connect(mapStateToProps)(FormPanel); class FormGenerator extends Component { constructor(props) { super(props); this.model = props.modelTop.subModel("formGenerator"); - this.locationHash = "uid_" + simpleHash(props.formLocation); + this.locationHash = locationToLocationHash(props.formLocation); this.coordinator = new FormGeneratorCoordinator(props.client, this.model, props.formLocation, this.locationHash); this.handlers = { addDraft: this.addDraft.bind(this), @@ -131,26 +142,18 @@ class FormGenerator extends Component { return this.coordinator.getFormDraftInternalValuesProperty(fieldName, property); } - mapStateToProps(state) { - const currentDraft = state.formGenerator.formDrafts[this.locationHash]; - const [inputs, setInputs, setSubmit] = useForm(this.props.submitCallback, this.handlers, currentDraft); - return { - handlers: this.handlers, - draft: currentDraft, - modelValues: this.getDraft(), - inputs: inputs, - setInputs: setInputs, - setSubmit: setSubmit, - loading: this.getDraft() === undefined - }; - } - - render() { - const VisibleFormGenerator = connect(this.mapStateToProps.bind(this))(FormPanel); + const draft = this.model.get("formDrafts")[this.locationHash]; + const [inputs, setInputs, setSubmit] = useForm(this.props.submitCallback, this.handlers, draft); return (); }