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> {
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> {
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> {
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"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.3",
"@scm-manager/ui-bundler": "^0.0.4",
"babel-eslint": "^8.2.6",
"enzyme": "^3.3.0",
"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
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json">
<link rel="shortcut icon" href="/favicon.ico">
<link rel="manifest" href="/scm/manifest.json">
<link rel="shortcut icon" href="/scm/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
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.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<base href="/">
<base href="/scm">
<title>SCM-Manager</title>
</head>
<body>
@@ -37,7 +37,10 @@
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script src="vendor.bundle.js"></script>
<script src="scm-ui.bundle.js"></script>
<script>
window.ctxPath = "/scm";
</script>
<script src="/scm/vendor.bundle.js"></script>
<script src="/scm/scm-ui.bundle.js"></script>
</body>
</html>

View File

@@ -1,7 +1,5 @@
// @flow
// get api base url from environment
const apiUrl = process.env.API_URL || process.env.PUBLIC_URL || "";
import { contextPath } from "./urls";
export const NOT_FOUND_ERROR = Error("not found");
export const UNAUTHORIZED_ERROR = Error("unauthorized");
@@ -34,7 +32,7 @@ export function createUrl(url: string) {
if (url.indexOf("/") !== 0) {
urlWithStartingSlash = "/" + urlWithStartingSlash;
}
return `${apiUrl}/api/rest/v2${urlWithStartingSlash}`;
return `${contextPath}/api/rest/v2${urlWithStartingSlash}`;
}
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 { translate } from "react-i18next";
import injectSheet from "react-jss";
import Image from "./Image";
const styles = {
wrapper: {
@@ -35,7 +36,7 @@ class Loading extends React.Component<Props> {
return (
<div className={classes.wrapper}>
<div className={classes.loading}>
<img
<Image
className={classes.image}
src="/images/loading.svg"
alt={t("loading.alt")}

View File

@@ -1,6 +1,7 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import Image from "./Image";
type Props = {
t: string => string
@@ -9,7 +10,7 @@ type Props = {
class Logo extends React.Component<Props> {
render() {
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,12 +62,8 @@ class PluginLoader extends React.Component<Props, State> {
const promises = [];
for (let bundle of plugin.bundles) {
// skip old bundles
// TODO remove old bundles
if (bundle.indexOf("/") !== 0) {
promises.push(this.loadBundle(bundle));
}
}
return Promise.all(promises);
};

View File

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

View File

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

View File

@@ -16,11 +16,11 @@ import createReduxStore from "./createReduxStore";
import { ConnectedRouter } from "react-router-redux";
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)
const history: BrowserHistory = createHistory({
basename: publicUrl
basename: contextPath
});
// Add the reducer to your store on the `router` key

View File

@@ -2,6 +2,7 @@
import React from "react";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import type { Repository } from "../../types/Repositories";
import Image from "../../../components/Image";
type Props = {
repository: Repository
@@ -13,7 +14,7 @@ class RepositoryAvatar extends React.Component<Props> {
return (
<p className="image is-64x64">
<ExtensionPoint name="repos.repository-avatar" props={{ repository }}>
<img src="/images/blib.jpg" alt="Logo" />
<Image src="/images/blib.jpg" alt="Logo" />
</ExtensionPoint>
</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"
url-template "^2.0.8"
"@scm-manager/ui-bundler@^0.0.3":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.3.tgz#06f99d8b17e9aa1bb6e69c2732160e1f46724c3c"
"@scm-manager/ui-bundler@^0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.4.tgz#0191a026d25b826692bccbc2b76d550388dd5528"
dependencies:
"@babel/core" "^7.0.0-rc.2"
"@babel/plugin-proposal-class-properties" "^7.0.0-rc.2"
@@ -720,6 +720,7 @@
budo "^11.3.2"
colors "^1.3.1"
commander "^2.17.1"
fast-xml-parser "^3.12.0"
jest "^23.5.0"
jest-junit "^5.1.0"
node-mkdirs "^0.0.1"
@@ -2830,6 +2831,12 @@ fast-levenshtein@~2.0.4:
version "2.0.6"
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:
version "2.0.0"
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"
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:
version "1.7.3"
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;
import com.google.common.base.Strings;
import de.otto.edison.hal.Links;
import sonia.scm.plugin.PluginWrapper;
import sonia.scm.util.HttpUtil;
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;
public class UIPluginDtoMapper {
private ResourceLinks resourceLinks;
private final ResourceLinks resourceLinks;
private final HttpServletRequest request;
@Inject
public UIPluginDtoMapper(ResourceLinks resourceLinks) {
public UIPluginDtoMapper(ResourceLinks resourceLinks, HttpServletRequest request) {
this.resourceLinks = resourceLinks;
this.request = request;
}
public UIPluginDto map(PluginWrapper plugin) {
UIPluginDto dto = new UIPluginDto(
plugin.getPlugin().getInformation().getName(),
plugin.getPlugin().getResources().getScriptResources()
getScriptResources(plugin)
);
Links.Builder linksBuilder = linkingTo()
@@ -31,4 +40,22 @@ public class UIPluginDtoMapper {
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.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import de.otto.edison.hal.HalRepresentation;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.PluginWrapper;
import sonia.scm.web.VndMediaType;

View File

@@ -12,14 +12,13 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.api.rest.resources.PluginResource;
import sonia.scm.plugin.*;
import sonia.scm.web.VndMediaType;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.List;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_OK;
@@ -36,12 +35,15 @@ public class UIRootResourceTest {
@Mock
private PluginLoader pluginLoader;
@Mock
private HttpServletRequest request;
private final URI baseUri = URI.create("/");
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@Before
public void setUpRestService() {
UIPluginDtoMapper mapper = new UIPluginDtoMapper(resourceLinks);
UIPluginDtoMapper mapper = new UIPluginDtoMapper(resourceLinks, request);
UIPluginDtoCollectionMapper collectionMapper = new UIPluginDtoCollectionMapper(resourceLinks, mapper);
UIPluginResource pluginResource = new UIPluginResource(pluginLoader, collectionMapper, mapper);
@@ -149,6 +151,24 @@ public class UIRootResourceTest {
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) {
when(pluginLoader.getInstalledPlugins()).thenReturn(Lists.newArrayList(plugins));
}