diff --git a/scm-core/src/main/java/sonia/scm/xml/XmlDateWithTimezoneAdapter.java b/scm-core/src/main/java/sonia/scm/xml/XmlUTCDateAdapter.java similarity index 95% rename from scm-core/src/main/java/sonia/scm/xml/XmlDateWithTimezoneAdapter.java rename to scm-core/src/main/java/sonia/scm/xml/XmlUTCDateAdapter.java index 97097b395b..f9f8e13bb6 100644 --- a/scm-core/src/main/java/sonia/scm/xml/XmlDateWithTimezoneAdapter.java +++ b/scm-core/src/main/java/sonia/scm/xml/XmlUTCDateAdapter.java @@ -28,7 +28,7 @@ import javax.xml.bind.annotation.adapters.XmlAdapter; import java.text.SimpleDateFormat; import java.util.Date; -public class XmlDateWithTimezoneAdapter extends XmlAdapter { +public class XmlUTCDateAdapter extends XmlAdapter { @Override public Date unmarshal(String date) throws Exception { SimpleDateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz"); diff --git a/scm-ui/ui-webapp/public/locales/de/admin.json b/scm-ui/ui-webapp/public/locales/de/admin.json index 620228df0b..6f8de6c7df 100644 --- a/scm-ui/ui-webapp/public/locales/de/admin.json +++ b/scm-ui/ui-webapp/public/locales/de/admin.json @@ -9,6 +9,12 @@ "info": { "title": "Administration", "currentAppVersion": "Aktuelle Software-Versionsnummer", + "newAppVersion": "Neue SCM-Manager Version verfügbar", + "newRelease": { + "title": "Neue SCM-Manager Version verfügbar", + "description": "Die neueste SCM-Manager Version {{version}} steht auf der offiziellen Webseite zum Download bereit.", + "downloadButton": "Neueste Version herunterladen" + }, "communityTitle": "Community Support", "communityIconAlt": "Community Support Icon", "communityInfo": "Das SCM-Manager Support-Team steht für allgemeine Fragen, die Meldung von Fehlern sowie Anfragen für Features gerne für Sie über die offiziellen Kanäle bereit.", diff --git a/scm-ui/ui-webapp/public/locales/en/admin.json b/scm-ui/ui-webapp/public/locales/en/admin.json index eae79e3a9c..ae404117fe 100644 --- a/scm-ui/ui-webapp/public/locales/en/admin.json +++ b/scm-ui/ui-webapp/public/locales/en/admin.json @@ -9,6 +9,12 @@ "info": { "title": "Administration", "currentAppVersion": "Current Application Version", + "newAppVersion": "New SCM-Manager version available", + "newRelease": { + "title": "New SCM-Manager version available", + "description": "Download the latest SCM-Manager version {{version}} from the official homepage.", + "downloadButton": "Get latest version" + }, "communityTitle": "Community Support", "communityIconAlt": "Community Support Icon", "communityInfo": "Contact the SCM-Manager support team for questions about SCM-Manager, to report bugs or to request features through the official channels.", diff --git a/scm-ui/ui-webapp/src/admin/containers/Admin.tsx b/scm-ui/ui-webapp/src/admin/containers/Admin.tsx index 98a308e4e3..e4a23dd7e6 100644 --- a/scm-ui/ui-webapp/src/admin/containers/Admin.tsx +++ b/scm-ui/ui-webapp/src/admin/containers/Admin.tsx @@ -36,7 +36,8 @@ import { SecondaryNavigation, SecondaryNavigationColumn, StateMenuContextProvider, - SubNavigation + SubNavigation, + urls } from "@scm-manager/ui-components"; import { getAvailablePluginsLink, getInstalledPluginsLink, getLinks } from "../../modules/indexResource"; import AdminDetails from "./AdminDetails"; @@ -45,7 +46,6 @@ import GlobalConfig from "./GlobalConfig"; import RepositoryRoles from "../roles/containers/RepositoryRoles"; import SingleRepositoryRole from "../roles/containers/SingleRepositoryRole"; import CreateRepositoryRole from "../roles/containers/CreateRepositoryRole"; -import { urls } from "@scm-manager/ui-components"; type Props = RouteComponentProps & WithTranslation & { @@ -55,7 +55,6 @@ type Props = RouteComponentProps & }; class Admin extends React.Component { - matchesRoles = (route: any) => { const url = urls.matchedUrl(this.props); const regex = new RegExp(`${url}/role/`); diff --git a/scm-ui/ui-webapp/src/admin/containers/AdminDetails.tsx b/scm-ui/ui-webapp/src/admin/containers/AdminDetails.tsx index 86eed31c49..d7db2b4c3a 100644 --- a/scm-ui/ui-webapp/src/admin/containers/AdminDetails.tsx +++ b/scm-ui/ui-webapp/src/admin/containers/AdminDetails.tsx @@ -25,18 +25,24 @@ import React from "react"; import { connect } from "react-redux"; import { WithTranslation, withTranslation } from "react-i18next"; import styled from "styled-components"; -import { Image, Loading, Subtitle, Title } from "@scm-manager/ui-components"; -import { getAppVersion } from "../../modules/indexResource"; +import { apiClient, ErrorNotification, Image, Loading, Subtitle, Title } from "@scm-manager/ui-components"; +import { getAppVersion, getReleaseInfoLink } from "../../modules/indexResource"; type Props = WithTranslation & { - loading: boolean; - error: Error; version: string; + releaseInfoLink?: string; }; -const NoBottomMarginSubtitle = styled(Subtitle)` - margin-bottom: 0.25rem !important; -`; +type State = { + loading: boolean; + error?: Error; + releaseInfo?: ReleaseInfo; +}; + +type ReleaseInfo = { + version: string; + link: string; +}; const BottomMarginDiv = styled.div` margin-bottom: 1.5rem; @@ -46,13 +52,43 @@ const BoxShadowBox = styled.div` box-shadow: 0 2px 3px rgba(40, 177, 232, 0.1), 0 0 0 2px rgba(40, 177, 232, 0.2); `; +const NoBottomMarginSubtitle = styled(Subtitle)` + margin-bottom: 0.25rem !important; +`; + const ImageWrapper = styled.div` padding: 0.2rem 0.4rem; `; -class AdminDetails extends React.Component { +class AdminDetails extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + loading: false + }; + } + + componentDidMount() { + const { releaseInfoLink } = this.props; + + if (releaseInfoLink) { + apiClient + .get(releaseInfoLink) + .then(r => r.json()) + .then(releaseInfo => this.setState({ releaseInfo })) + .then(() => this.setState({ loading: false })) + .catch(error => this.setState({ error })); + } + } + render() { - const { loading, t } = this.props; + const { version, t } = this.props; + const { loading, error, releaseInfo } = this.state; + + if (error) { + return ; + } if (loading) { return ; @@ -62,7 +98,33 @@ class AdminDetails extends React.Component { <> <NoBottomMarginSubtitle subtitle={t("admin.info.currentAppVersion")} /> - <BottomMarginDiv>{this.props.version}</BottomMarginDiv> + <BottomMarginDiv>{version}</BottomMarginDiv> + {releaseInfo && ( + <> + <BoxShadowBox className="box"> + <article className="media"> + <ImageWrapper className="media-left image is-96x96"> + <Image src="/images/blib.jpg" alt={t("admin.info.logo")} /> + </ImageWrapper> + <div className="media-content"> + <div className="content"> + <h3 className="has-text-weight-medium">{t("admin.info.newRelease.title")}</h3> + <p> + {t("admin.info.newRelease.description", { + version: releaseInfo?.version + })} + </p> + <a className="button is-info is-pulled-right" target="_blank" href={releaseInfo?.link}> + {t("admin.info.newRelease.downloadButton")} + </a> + </div> + </div> + </article> + </BoxShadowBox> + <hr /> + </> + )} + <BoxShadowBox className="box"> <article className="media"> <ImageWrapper className="media-left"> @@ -110,8 +172,10 @@ class AdminDetails extends React.Component<Props> { const mapStateToProps = (state: any) => { const version = getAppVersion(state); + const releaseInfoLink = getReleaseInfoLink(state); return { - version + version, + releaseInfoLink }; }; diff --git a/scm-ui/ui-webapp/src/modules/indexResource.ts b/scm-ui/ui-webapp/src/modules/indexResource.ts index b204e5600f..5dd417e756 100644 --- a/scm-ui/ui-webapp/src/modules/indexResource.ts +++ b/scm-ui/ui-webapp/src/modules/indexResource.ts @@ -158,6 +158,10 @@ export function getAppVersion(state: object) { return state.indexResources.version; } +export function getReleaseInfoLink(state: object) { + return getLink(state, "releaseInfo"); +} + export function getUiPluginsLink(state: object) { return getLink(state, "uiPlugins"); } diff --git a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedDto.java b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedDto.java index 0eb8f1f963..293ddf2cb8 100644 --- a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedDto.java +++ b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedDto.java @@ -28,7 +28,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import sonia.scm.xml.XmlDateWithTimezoneAdapter; +import sonia.scm.xml.XmlUTCDateAdapter; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; @@ -64,7 +64,7 @@ public final class ReleaseFeedDto { private String description; private String link; private String generator; - @XmlJavaTypeAdapter(XmlDateWithTimezoneAdapter.class) + @XmlJavaTypeAdapter(XmlUTCDateAdapter.class) private Date lastBuildDate; @XmlElement(name = "item") private List<Release> releases; @@ -80,7 +80,7 @@ public final class ReleaseFeedDto { private String description; private String link; private String guid; - @XmlJavaTypeAdapter(XmlDateWithTimezoneAdapter.class) + @XmlJavaTypeAdapter(XmlUTCDateAdapter.class) private Date pubDate; } } diff --git a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedParser.java b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedParser.java index ad909d010a..3976ac0e13 100644 --- a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedParser.java +++ b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedParser.java @@ -30,7 +30,6 @@ import sonia.scm.net.ahc.AdvancedHttpClient; import javax.inject.Inject; import java.io.IOException; -import java.time.Instant; import java.util.Comparator; import java.util.Optional; @@ -51,7 +50,7 @@ public class ReleaseFeedParser { Optional<ReleaseFeedDto.Release> latestRelease = filterForLatestRelease(releaseFeed); if (latestRelease.isPresent()) { ReleaseFeedDto.Release release = latestRelease.get(); - return Optional.of(new ReleaseInfo(release.getTitle(), release.getLink(), Instant.now())); + return Optional.of(new ReleaseInfo(release.getTitle(), release.getLink())); } return Optional.empty(); } diff --git a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseInfo.java b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseInfo.java index fc0cc8b6ac..cb37703746 100644 --- a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseInfo.java +++ b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseInfo.java @@ -27,12 +27,9 @@ package sonia.scm.admin; import lombok.AllArgsConstructor; import lombok.Getter; -import java.time.Instant; - @AllArgsConstructor @Getter public class ReleaseInfo { - private final String title; + private final String version; private final String link; - private final Instant releaseDate; } diff --git a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseVersionChecker.java b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseVersionChecker.java index 371f4daef1..b83e590c5b 100644 --- a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseVersionChecker.java +++ b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseVersionChecker.java @@ -82,11 +82,12 @@ public class ReleaseVersionChecker { LOG.info("No newer version found for SCM-Manager"); return Optional.empty(); } + LOG.info("No newer version found for SCM-Manager"); return Optional.empty(); } private boolean isNewerVersion(ReleaseInfo releaseInfo) { - Version versionFromReleaseFeed = Version.parse(releaseInfo.getTitle()); + Version versionFromReleaseFeed = Version.parse(releaseInfo.getVersion()); return versionFromReleaseFeed.isNewer(scmContextProvider.getVersion()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AdminInfoResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AdminInfoResource.java index 03a703440d..1f6bc8619d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AdminInfoResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AdminInfoResource.java @@ -62,7 +62,7 @@ public class AdminInfoResource { @GET @Path("releaseInfo") @Produces(VndMediaType.ADMIN_INFO) - @Operation(summary = "Returns release info.", description = "Returns a release info if a newer version of SCM-Manager is available.", tags = "AdminInfo") + @Operation(summary = "Returns release info.", description = "Returns information about the latest release if a newer version of SCM-Manager is available.", tags = "AdminInfo") @ApiResponse(responseCode = "200", description = "success") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the information") diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReleaseInfoDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReleaseInfoDto.java index 1f3e2c92e3..c4d241d2f5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReleaseInfoDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReleaseInfoDto.java @@ -29,15 +29,12 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import java.time.Instant; - @NoArgsConstructor @Setter @Getter @SuppressWarnings("squid:S2160") // we do not need equals for dto public class ReleaseInfoDto extends HalRepresentation { - private String title; + private String version; private String link; - private Instant releaseDate; } diff --git a/scm-webapp/src/main/resources/config/gcache.xml b/scm-webapp/src/main/resources/config/gcache.xml index 68bc7a2e9d..d2dc79d8f0 100644 --- a/scm-webapp/src/main/resources/config/gcache.xml +++ b/scm-webapp/src/main/resources/config/gcache.xml @@ -66,6 +66,16 @@ expireAfterWrite="3600" /> + <!-- + ReleaseInfo cache + average: 30K +--> + <cache + name="sonia.cache.releaseInfo" + maximumSize="1" + expireAfterWrite="3600" + /> + <!-- Search cache for users average: 0.5K diff --git a/scm-webapp/src/test/java/sonia/scm/admin/ReleaseFeedParserTest.java b/scm-webapp/src/test/java/sonia/scm/admin/ReleaseFeedParserTest.java index 7dab58f2fd..5ca681cb21 100644 --- a/scm-webapp/src/test/java/sonia/scm/admin/ReleaseFeedParserTest.java +++ b/scm-webapp/src/test/java/sonia/scm/admin/ReleaseFeedParserTest.java @@ -58,8 +58,8 @@ class ReleaseFeedParserTest { Optional<ReleaseInfo> release = releaseFeedParser.findLatestRelease(url); assertThat(release).isPresent(); - assertThat(release.get().getTitle()).isEqualTo("3"); - assertThat(release.get().getTitle()).isEqualTo("download-3"); + assertThat(release.get().getVersion()).isEqualTo("3"); + assertThat(release.get().getLink()).isEqualTo("download-3"); } private ReleaseFeedDto createReleaseFeedDto() { diff --git a/scm-webapp/src/test/java/sonia/scm/admin/ReleaseInfoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/admin/ReleaseInfoMapperTest.java index 82abd6ce74..ec58828a88 100644 --- a/scm-webapp/src/test/java/sonia/scm/admin/ReleaseInfoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/admin/ReleaseInfoMapperTest.java @@ -37,12 +37,10 @@ class ReleaseInfoMapperTest { @Test void shouldMapToDto() { - Instant releaseDate = Instant.now(); - ReleaseInfo releaseInfo = new ReleaseInfo("1.2.3", "download-link", releaseDate); + ReleaseInfo releaseInfo = new ReleaseInfo("1.2.3", "download-link"); ReleaseInfoDto dto = mapper.map(releaseInfo); assertThat(dto.getLink()).isEqualTo(releaseInfo.getLink()); - assertThat(dto.getReleaseDate()).isEqualTo(releaseDate); - assertThat(dto.getTitle()).isEqualTo(releaseInfo.getTitle()); + assertThat(dto.getVersion()).isEqualTo(releaseInfo.getVersion()); } } diff --git a/scm-webapp/src/test/java/sonia/scm/admin/ReleaseVersionCheckerTest.java b/scm-webapp/src/test/java/sonia/scm/admin/ReleaseVersionCheckerTest.java index 87a607a9cb..c5f412f0f4 100644 --- a/scm-webapp/src/test/java/sonia/scm/admin/ReleaseVersionCheckerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/admin/ReleaseVersionCheckerTest.java @@ -35,7 +35,6 @@ import sonia.scm.cache.MapCacheManager; import sonia.scm.config.ScmConfiguration; import java.io.IOException; -import java.time.Instant; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -72,7 +71,7 @@ class ReleaseVersionCheckerTest { @Test void shouldReturnReleaseInfoFromCache() { - ReleaseInfo cachedReleaseInfo = new ReleaseInfo("1.42.9", "download-link", Instant.now()); + ReleaseInfo cachedReleaseInfo = new ReleaseInfo("1.42.9", "download-link"); Cache<String, ReleaseInfo> cache = new MapCacheManager().getCache("sonia.cache.releaseInfo"); cache.put("latest", cachedReleaseInfo); checker.setCache(cache); @@ -80,13 +79,13 @@ class ReleaseVersionCheckerTest { Optional<ReleaseInfo> releaseInfo = checker.checkForNewerVersion(); assertThat(releaseInfo).isPresent(); - assertThat(releaseInfo.get().getTitle()).isEqualTo("1.42.9"); + assertThat(releaseInfo.get().getVersion()).isEqualTo("1.42.9"); assertThat(releaseInfo.get().getLink()).isEqualTo("download-link"); } @Test void shouldReturnReleaseInfo() throws IOException { - ReleaseInfo releaseInfo = new ReleaseInfo("2.0.0", "download-link", Instant.now()); + ReleaseInfo releaseInfo = new ReleaseInfo("2.5.0", "download-link"); when(scmConfiguration.getReleaseFeedUrl()).thenReturn("releaseFeed"); when(feedReader.findLatestRelease("releaseFeed")).thenReturn(Optional.of(releaseInfo)); when(contextProvider.getVersion()).thenReturn("1.9.0"); @@ -94,7 +93,7 @@ class ReleaseVersionCheckerTest { Optional<ReleaseInfo> latestRelease = checker.checkForNewerVersion(); assertThat(latestRelease).isPresent(); - assertThat(latestRelease.get().getTitle()).isEqualTo("2.0.0"); + assertThat(latestRelease.get().getVersion()).isEqualTo("2.5.0"); assertThat(latestRelease.get().getLink()).isEqualTo("download-link"); } }