restore context path support

This commit is contained in:
Sebastian Sdorra
2018-08-27 15:47:02 +02:00
parent b09c46abcf
commit e6e1c5871a
19 changed files with 132 additions and 36 deletions

View File

@@ -7,7 +7,8 @@ type Props = {
class GitAvatar extends React.Component<Props> { class GitAvatar extends React.Component<Props> {
render() { render() {
return <img src="/images/git-logo.png" alt="Git Logo" />; // TODO we have to use Image from ui-components
return <img src="/scm/images/git-logo.png" alt="Git Logo" />;
} }
} }

View File

@@ -7,7 +7,8 @@ type Props = {
class HgAvatar extends React.Component<Props> { class HgAvatar extends React.Component<Props> {
render() { render() {
return <img src="/images/hg-logo.png" alt="Mercurial Logo" />; // TODO we have to use Image from ui-components
return <img src="/scm/images/hg-logo.png" alt="Mercurial Logo" />;
} }
} }

View File

@@ -7,7 +7,8 @@ type Props = {
class SvnAvatar extends React.Component<Props> { class SvnAvatar extends React.Component<Props> {
render() { render() {
return <img src="/images/svn-logo.gif" alt="Subversion Logo" />; // TODO we have to use Image from ui-components
return <img src="/scm/images/svn-logo.gif" alt="Subversion Logo" />;
} }
} }

View File

@@ -40,7 +40,7 @@
"pre-commit": "jest && flow && eslint src" "pre-commit": "jest && flow && eslint src"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.3", "@scm-manager/ui-bundler": "^0.0.4",
"babel-eslint": "^8.2.6", "babel-eslint": "^8.2.6",
"enzyme": "^3.3.0", "enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1", "enzyme-adapter-react-16": "^1.1.1",

View File

@@ -8,8 +8,8 @@
manifest.json provides metadata used when your web app is added to the manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
--> -->
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/scm/manifest.json">
<link rel="shortcut icon" href="/favicon.ico"> <link rel="shortcut icon" href="/scm/favicon.ico">
<!-- <!--
Notice the use of %PUBLIC_URL% in the tags above. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.
@@ -19,7 +19,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<base href="/"> <base href="/scm">
<title>SCM-Manager</title> <title>SCM-Manager</title>
</head> </head>
<body> <body>
@@ -37,7 +37,10 @@
To begin the development, run `npm start` or `yarn start`. To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
<script src="vendor.bundle.js"></script> <script>
<script src="scm-ui.bundle.js"></script> window.ctxPath = "/scm";
</script>
<script src="/scm/vendor.bundle.js"></script>
<script src="/scm/scm-ui.bundle.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,7 +1,5 @@
// @flow // @flow
import { contextPath } from "./urls";
// get api base url from environment
const apiUrl = process.env.API_URL || process.env.PUBLIC_URL || "";
export const NOT_FOUND_ERROR = Error("not found"); export const NOT_FOUND_ERROR = Error("not found");
export const UNAUTHORIZED_ERROR = Error("unauthorized"); export const UNAUTHORIZED_ERROR = Error("unauthorized");
@@ -34,7 +32,7 @@ export function createUrl(url: string) {
if (url.indexOf("/") !== 0) { if (url.indexOf("/") !== 0) {
urlWithStartingSlash = "/" + urlWithStartingSlash; urlWithStartingSlash = "/" + urlWithStartingSlash;
} }
return `${apiUrl}/api/rest/v2${urlWithStartingSlash}`; return `${contextPath}/api/rest/v2${urlWithStartingSlash}`;
} }
class ApiClient { class ApiClient {

View File

@@ -0,0 +1,18 @@
//@flow
import React from "react";
import { withContextPath } from "../urls";
type Props = {
src: string,
alt: string,
className: any
};
class Image extends React.Component<Props> {
render() {
const { src, alt, className } = this.props;
return <img className={className} src={withContextPath(src)} alt={alt} />;
}
}
export default Image;

View File

@@ -2,6 +2,7 @@
import React from "react"; import React from "react";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import injectSheet from "react-jss"; import injectSheet from "react-jss";
import Image from "./Image";
const styles = { const styles = {
wrapper: { wrapper: {
@@ -35,7 +36,7 @@ class Loading extends React.Component<Props> {
return ( return (
<div className={classes.wrapper}> <div className={classes.wrapper}>
<div className={classes.loading}> <div className={classes.loading}>
<img <Image
className={classes.image} className={classes.image}
src="/images/loading.svg" src="/images/loading.svg"
alt={t("loading.alt")} alt={t("loading.alt")}

View File

@@ -1,6 +1,7 @@
//@flow //@flow
import React from "react"; import React from "react";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import Image from "./Image";
type Props = { type Props = {
t: string => string t: string => string
@@ -9,7 +10,7 @@ type Props = {
class Logo extends React.Component<Props> { class Logo extends React.Component<Props> {
render() { render() {
const { t } = this.props; const { t } = this.props;
return <img src="images/logo.png" alt={t("logo.alt")} />; return <Image src="/images/logo.png" alt={t("logo.alt")} />;
} }
} }

View File

@@ -62,11 +62,7 @@ class PluginLoader extends React.Component<Props, State> {
const promises = []; const promises = [];
for (let bundle of plugin.bundles) { for (let bundle of plugin.bundles) {
// skip old bundles promises.push(this.loadBundle(bundle));
// TODO remove old bundles
if (bundle.indexOf("/") !== 0) {
promises.push(this.loadBundle(bundle));
}
} }
return Promise.all(promises); return Promise.all(promises);
}; };

View File

@@ -16,6 +16,7 @@ import { SubmitButton } from "../components/buttons";
import classNames from "classnames"; import classNames from "classnames";
import ErrorNotification from "../components/ErrorNotification"; import ErrorNotification from "../components/ErrorNotification";
import Image from "../components/Image";
const styles = { const styles = {
avatar: { avatar: {
@@ -105,7 +106,7 @@ class Login extends React.Component<Props, State> {
<p className="subtitle">{t("login.subtitle")}</p> <p className="subtitle">{t("login.subtitle")}</p>
<div className={classNames("box", classes.avatarSpacing)}> <div className={classNames("box", classes.avatarSpacing)}>
<figure className={classes.avatar}> <figure className={classes.avatar}>
<img <Image
className={classes.avatarImage} className={classes.avatarImage}
src="/images/blib.jpg" src="/images/blib.jpg"
alt={t("login.logo-alt")} alt={t("login.logo-alt")}

View File

@@ -2,9 +2,9 @@ import i18n from "i18next";
import Backend from "i18next-fetch-backend"; import Backend from "i18next-fetch-backend";
import LanguageDetector from "i18next-browser-languagedetector"; import LanguageDetector from "i18next-browser-languagedetector";
import { reactI18nextModule } from "react-i18next"; import { reactI18nextModule } from "react-i18next";
import { withContextPath } from "./urls";
const loadPath = const loadPath = withContextPath("/locales/{{lng}}/{{ns}}.json");
(process.env.PUBLIC_URL || "") + "/locales/{{lng}}/{{ns}}.json";
// TODO load locales for moment // TODO load locales for moment

View File

@@ -16,11 +16,11 @@ import createReduxStore from "./createReduxStore";
import { ConnectedRouter } from "react-router-redux"; import { ConnectedRouter } from "react-router-redux";
import PluginLoader from "./components/PluginLoader"; import PluginLoader from "./components/PluginLoader";
const publicUrl: string = process.env.PUBLIC_URL || ""; import { contextPath } from "./urls";
// Create a history of your choosing (we're using a browser history in this case) // Create a history of your choosing (we're using a browser history in this case)
const history: BrowserHistory = createHistory({ const history: BrowserHistory = createHistory({
basename: publicUrl basename: contextPath
}); });
// Add the reducer to your store on the `router` key // Add the reducer to your store on the `router` key

View File

@@ -2,6 +2,7 @@
import React from "react"; import React from "react";
import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { ExtensionPoint } from "@scm-manager/ui-extensions";
import type { Repository } from "../../types/Repositories"; import type { Repository } from "../../types/Repositories";
import Image from "../../../components/Image";
type Props = { type Props = {
repository: Repository repository: Repository
@@ -13,7 +14,7 @@ class RepositoryAvatar extends React.Component<Props> {
return ( return (
<p className="image is-64x64"> <p className="image is-64x64">
<ExtensionPoint name="repos.repository-avatar" props={{ repository }}> <ExtensionPoint name="repos.repository-avatar" props={{ repository }}>
<img src="/images/blib.jpg" alt="Logo" /> <Image src="/images/blib.jpg" alt="Logo" />
</ExtensionPoint> </ExtensionPoint>
</p> </p>
); );

6
scm-ui/src/urls.js Normal file
View File

@@ -0,0 +1,6 @@
// @flow
export const contextPath = window.ctxPath || "";
export function withContextPath(path: string) {
return contextPath + path;
}

View File

@@ -703,9 +703,9 @@
node-fetch "^2.1.1" node-fetch "^2.1.1"
url-template "^2.0.8" url-template "^2.0.8"
"@scm-manager/ui-bundler@^0.0.3": "@scm-manager/ui-bundler@^0.0.4":
version "0.0.3" version "0.0.4"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.3.tgz#06f99d8b17e9aa1bb6e69c2732160e1f46724c3c" resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.4.tgz#0191a026d25b826692bccbc2b76d550388dd5528"
dependencies: dependencies:
"@babel/core" "^7.0.0-rc.2" "@babel/core" "^7.0.0-rc.2"
"@babel/plugin-proposal-class-properties" "^7.0.0-rc.2" "@babel/plugin-proposal-class-properties" "^7.0.0-rc.2"
@@ -720,6 +720,7 @@
budo "^11.3.2" budo "^11.3.2"
colors "^1.3.1" colors "^1.3.1"
commander "^2.17.1" commander "^2.17.1"
fast-xml-parser "^3.12.0"
jest "^23.5.0" jest "^23.5.0"
jest-junit "^5.1.0" jest-junit "^5.1.0"
node-mkdirs "^0.0.1" node-mkdirs "^0.0.1"
@@ -2830,6 +2831,12 @@ fast-levenshtein@~2.0.4:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
fast-xml-parser@^3.12.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.12.0.tgz#84ddcd98ca005f94e99af3ac387adc32ffb239d8"
dependencies:
nimnjs "^1.3.2"
fb-watchman@^2.0.0: fb-watchman@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
@@ -5040,6 +5047,21 @@ nice-try@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4"
nimn-date-parser@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/nimn-date-parser/-/nimn-date-parser-1.0.0.tgz#4ce55d1fd5ea206bbe82b76276f7b7c582139351"
nimn_schema_builder@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/nimn_schema_builder/-/nimn_schema_builder-1.1.0.tgz#b370ccf5b647d66e50b2dcfb20d0aa12468cd247"
nimnjs@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/nimnjs/-/nimnjs-1.3.2.tgz#a6a877968d87fad836375a4f616525e55079a5ba"
dependencies:
nimn-date-parser "^1.0.0"
nimn_schema_builder "^1.0.0"
node-fetch@^1.0.1: node-fetch@^1.0.1:
version "1.7.3" version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"

View File

@@ -1,25 +1,34 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import com.google.common.base.Strings;
import de.otto.edison.hal.Links; import de.otto.edison.hal.Links;
import sonia.scm.plugin.PluginWrapper; import sonia.scm.plugin.PluginWrapper;
import sonia.scm.util.HttpUtil;
import javax.inject.Inject; import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import static de.otto.edison.hal.Links.linkingTo; import static de.otto.edison.hal.Links.linkingTo;
public class UIPluginDtoMapper { public class UIPluginDtoMapper {
private ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;
private final HttpServletRequest request;
@Inject @Inject
public UIPluginDtoMapper(ResourceLinks resourceLinks) { public UIPluginDtoMapper(ResourceLinks resourceLinks, HttpServletRequest request) {
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
this.request = request;
} }
public UIPluginDto map(PluginWrapper plugin) { public UIPluginDto map(PluginWrapper plugin) {
UIPluginDto dto = new UIPluginDto( UIPluginDto dto = new UIPluginDto(
plugin.getPlugin().getInformation().getName(), plugin.getPlugin().getInformation().getName(),
plugin.getPlugin().getResources().getScriptResources() getScriptResources(plugin)
); );
Links.Builder linksBuilder = linkingTo() Links.Builder linksBuilder = linkingTo()
@@ -31,4 +40,22 @@ public class UIPluginDtoMapper {
return dto; return dto;
} }
private Set<String> getScriptResources(PluginWrapper wrapper) {
Set<String> scriptResources = wrapper.getPlugin().getResources().getScriptResources();
if (scriptResources != null) {
return scriptResources.stream()
.map(this::addContextPath)
.collect(Collectors.toSet());
}
return Collections.emptySet();
}
private String addContextPath(String resource) {
String ctxPath = request.getContextPath();
if (Strings.isNullOrEmpty(ctxPath)) {
return resource;
}
return HttpUtil.append(ctxPath, resource);
}
} }

View File

@@ -3,7 +3,6 @@ package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint; import com.webcohesion.enunciate.metadata.rs.TypeHint;
import de.otto.edison.hal.HalRepresentation;
import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.PluginWrapper; import sonia.scm.plugin.PluginWrapper;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;

View File

@@ -12,14 +12,13 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.api.rest.resources.PluginResource;
import sonia.scm.plugin.*; import sonia.scm.plugin.*;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.servlet.http.HttpServletRequest;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_OK;
@@ -36,12 +35,15 @@ public class UIRootResourceTest {
@Mock @Mock
private PluginLoader pluginLoader; private PluginLoader pluginLoader;
@Mock
private HttpServletRequest request;
private final URI baseUri = URI.create("/"); private final URI baseUri = URI.create("/");
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@Before @Before
public void setUpRestService() { public void setUpRestService() {
UIPluginDtoMapper mapper = new UIPluginDtoMapper(resourceLinks); UIPluginDtoMapper mapper = new UIPluginDtoMapper(resourceLinks, request);
UIPluginDtoCollectionMapper collectionMapper = new UIPluginDtoCollectionMapper(resourceLinks, mapper); UIPluginDtoCollectionMapper collectionMapper = new UIPluginDtoCollectionMapper(resourceLinks, mapper);
UIPluginResource pluginResource = new UIPluginResource(pluginLoader, collectionMapper, mapper); UIPluginResource pluginResource = new UIPluginResource(pluginLoader, collectionMapper, mapper);
@@ -149,6 +151,24 @@ public class UIRootResourceTest {
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"" + uri + "\"}")); assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"" + uri + "\"}"));
} }
@Test
public void shouldHaveBundleWithContextPath() throws Exception {
when(request.getContextPath()).thenReturn("/scm");
mockPlugins(mockPlugin("awesome", "Awesome", createPluginResources("my/bundle.js")));
String uri = "/v2/ui/plugins/awesome";
MockHttpRequest request = MockHttpRequest.get(uri);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(SC_OK, response.getStatus());
System.out.println();
assertTrue(response.getContentAsString().contains("/scm/my/bundle.js"));
}
private void mockPlugins(PluginWrapper... plugins) { private void mockPlugins(PluginWrapper... plugins) {
when(pluginLoader.getInstalledPlugins()).thenReturn(Lists.newArrayList(plugins)); when(pluginLoader.getInstalledPlugins()).thenReturn(Lists.newArrayList(plugins));
} }