mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 16:35:45 +01:00
scm-ui: new repository layout
This commit is contained in:
124
scm-ui/ui-webapp/src/containers/App.js
Normal file
124
scm-ui/ui-webapp/src/containers/App.js
Normal file
@@ -0,0 +1,124 @@
|
||||
// @flow
|
||||
import React, { Component } from "react";
|
||||
import Main from "./Main";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import {
|
||||
fetchMe,
|
||||
isAuthenticated,
|
||||
getMe,
|
||||
isFetchMePending,
|
||||
getFetchMeFailure
|
||||
} from "../modules/auth";
|
||||
|
||||
import {
|
||||
PrimaryNavigation,
|
||||
Loading,
|
||||
ErrorPage,
|
||||
Footer,
|
||||
Header
|
||||
} from "@scm-manager/ui-components";
|
||||
import type { Links, Me } from "@scm-manager/ui-types";
|
||||
import {
|
||||
getFetchIndexResourcesFailure,
|
||||
getLinks,
|
||||
getMeLink,
|
||||
isFetchIndexResourcesPending
|
||||
} from "../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
me: Me,
|
||||
authenticated: boolean,
|
||||
error: Error,
|
||||
loading: boolean,
|
||||
links: Links,
|
||||
meLink: string,
|
||||
|
||||
// dispatcher functions
|
||||
fetchMe: (link: string) => void,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class App extends Component<Props> {
|
||||
componentDidMount() {
|
||||
if (this.props.meLink) {
|
||||
this.props.fetchMe(this.props.meLink);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
me,
|
||||
loading,
|
||||
error,
|
||||
authenticated,
|
||||
links,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
let content;
|
||||
const navigation = authenticated ? (
|
||||
<PrimaryNavigation
|
||||
links={links}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
content = <Loading />;
|
||||
} else if (error) {
|
||||
content = (
|
||||
<ErrorPage
|
||||
title={t("app.error.title")}
|
||||
subtitle={t("app.error.subtitle")}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = <Main authenticated={authenticated} links={links} />;
|
||||
}
|
||||
return (
|
||||
<div className="App">
|
||||
<Header>{navigation}</Header>
|
||||
{content}
|
||||
<Footer me={me} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
fetchMe: (link: string) => dispatch(fetchMe(link))
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const authenticated = isAuthenticated(state);
|
||||
const me = getMe(state);
|
||||
const loading =
|
||||
isFetchMePending(state) || isFetchIndexResourcesPending(state);
|
||||
const error =
|
||||
getFetchMeFailure(state) || getFetchIndexResourcesFailure(state);
|
||||
const links = getLinks(state);
|
||||
const meLink = getMeLink(state);
|
||||
return {
|
||||
authenticated,
|
||||
me,
|
||||
loading,
|
||||
error,
|
||||
links,
|
||||
meLink
|
||||
};
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("commons")(App))
|
||||
);
|
||||
159
scm-ui/ui-webapp/src/containers/ChangeUserPassword.js
Normal file
159
scm-ui/ui-webapp/src/containers/ChangeUserPassword.js
Normal file
@@ -0,0 +1,159 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import {
|
||||
ErrorNotification,
|
||||
InputField,
|
||||
Notification,
|
||||
PasswordConfirmation,
|
||||
SubmitButton
|
||||
} from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
import type { Me } from "@scm-manager/ui-types";
|
||||
import { changePassword } from "../modules/changePassword";
|
||||
|
||||
type Props = {
|
||||
me: Me,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = {
|
||||
oldPassword: string,
|
||||
password: string,
|
||||
loading: boolean,
|
||||
error?: Error,
|
||||
passwordChanged: boolean,
|
||||
passwordValid: boolean
|
||||
};
|
||||
|
||||
class ChangeUserPassword extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
oldPassword: "",
|
||||
password: "",
|
||||
loading: false,
|
||||
passwordConfirmationError: false,
|
||||
validatePasswordError: false,
|
||||
validatePassword: "",
|
||||
passwordChanged: false,
|
||||
passwordValid: false
|
||||
};
|
||||
}
|
||||
|
||||
setLoadingState = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loading: true
|
||||
});
|
||||
};
|
||||
|
||||
setErrorState = (error: Error) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
error: error,
|
||||
loading: false
|
||||
});
|
||||
};
|
||||
|
||||
setSuccessfulState = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loading: false,
|
||||
passwordChanged: true,
|
||||
oldPassword: "",
|
||||
password: ""
|
||||
});
|
||||
};
|
||||
|
||||
submit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
if (this.state.password) {
|
||||
const { oldPassword, password } = this.state;
|
||||
this.setLoadingState();
|
||||
changePassword(this.props.me._links.password.href, oldPassword, password)
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
this.setErrorState(result.error);
|
||||
} else {
|
||||
this.setSuccessfulState();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.setErrorState(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
isValid = () => {
|
||||
return this.state.oldPassword && this.state.passwordValid;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { loading, passwordChanged, error } = this.state;
|
||||
|
||||
let message = null;
|
||||
|
||||
if (passwordChanged) {
|
||||
message = (
|
||||
<Notification
|
||||
type={"success"}
|
||||
children={t("password.changedSuccessfully")}
|
||||
onClose={() => this.onClose()}
|
||||
/>
|
||||
);
|
||||
} else if (error) {
|
||||
message = <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={this.submit}>
|
||||
{message}
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<InputField
|
||||
label={t("password.currentPassword")}
|
||||
type="password"
|
||||
onChange={oldPassword =>
|
||||
this.setState({ ...this.state, oldPassword })
|
||||
}
|
||||
value={this.state.oldPassword ? this.state.oldPassword : ""}
|
||||
helpText={t("password.currentPasswordHelpText")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PasswordConfirmation
|
||||
passwordChanged={this.passwordChanged}
|
||||
key={this.state.passwordChanged ? "changed" : "unchanged"}
|
||||
/>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<SubmitButton
|
||||
disabled={!this.isValid()}
|
||||
loading={loading}
|
||||
label={t("password.submit")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
passwordChanged = (password: string, passwordValid: boolean) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
password,
|
||||
passwordValid: !!password && passwordValid
|
||||
});
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
passwordChanged: false
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("commons")(ChangeUserPassword);
|
||||
101
scm-ui/ui-webapp/src/containers/Index.js
Normal file
101
scm-ui/ui-webapp/src/containers/Index.js
Normal file
@@ -0,0 +1,101 @@
|
||||
// @flow
|
||||
import React, { Component } from "react";
|
||||
import App from "./App";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
import { Loading, ErrorBoundary } from "@scm-manager/ui-components";
|
||||
import {
|
||||
fetchIndexResources,
|
||||
getFetchIndexResourcesFailure,
|
||||
getLinks,
|
||||
isFetchIndexResourcesPending
|
||||
} from "../modules/indexResource";
|
||||
import PluginLoader from "./PluginLoader";
|
||||
import type { IndexResources } from "@scm-manager/ui-types";
|
||||
import ScrollToTop from "./ScrollToTop";
|
||||
import IndexErrorPage from "./IndexErrorPage";
|
||||
|
||||
type Props = {
|
||||
error: Error,
|
||||
loading: boolean,
|
||||
indexResources: IndexResources,
|
||||
|
||||
// dispatcher functions
|
||||
fetchIndexResources: () => void,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = {
|
||||
pluginsLoaded: boolean
|
||||
};
|
||||
|
||||
class Index extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
pluginsLoaded: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchIndexResources();
|
||||
}
|
||||
|
||||
pluginLoaderCallback = () => {
|
||||
this.setState({
|
||||
pluginsLoaded: true
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { indexResources, loading, error } = this.props;
|
||||
const { pluginsLoaded } = this.state;
|
||||
|
||||
if (error) {
|
||||
return <IndexErrorPage error={error}/>;
|
||||
} else if (loading || !indexResources) {
|
||||
return <Loading />;
|
||||
} else {
|
||||
return (
|
||||
<ErrorBoundary fallback={IndexErrorPage}>
|
||||
<ScrollToTop>
|
||||
<PluginLoader
|
||||
loaded={pluginsLoaded}
|
||||
callback={this.pluginLoaderCallback}
|
||||
>
|
||||
<App />
|
||||
</PluginLoader>
|
||||
</ScrollToTop>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
fetchIndexResources: () => dispatch(fetchIndexResources())
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const loading = isFetchIndexResourcesPending(state);
|
||||
const error = getFetchIndexResourcesFailure(state);
|
||||
const indexResources = getLinks(state);
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
indexResources
|
||||
};
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("commons")(Index))
|
||||
);
|
||||
26
scm-ui/ui-webapp/src/containers/IndexErrorPage.js
Normal file
26
scm-ui/ui-webapp/src/containers/IndexErrorPage.js
Normal file
@@ -0,0 +1,26 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate, type TFunction } from "react-i18next";
|
||||
import { ErrorPage } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
error: Error,
|
||||
t: TFunction
|
||||
}
|
||||
|
||||
class IndexErrorPage extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { error, t } = this.props;
|
||||
return (
|
||||
<ErrorPage
|
||||
title={t("app.error.title")}
|
||||
subtitle={t("app.error.subtitle")}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate("commons")(IndexErrorPage);
|
||||
100
scm-ui/ui-webapp/src/containers/Login.js
Normal file
100
scm-ui/ui-webapp/src/containers/Login.js
Normal file
@@ -0,0 +1,100 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { Redirect, withRouter } from "react-router-dom";
|
||||
import {
|
||||
login,
|
||||
isAuthenticated,
|
||||
isLoginPending,
|
||||
getLoginFailure
|
||||
} from "../modules/auth";
|
||||
import { connect } from "react-redux";
|
||||
import { getLoginLink, getLoginInfoLink } from "../modules/indexResource";
|
||||
import LoginInfo from "../components/LoginInfo";
|
||||
import classNames from "classnames";
|
||||
import injectSheet from "react-jss";
|
||||
|
||||
const styles = {
|
||||
section: {
|
||||
paddingTop: "2em"
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
authenticated: boolean,
|
||||
loading: boolean,
|
||||
error?: Error,
|
||||
link: string,
|
||||
loginInfoLink?: string,
|
||||
|
||||
// dispatcher props
|
||||
login: (link: string, username: string, password: string) => void,
|
||||
|
||||
// context props
|
||||
classes: any,
|
||||
t: string => string,
|
||||
from: any,
|
||||
location: any
|
||||
};
|
||||
|
||||
class Login extends React.Component<Props> {
|
||||
|
||||
handleLogin = (username: string, password: string): void => {
|
||||
const { link, login } = this.props;
|
||||
login(link, username, password);
|
||||
};
|
||||
|
||||
renderRedirect = () => {
|
||||
const { from } = this.props.location.state || { from: { pathname: "/" } };
|
||||
return <Redirect to={from}/>;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { authenticated, classes, ...restProps } = this.props;
|
||||
|
||||
if (authenticated) {
|
||||
return this.renderRedirect();
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={classNames("hero", classes.section )}>
|
||||
<div className="hero-body">
|
||||
<div className="container">
|
||||
<div className="columns is-centered">
|
||||
<LoginInfo loginHandler={this.handleLogin} {...restProps} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const authenticated = isAuthenticated(state);
|
||||
const loading = isLoginPending(state);
|
||||
const error = getLoginFailure(state);
|
||||
const link = getLoginLink(state);
|
||||
const loginInfoLink = getLoginInfoLink(state);
|
||||
return {
|
||||
authenticated,
|
||||
loading,
|
||||
error,
|
||||
link,
|
||||
loginInfoLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
login: (loginLink: string, username: string, password: string) =>
|
||||
dispatch(login(loginLink, username, password))
|
||||
};
|
||||
};
|
||||
|
||||
const StyledLogin = injectSheet(styles)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Login)
|
||||
);
|
||||
export default withRouter(StyledLogin);
|
||||
77
scm-ui/ui-webapp/src/containers/Logout.js
Normal file
77
scm-ui/ui-webapp/src/containers/Logout.js
Normal file
@@ -0,0 +1,77 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
import { Redirect } from "react-router-dom";
|
||||
|
||||
import {
|
||||
logout,
|
||||
isAuthenticated,
|
||||
isLogoutPending,
|
||||
getLogoutFailure, isRedirecting
|
||||
} from "../modules/auth";
|
||||
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
||||
import { getLogoutLink } from "../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
authenticated: boolean,
|
||||
loading: boolean,
|
||||
redirecting: boolean,
|
||||
error: Error,
|
||||
logoutLink: string,
|
||||
|
||||
// dispatcher functions
|
||||
logout: (link: string) => void,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class Logout extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.logout(this.props.logoutLink);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { authenticated, redirecting, loading, error, t } = this.props;
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorPage
|
||||
title={t("logout.error.title")}
|
||||
subtitle={t("logout.error.subtitle")}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
} else if (loading || authenticated || redirecting) {
|
||||
return <Loading />;
|
||||
} else {
|
||||
return <Redirect to="/login" />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const authenticated = isAuthenticated(state);
|
||||
const loading = isLogoutPending(state);
|
||||
const redirecting = isRedirecting(state);
|
||||
const error = getLogoutFailure(state);
|
||||
const logoutLink = getLogoutLink(state);
|
||||
return {
|
||||
authenticated,
|
||||
loading,
|
||||
redirecting,
|
||||
error,
|
||||
logoutLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
logout: (link: string) => dispatch(logout(link))
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("commons")(Logout));
|
||||
138
scm-ui/ui-webapp/src/containers/Main.js
Normal file
138
scm-ui/ui-webapp/src/containers/Main.js
Normal file
@@ -0,0 +1,138 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
|
||||
import { Redirect, Route, Switch, withRouter } from "react-router-dom";
|
||||
import type { Links } from "@scm-manager/ui-types";
|
||||
|
||||
import Overview from "../repos/containers/Overview";
|
||||
import Users from "../users/containers/Users";
|
||||
import Login from "../containers/Login";
|
||||
import Logout from "../containers/Logout";
|
||||
|
||||
import { ProtectedRoute } from "@scm-manager/ui-components";
|
||||
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
|
||||
import CreateUser from "../users/containers/CreateUser";
|
||||
import SingleUser from "../users/containers/SingleUser";
|
||||
import RepositoryRoot from "../repos/containers/RepositoryRoot";
|
||||
import Create from "../repos/containers/Create";
|
||||
|
||||
import Groups from "../groups/containers/Groups";
|
||||
import SingleGroup from "../groups/containers/SingleGroup";
|
||||
import CreateGroup from "../groups/containers/CreateGroup";
|
||||
|
||||
import Admin from "../admin/containers/Admin";
|
||||
|
||||
import Profile from "./Profile";
|
||||
|
||||
type Props = {
|
||||
authenticated?: boolean,
|
||||
links: Links
|
||||
};
|
||||
|
||||
class Main extends React.Component<Props> {
|
||||
render() {
|
||||
const { authenticated, links } = this.props;
|
||||
const redirectUrlFactory = binder.getExtension("main.redirect", this.props);
|
||||
let url = "/repos/";
|
||||
if (redirectUrlFactory) {
|
||||
url = redirectUrlFactory(this.props);
|
||||
}
|
||||
return (
|
||||
<div className="main">
|
||||
<Switch>
|
||||
<Redirect exact from="/" to={url} />
|
||||
<Route exact path="/login" component={Login} />
|
||||
<Route path="/logout" component={Logout} />
|
||||
<Redirect exact strict from="/repos" to="/repos/" />
|
||||
<ProtectedRoute
|
||||
exact
|
||||
path="/repos/"
|
||||
component={Overview}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
exact
|
||||
path="/repos/create"
|
||||
component={Create}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
exact
|
||||
path="/repos/:page"
|
||||
component={Overview}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
path="/repo/:namespace/:name"
|
||||
component={RepositoryRoot}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<Redirect exact strict from="/users" to="/users/" />
|
||||
<ProtectedRoute
|
||||
exact
|
||||
path="/users/"
|
||||
component={Users}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
authenticated={authenticated}
|
||||
path="/users/create"
|
||||
component={CreateUser}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
exact
|
||||
path="/users/:page"
|
||||
component={Users}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
authenticated={authenticated}
|
||||
path="/user/:name"
|
||||
component={SingleUser}
|
||||
/>
|
||||
<Redirect exact strict from="/groups" to="/groups/" />
|
||||
<ProtectedRoute
|
||||
exact
|
||||
path="/groups/"
|
||||
component={Groups}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
authenticated={authenticated}
|
||||
path="/group/:name"
|
||||
component={SingleGroup}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
authenticated={authenticated}
|
||||
path="/groups/create"
|
||||
component={CreateGroup}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
exact
|
||||
path="/groups/:page"
|
||||
component={Groups}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
path="/admin"
|
||||
component={Admin}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
path="/me"
|
||||
component={Profile}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<ExtensionPoint
|
||||
name="main.route"
|
||||
renderAll={true}
|
||||
props={{ authenticated, links }}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(Main);
|
||||
125
scm-ui/ui-webapp/src/containers/PluginLoader.js
Normal file
125
scm-ui/ui-webapp/src/containers/PluginLoader.js
Normal file
@@ -0,0 +1,125 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { apiClient, Loading } from "@scm-manager/ui-components";
|
||||
import { getUiPluginsLink } from "../modules/indexResource";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
type Props = {
|
||||
loaded: boolean,
|
||||
children: React.Node,
|
||||
link: string,
|
||||
callback: () => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
message: string
|
||||
};
|
||||
|
||||
type Plugin = {
|
||||
name: string,
|
||||
bundles: string[]
|
||||
};
|
||||
|
||||
class PluginLoader extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
message: "booting"
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { loaded } = this.props;
|
||||
if (!loaded) {
|
||||
this.setState({
|
||||
message: "loading plugin information"
|
||||
});
|
||||
|
||||
this.getPlugins(this.props.link);
|
||||
}
|
||||
}
|
||||
|
||||
getPlugins = (link: string): Promise<any> => {
|
||||
return apiClient
|
||||
.get(link)
|
||||
.then(response => response.text())
|
||||
.then(JSON.parse)
|
||||
.then(pluginCollection => pluginCollection._embedded.plugins)
|
||||
.then(this.loadPlugins)
|
||||
.then(this.props.callback);
|
||||
};
|
||||
|
||||
loadPlugins = (plugins: Plugin[]) => {
|
||||
this.setState({
|
||||
message: "loading plugins"
|
||||
});
|
||||
|
||||
const promises = [];
|
||||
const sortedPlugins = plugins.sort(comparePluginsByName);
|
||||
for (let plugin of sortedPlugins) {
|
||||
promises.push(this.loadPlugin(plugin));
|
||||
}
|
||||
return promises.reduce((chain, current) => {
|
||||
return chain.then(chainResults => {
|
||||
return current.then(currentResult => [...chainResults, currentResult]);
|
||||
});
|
||||
}, Promise.resolve([]));
|
||||
};
|
||||
|
||||
loadPlugin = (plugin: Plugin) => {
|
||||
this.setState({
|
||||
message: `loading ${plugin.name}`
|
||||
});
|
||||
|
||||
const promises = [];
|
||||
for (let bundle of plugin.bundles) {
|
||||
promises.push(this.loadBundle(bundle));
|
||||
}
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
loadBundle = (bundle: string) => {
|
||||
return fetch(bundle, {
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
Cache: "no-cache",
|
||||
// identify the request as ajax request
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
return response.text();
|
||||
})
|
||||
.then(script => {
|
||||
// TODO is this safe???
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(script); // NOSONAR
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loaded } = this.props;
|
||||
const { message } = this.state;
|
||||
if (loaded) {
|
||||
return <div>{this.props.children}</div>;
|
||||
}
|
||||
return <Loading message={message} />;
|
||||
}
|
||||
}
|
||||
const comparePluginsByName = (a: Plugin, b: Plugin) => {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
const mapStateToProps = state => {
|
||||
const link = getUiPluginsLink(state);
|
||||
return {
|
||||
link
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(PluginLoader);
|
||||
123
scm-ui/ui-webapp/src/containers/Profile.js
Normal file
123
scm-ui/ui-webapp/src/containers/Profile.js
Normal file
@@ -0,0 +1,123 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { Route, withRouter } from "react-router-dom";
|
||||
import { getMe } from "../modules/auth";
|
||||
import { compose } from "redux";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
import type { Me } from "@scm-manager/ui-types";
|
||||
import {
|
||||
ErrorPage,
|
||||
Page,
|
||||
Navigation,
|
||||
SubNavigation,
|
||||
Section,
|
||||
NavLink
|
||||
} from "@scm-manager/ui-components";
|
||||
import ChangeUserPassword from "./ChangeUserPassword";
|
||||
import ProfileInfo from "./ProfileInfo";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
|
||||
type Props = {
|
||||
me: Me,
|
||||
|
||||
// Context props
|
||||
t: string => string,
|
||||
match: any
|
||||
};
|
||||
type State = {};
|
||||
|
||||
class Profile extends React.Component<Props, State> {
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const url = this.matchedUrl();
|
||||
|
||||
const { me, t } = this.props;
|
||||
|
||||
if (!me) {
|
||||
return (
|
||||
<ErrorPage
|
||||
title={t("profile.error-title")}
|
||||
subtitle={t("profile.error-subtitle")}
|
||||
error={{
|
||||
name: t("profile.error"),
|
||||
message: t("profile.error-message")
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const extensionProps = {
|
||||
me,
|
||||
url
|
||||
};
|
||||
|
||||
return (
|
||||
<Page title={me.displayName}>
|
||||
<div className="columns">
|
||||
<div className="column is-three-quarters">
|
||||
<Route path={url} exact render={() => <ProfileInfo me={me} />} />
|
||||
<Route
|
||||
path={`${url}/settings/password`}
|
||||
render={() => <ChangeUserPassword me={me} />}
|
||||
/>
|
||||
<ExtensionPoint
|
||||
name="profile.route"
|
||||
props={extensionProps}
|
||||
renderAll={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="column">
|
||||
<Navigation>
|
||||
<Section label={t("profile.navigationLabel")}>
|
||||
<NavLink
|
||||
to={`${url}`}
|
||||
icon="fas fa-info-circle"
|
||||
label={t("profile.informationNavLink")}
|
||||
/>
|
||||
<SubNavigation
|
||||
to={`${url}/settings/password`}
|
||||
label={t("profile.settingsNavLink")}
|
||||
>
|
||||
<NavLink
|
||||
to={`${url}/settings/password`}
|
||||
label={t("profile.changePasswordNavLink")}
|
||||
/>
|
||||
<ExtensionPoint
|
||||
name="profile.setting"
|
||||
props={extensionProps}
|
||||
renderAll={true}
|
||||
/>
|
||||
</SubNavigation>
|
||||
</Section>
|
||||
</Navigation>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
me: getMe(state)
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
translate("commons"),
|
||||
connect(mapStateToProps),
|
||||
withRouter
|
||||
)(Profile);
|
||||
89
scm-ui/ui-webapp/src/containers/ProfileInfo.js
Normal file
89
scm-ui/ui-webapp/src/containers/ProfileInfo.js
Normal file
@@ -0,0 +1,89 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import type { Me } from "@scm-manager/ui-types";
|
||||
import {
|
||||
MailLink,
|
||||
AvatarWrapper,
|
||||
AvatarImage
|
||||
} from "@scm-manager/ui-components";
|
||||
import { compose } from "redux";
|
||||
import { translate } from "react-i18next";
|
||||
import injectSheet from "react-jss";
|
||||
|
||||
type Props = {
|
||||
me: Me,
|
||||
|
||||
// Context props
|
||||
classes: any,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
const styles = {
|
||||
spacing: {
|
||||
padding: "0 !important"
|
||||
}
|
||||
};
|
||||
|
||||
class ProfileInfo extends React.Component<Props> {
|
||||
render() {
|
||||
const { me, t } = this.props;
|
||||
return (
|
||||
<div className="media">
|
||||
<AvatarWrapper>
|
||||
<figure className="media-left">
|
||||
<p className="image is-64x64">
|
||||
<AvatarImage person={me} />
|
||||
</p>
|
||||
</figure>
|
||||
</AvatarWrapper>
|
||||
<div className="media-content">
|
||||
<table className="table content">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{t("profile.username")}</th>
|
||||
<td>{me.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("profile.displayName")}</th>
|
||||
<td>{me.displayName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t("profile.mail")}</th>
|
||||
<td>
|
||||
<MailLink address={me.mail} />
|
||||
</td>
|
||||
</tr>
|
||||
{this.renderGroups()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderGroups() {
|
||||
const { me, t, classes } = this.props;
|
||||
|
||||
let groups = null;
|
||||
if (me.groups.length > 0) {
|
||||
groups = (
|
||||
<tr>
|
||||
<th>{t("profile.groups")}</th>
|
||||
<td className={classes.spacing}>
|
||||
<ul>
|
||||
{me.groups.map(group => {
|
||||
return <li>{group}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
injectSheet(styles),
|
||||
translate("commons")
|
||||
)(ProfileInfo);
|
||||
23
scm-ui/ui-webapp/src/containers/ScrollToTop.js
Normal file
23
scm-ui/ui-webapp/src/containers/ScrollToTop.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
|
||||
type Props = {
|
||||
location: any,
|
||||
children: any
|
||||
}
|
||||
|
||||
class ScrollToTop extends React.Component<Props> {
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.location.pathname !== prevProps.location.pathname) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(ScrollToTop);
|
||||
Reference in New Issue
Block a user