Skip to content

feat(client): add WebSocket channel setup #2013

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@
"nullable",
"objectstores",
"onloadend",
"onopen",
"papermill",
"pathname",
"pdfjs",
Expand Down
1,524 changes: 1,133 additions & 391 deletions client/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"history": "^4.10.1",
"jest-canvas-mock": "^2.1.2",
"jest-localstorage-mock": "^2.4.19",
"jest-websocket-mock": "^2.4.0",
"mermaid": "^9.1.3",
"node-fetch": "^2.6.7",
"react-docgen-typescript": "^2.2.2",
Expand Down
8 changes: 8 additions & 0 deletions client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { Loader } from "./utils/components/Loader";
import { AddDataset } from "./dataset/addtoproject/DatasetAdd.container";
import { DatasetCoordinator } from "./dataset/Dataset.state";
import AppContext from "./utils/context/appContext";
import { setupWebSocket } from "./websocket";

export const ContainerWrap = ({ children }) => {
return <div className="container-xxl py-4 mt-2 renku-container">{children}</div>;
Expand Down Expand Up @@ -202,6 +203,13 @@ class App extends Component {
// Setup authentication listeners and notifications
LoginHelper.setupListener();
LoginHelper.triggerNotifications(this.notifications);

// Setup WebSocket channel
let webSocketUrl = props.client.uiserverUrl + "/ws";
if (webSocketUrl.startsWith("http"))
webSocketUrl = "ws" + webSocketUrl.substring(4);
// ? adding a small delay to allow session cookie to be saved to local browser before sending requests
setTimeout(() => setupWebSocket(webSocketUrl, this.props.model), 1000);
}

render() {
Expand Down
14 changes: 0 additions & 14 deletions client/src/api-client/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,6 @@
*/

function addEnvironmentMethods(client) {
/**
* Get the versions of the RenkuLab components.
*/
client.getComponentsVersion = async () => {
const urlApi = `${client.baseUrl}/versions`;
let headers = client.getBasicHeaders();
headers.append("Content-Type", "application/json");
headers.append("X-Requested-With", "XMLHttpRequest");
return client.clientFetch(urlApi, {
method: "GET",
headers: headers
}).then(resp => resp.data);
};

/**
* Get the version of the core service
*/
Expand Down
2 changes: 0 additions & 2 deletions client/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import App from "./App";
import APIClient from "./api-client";
import { LoginHelper } from "./authentication";
import { EnvironmentCoordinator } from "./environment";
import { pollComponentsVersion } from "./landing";
import { Maintenance } from "./Maintenance";
import { StateModel, globalSchema } from "./model";
import { pollStatuspage } from "./statuspage";
Expand Down Expand Up @@ -89,7 +88,6 @@ Promise.all([configFetch, privacyFetch]).then(valuesRead => {
// Set up polling
const statuspageId = params["STATUSPAGE_ID"];
pollStatuspage(statuspageId, model);
pollComponentsVersion(model.subModel("environment"), client);

// Retrieve service environment information
new EnvironmentCoordinator(client, model.subModel("environment")).fetchCoreServiceVersions();
Expand Down
4 changes: 2 additions & 2 deletions client/src/landing/AnonymousHome.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { Url } from "../utils/helpers/url";
import { StatuspageBanner } from "../statuspage";
import QuickNav from "../utils/components/quicknav";
import { RenkuToolbarHelpMenu, RenkuToolbarNotifications } from "./NavBar";
import { VersionsBanner } from "./NabBarWarnings";
import { NavBarWarnings } from "./NavBarWarnings";

import VisualHead from "./Assets/Visual_Head.svg";
import VisualDetail from "./Assets/Visual_Detail.svg";
Expand Down Expand Up @@ -86,7 +86,7 @@ function HomeHeader(props) {
<Col>
<StatuspageBanner siteStatusUrl={urlMap.siteStatusUrl} model={props.model}
location={{ pathname: Url.get(Url.pages.landing) }} />
<VersionsBanner model={props.model} uiShortSha={props.params["UI_SHORT_SHA"]} />
<NavBarWarnings model={props.model} uiShortSha={props.params["UI_SHORT_SHA"]} />
</Col>
</Row>
<header className="px-0 pt-2 pb-4 d-flex rk-anon-home">
Expand Down
6 changes: 3 additions & 3 deletions client/src/landing/NavBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import logo from "./logo.svg";
import { getActiveProjectPathWithNamespace, gitLabUrlFromProfileUrl } from "../utils/helpers/HelperFunctions";
import QuickNav from "../utils/components/quicknav";
import { Url } from "../utils/helpers/url";
import { VersionsBanner } from "./NabBarWarnings";
import { NavBarWarnings } from "./NavBarWarnings";
import { NotificationsMenu } from "../notifications";
import { LoginHelper } from "../authentication";
import { StatuspageBanner } from "../statuspage";
Expand Down Expand Up @@ -305,7 +305,7 @@ class LoggedInNavBar extends Component {
<StatuspageBanner siteStatusUrl={Url.get(Url.pages.help.status)}
model={this.props.model}
location={this.props.location} />
<VersionsBanner model={this.props.model} uiShortSha={this.props.params["UI_SHORT_SHA"]} />
<NavBarWarnings model={this.props.model} uiShortSha={this.props.params["UI_SHORT_SHA"]} />
</Fragment>
);
}
Expand Down Expand Up @@ -366,7 +366,7 @@ class AnonymousNavBar extends Component {
</Collapse>
</Navbar>
</header>
<VersionsBanner model={this.props.model} uiShortSha={this.props.params["UI_SHORT_SHA"]} />
<NavBarWarnings model={this.props.model} uiShortSha={this.props.params["UI_SHORT_SHA"]} />
</Fragment>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,61 +31,34 @@ import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
import { WarnAlert } from "../utils/components/Alert";


const versionUpdateInterval = 1000 * 60 * 5; // Update every 5 minutes

/**
* Poller function to keep the server components version up-to-date. It doesn't need to run often.
*
* @param {Object} model - current model object for the environments (sub-model of the global model)
* @param {*} client - API client
*/
function pollComponentsVersion(model, client) {
async function fetchVersions() {
model.setObject({ fetching: true });
const componentsVersion = await client.getComponentsVersion();
const environment = {
fetching: false,
fetched: new Date(),
data: componentsVersion
};
model.setObject(environment);
}
fetchVersions();

if (model.get("timeout"))
return null;
const idTimeout = setInterval(fetchVersions, versionUpdateInterval);
model.setObject({ timeout: idTimeout });
return null;
}

/**
* Container component for the warning banners
*/
function VersionsBanner(props) {
function NavBarWarnings(props) {
function mapStateToProps(state, ownProps) {
return { environment: state.stateModel.environment };
}

const VisibleBanner = connect(mapStateToProps)(VersionsBannerPresent);
const VisibleBanner = connect(mapStateToProps)(NavBarWarningsPresent);
return (<VisibleBanner store={props.model.reduxStore} uiShortSha={props.uiShortSha} />);
}

/**
* Presentational component for the warning banners
*/
function VersionsBannerPresent(props) {
const { environment, uiShortSha } = props;
function NavBarWarningsPresent(props) {
let { environment, uiShortSha } = props;
const { uiVersion } = environment;

// return when local ui version data is not available
if (!uiShortSha || uiShortSha.toLowerCase() === "development" || uiShortSha.toLowerCase() === "dev")
return null;

// return when remote ui version data is not available
if (!environment.fetched || !environment.data["ui-short-sha"])
if (!uiVersion.webSocket || !uiVersion.lastValue)
return null;

if (uiShortSha === environment.data["ui-short-sha"])
if (uiShortSha === uiVersion.lastValue)
return null;

return (
Expand All @@ -105,4 +78,4 @@ function VersionsBannerPresent(props) {
}


export { pollComponentsVersion, VersionsBanner };
export { NavBarWarnings };
3 changes: 1 addition & 2 deletions client/src/landing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,5 @@
import AnonymousHome from "./AnonymousHome";
import Landing from "./Landing";
import { RenkuNavBar, FooterNavbar, MaintenanceNavBar } from "./NavBar";
import { pollComponentsVersion } from "./NabBarWarnings";

export { AnonymousHome, Landing, FooterNavbar, MaintenanceNavBar, pollComponentsVersion, RenkuNavBar };
export { AnonymousHome, Landing, FooterNavbar, MaintenanceNavBar, RenkuNavBar };
3 changes: 2 additions & 1 deletion client/src/model/GlobalSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import { Schema, PropertyName as Prop } from "./index";
import {
datasetSchema, environmentSchema, formGeneratorSchema, newProjectSchema, notebooksSchema, notificationsSchema,
projectsSchema, projectSchema, statuspageSchema, userSchema
projectsSchema, projectSchema, statuspageSchema, userSchema, webSocketSchema
} from "./RenkuModels";

const globalSchema = new Schema({
Expand All @@ -40,6 +40,7 @@ const globalSchema = new Schema({
dataset: { [Prop.SCHEMA]: datasetSchema },
statuspage: { [Prop.SCHEMA]: statuspageSchema },
user: { [Prop.SCHEMA]: userSchema },
webSocket: { [Prop.SCHEMA]: webSocketSchema },
});

export { globalSchema };
28 changes: 23 additions & 5 deletions client/src/model/RenkuModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,21 @@ const newProjectSchema = new Schema({
}
});

const webSocketSchema = new Schema({
open: { [Prop.INITIAL]: false },
error: { [Prop.INITIAL]: false },
errorObject: { [Prop.INITIAL]: {} },
lastPing: { [Prop.INITIAL]: null },
lastReceived: { [Prop.INITIAL]: null },
reconnect: {
[Prop.SCHEMA]: new Schema({
retrying: { [Prop.INITIAL]: false },
attempts: { [Prop.INITIAL]: 0 },
lastTime: { [Prop.INITIAL]: null },
})
}
});

const projectSchema = new Schema({
branches: {
[Prop.SCHEMA]: new Schema({
Expand Down Expand Up @@ -651,10 +666,13 @@ const datasetImportFormSchema = new Schema({
});

const environmentSchema = new Schema({
fetched: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true },
fetching: { [Prop.INITIAL]: false, [Prop.MANDATORY]: true },
data: { [Prop.INITIAL]: {}, [Prop.MANDATORY]: true },
timeout: { [Prop.INITIAL]: null, [Prop.MANDATORY]: true },
uiVersion: {
[Prop.SCHEMA]: new Schema({
webSocket: { [Prop.INITIAL]: null },
lastValue: { [Prop.INITIAL]: null },
lastReceived: { [Prop.INITIAL]: null },
})
},
coreVersions: {
[Prop.SCHEMA]: new Schema({
available: { [Prop.INITIAL]: {}, [Prop.MANDATORY]: true },
Expand Down Expand Up @@ -704,5 +722,5 @@ const formGeneratorSchema = new Schema({
export {
datasetFormSchema, datasetSchema, datasetImportFormSchema, environmentSchema,
formGeneratorSchema, issueFormSchema, newProjectSchema, notebooksSchema, notificationsSchema,
projectSchema, projectsSchema, statuspageSchema, userSchema
projectSchema, projectsSchema, statuspageSchema, userSchema, webSocketSchema
};
60 changes: 60 additions & 0 deletions client/src/websocket/WsMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*!
* Copyright 2022 - Swiss Data Science Center (SDSC)
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
* Eidgenössische Technische Hochschule Zürich (ETHZ).
*
* 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.
*/

class WsMessage {
timestamp: Date;
type: string; // E.G. "init", "ping"
data: Record<string, unknown>;

constructor(data: string | Record<string, unknown>, type: string) {
this.timestamp = new Date();
if (typeof data === "string")
this.data = { message: data };
else
this.data = data;
this.type = type;
}

toString(): string {
return JSON.stringify({
timestamp: this.timestamp,
type: this.type,
data: this.data
});
}
}

interface WsServerMessage {
timestamp: Date;
type: string;
scope: string;
data: Record<string, unknown>;
}

function checkWsServerMessage(obj: any): obj is WsServerMessage { // eslint-disable-line
return (
"timestamp" in obj && obj.timestamp != null &&
"scope" in obj && obj.scope != null &&
"type" in obj && obj.type != null &&
"data" in obj && obj.data != null && typeof obj.data === "object"
);
}


export { checkWsServerMessage, WsMessage };
export type { WsServerMessage };
Loading