diff --git a/scm-plugins/scm-git-plugin/src/main/js/GitAvatar.js b/scm-plugins/scm-git-plugin/src/main/js/GitAvatar.js index 3bb4b376e8..6af17b95bf 100644 --- a/scm-plugins/scm-git-plugin/src/main/js/GitAvatar.js +++ b/scm-plugins/scm-git-plugin/src/main/js/GitAvatar.js @@ -7,7 +7,8 @@ type Props = { class GitAvatar extends React.Component { render() { - return Git Logo; + // TODO we have to use Image from ui-components + return Git Logo; } } diff --git a/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.js b/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.js index a1c63ef119..9c4d808df1 100644 --- a/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.js +++ b/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.js @@ -7,7 +7,8 @@ type Props = { class HgAvatar extends React.Component { render() { - return Mercurial Logo; + // TODO we have to use Image from ui-components + return Mercurial Logo; } } diff --git a/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.js b/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.js index f274a336c8..4a6ba0fc94 100644 --- a/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.js +++ b/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.js @@ -7,7 +7,8 @@ type Props = { class SvnAvatar extends React.Component { render() { - return Subversion Logo; + // TODO we have to use Image from ui-components + return Subversion Logo; } } diff --git a/scm-ui/package.json b/scm-ui/package.json index 267e2926ce..ed86702874 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -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", diff --git a/scm-ui/public/index.html b/scm-ui/public/index.html index c76e55cac3..b253786fe3 100644 --- a/scm-ui/public/index.html +++ b/scm-ui/public/index.html @@ -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/ --> - - + + - + SCM-Manager @@ -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`. --> - - + + + diff --git a/scm-ui/src/apiclient.js b/scm-ui/src/apiclient.js index 0ef0182120..7bf3232260 100644 --- a/scm-ui/src/apiclient.js +++ b/scm-ui/src/apiclient.js @@ -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 { diff --git a/scm-ui/src/components/Image.js b/scm-ui/src/components/Image.js new file mode 100644 index 0000000000..a81754dac9 --- /dev/null +++ b/scm-ui/src/components/Image.js @@ -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 { + render() { + const { src, alt, className } = this.props; + return {alt}; + } +} + +export default Image; diff --git a/scm-ui/src/components/Loading.js b/scm-ui/src/components/Loading.js index f76e1be05b..0a472ecb02 100644 --- a/scm-ui/src/components/Loading.js +++ b/scm-ui/src/components/Loading.js @@ -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 { return (
- {t("loading.alt")} string @@ -9,7 +10,7 @@ type Props = { class Logo extends React.Component { render() { const { t } = this.props; - return {t("logo.alt")}; + return {t("logo.alt")}; } } diff --git a/scm-ui/src/components/PluginLoader.js b/scm-ui/src/components/PluginLoader.js index a41bf8231f..cdb48127fc 100644 --- a/scm-ui/src/components/PluginLoader.js +++ b/scm-ui/src/components/PluginLoader.js @@ -62,11 +62,7 @@ class PluginLoader extends React.Component { const promises = []; for (let bundle of plugin.bundles) { - // skip old bundles - // TODO remove old bundles - if (bundle.indexOf("/") !== 0) { - promises.push(this.loadBundle(bundle)); - } + promises.push(this.loadBundle(bundle)); } return Promise.all(promises); }; diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index 8735f1bdcb..f807bb01bb 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -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 {

{t("login.subtitle")}

- {t("login.logo-alt")} { return (

- Logo + Logo

); diff --git a/scm-ui/src/urls.js b/scm-ui/src/urls.js new file mode 100644 index 0000000000..74e5388e16 --- /dev/null +++ b/scm-ui/src/urls.js @@ -0,0 +1,6 @@ +// @flow +export const contextPath = window.ctxPath || ""; + +export function withContextPath(path: string) { + return contextPath + path; +} diff --git a/scm-ui/yarn.lock b/scm-ui/yarn.lock index 5f5e1f05ed..7dadc52924 100644 --- a/scm-ui/yarn.lock +++ b/scm-ui/yarn.lock @@ -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" diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java index 7ef2cbded3..10ae79b5bf 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java @@ -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 getScriptResources(PluginWrapper wrapper) { + Set 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); + } + } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java index 0807c600ff..a76c39dd69 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginResource.java @@ -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; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UIRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UIRootResourceTest.java index 4e8c2d43b8..99a1435923 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UIRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UIRootResourceTest.java @@ -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)); }