Merge branch 'develop' into feature/user_converter

This commit is contained in:
Konstantin Schaper
2020-10-21 14:34:57 +02:00
committed by GitHub
15 changed files with 1435 additions and 125 deletions

View File

@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Generation of email addresses for users, where none is configured ([#1370](https://github.com/scm-manager/scm-manager/pull/1370)) - Generation of email addresses for users, where none is configured ([#1370](https://github.com/scm-manager/scm-manager/pull/1370))
- Automatic user converter for external users ([#1380](https://github.com/scm-manager/scm-manager/pull/1380)) - Automatic user converter for external users ([#1380](https://github.com/scm-manager/scm-manager/pull/1380))
- Source code fullscreen view ([#1376](https://github.com/scm-manager/scm-manager/pull/1376))
## [2.6.3] - 2020-10-16
### Fixed
- Missing default permission to manage public gpg keys ([#1377](https://github.com/scm-manager/scm-manager/pull/1377))
## [2.7.1] - 2020-10-14 ## [2.7.1] - 2020-10-14
### Fixed ### Fixed
@@ -363,3 +368,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[2.6.0]: https://www.scm-manager.org/download/2.6.0 [2.6.0]: https://www.scm-manager.org/download/2.6.0
[2.6.1]: https://www.scm-manager.org/download/2.6.1 [2.6.1]: https://www.scm-manager.org/download/2.6.1
[2.6.2]: https://www.scm-manager.org/download/2.6.2 [2.6.2]: https://www.scm-manager.org/download/2.6.2
[2.6.3]: https://www.scm-manager.org/download/2.6.3
[2.7.0]: https://www.scm-manager.org/download/2.7.0
[2.7.1]: https://www.scm-manager.org/download/2.7.1

34
Jenkinsfile vendored
View File

@@ -7,7 +7,8 @@ import com.cloudogu.ces.cesbuildlib.*
node('docker') { node('docker') {
mainBranch = 'develop' developmentBranch = 'develop'
mainBranch = 'master'
properties([ properties([
// Keep only the last 10 build to preserve space // Keep only the last 10 build to preserve space
@@ -51,9 +52,9 @@ node('docker') {
sh "git config 'remote.origin.fetch' '+refs/heads/*:refs/remotes/origin/*'" sh "git config 'remote.origin.fetch' '+refs/heads/*:refs/remotes/origin/*'"
sh "git fetch --all" sh "git fetch --all"
// merge release branch into master // merge release branch into main branch
sh "git checkout master" sh "git checkout ${mainBranch}"
sh "git reset --hard origin/master" sh "git reset --hard origin/${mainBranch}"
sh "git merge --ff-only ${env.BRANCH_NAME}" sh "git merge --ff-only ${env.BRANCH_NAME}"
// set tag // set tag
@@ -87,7 +88,7 @@ node('docker') {
sonarQube.analyzeWith(mvn) sonarQube.analyzeWith(mvn)
} }
if (isBuildSuccessful() && (isMainBranch() || isReleaseBranch())) { if (isBuildSuccessful() && (isDevelopmentBranch() || isReleaseBranch())) {
def commitHash = git.getCommitHash() def commitHash = git.getCommitHash()
def imageVersion = mvn.getVersion() def imageVersion = mvn.getVersion()
@@ -143,20 +144,28 @@ node('docker') {
} }
stage('Presentation Environment') { stage('Presentation Environment') {
// we don't use developmentBranch, because we only want the lastest version of develop branch on
// next-scm. We don't want a support branch or something similar on the presentation environment.
if ("develop".equals(env.BRANCH_NAME)) {
build job: 'scm-manager/next-scm.cloudogu.com', propagate: false, wait: false, parameters: [ build job: 'scm-manager/next-scm.cloudogu.com', propagate: false, wait: false, parameters: [
string(name: 'changeset', value: commitHash), string(name: 'changeset', value: commitHash),
string(name: 'imageTag', value: imageVersion) string(name: 'imageTag', value: imageVersion)
] ]
} }
}
if (isReleaseBranch()) { if (isReleaseBranch()) {
stage('Update Repository') { stage('Update Repository') {
// merge changes into develop // merge changes into develop
sh "git checkout develop" sh "git checkout ${developmentBranch}"
// TODO what if we have a conflict // TODO what if we have a conflict
// e.g.: someone has edited the changelog during the release // e.g.: someone has edited the changelog during the release
sh "git merge master" if (!developmentBranch.equals(mainBranch)) {
sh "git merge ${mainBranch}"
}
// set versions for maven packages // set versions for maven packages
mvn "build-helper:parse-version versions:set -DgenerateBackupPoms=false -DnewVersion='\${parsedVersion.majorVersion}.\${parsedVersion.nextMinorVersion}.0-SNAPSHOT'" mvn "build-helper:parse-version versions:set -DgenerateBackupPoms=false -DnewVersion='\${parsedVersion.majorVersion}.\${parsedVersion.nextMinorVersion}.0-SNAPSHOT'"
@@ -176,8 +185,10 @@ node('docker') {
// push changes back to remote repository // push changes back to remote repository
withCredentials([usernamePassword(credentialsId: 'cesmarvin-github', usernameVariable: 'GIT_AUTH_USR', passwordVariable: 'GIT_AUTH_PSW')]) { withCredentials([usernamePassword(credentialsId: 'cesmarvin-github', usernameVariable: 'GIT_AUTH_USR', passwordVariable: 'GIT_AUTH_PSW')]) {
sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin master --tags" sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin ${mainBranch} --tags"
if (!developmentBranch.equals(mainBranch)) {
sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin develop --tags" sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin develop --tags"
}
sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin :${env.BRANCH_NAME}" sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin :${env.BRANCH_NAME}"
} }
} }
@@ -189,6 +200,7 @@ node('docker') {
} }
} }
String developmentBranch
String mainBranch String mainBranch
Maven setupMavenBuild() { Maven setupMavenBuild() {
@@ -201,7 +213,7 @@ Maven setupMavenBuild() {
mvn.additionalArgs += " -Dscm-it.logbackConfiguration=${logConf}" mvn.additionalArgs += " -Dscm-it.logbackConfiguration=${logConf}"
mvn.additionalArgs += " -Dsonar.coverage.exclusions=**/*.test.ts,**/*.test.tsx,**/*.stories.tsx" mvn.additionalArgs += " -Dsonar.coverage.exclusions=**/*.test.ts,**/*.test.tsx,**/*.stories.tsx"
if (isMainBranch() || isReleaseBranch()) { if (isDevelopmentBranch() || isReleaseBranch()) {
// Release starts javadoc, which takes very long, so do only for certain branches // Release starts javadoc, which takes very long, so do only for certain branches
mvn.additionalArgs += ' -DperformRelease' mvn.additionalArgs += ' -DperformRelease'
// JDK8 is more strict, we should fix this before the next release. Right now, this is just not the focus, yet. // JDK8 is more strict, we should fix this before the next release. Right now, this is just not the focus, yet.
@@ -218,8 +230,8 @@ String getReleaseVersion() {
return env.BRANCH_NAME.substring("release/".length()); return env.BRANCH_NAME.substring("release/".length());
} }
boolean isMainBranch() { boolean isDevelopmentBranch() {
return mainBranch.equals(env.BRANCH_NAME) return developmentBranch.equals(env.BRANCH_NAME)
} }
void withGPGEnvironment(def closure) { void withGPGEnvironment(def closure) {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,79 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import * as React from "react";
import { FC, ReactNode, useState } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import FullscreenModal from "../modals/FullscreenModal";
import Tooltip from "../Tooltip";
type Props = {
modalTitle: string;
modalBody: ReactNode;
tooltipStyle?: "tooltipComponent" | "htmlTitle";
};
const Button = styled.a`
width: 50px;
&:hover {
color: #33b2e8;
}
`;
const OpenInFullscreenButton: FC<Props> = ({ modalTitle, modalBody, tooltipStyle = "tooltipComponent" }) => {
const [t] = useTranslation("repos");
const [showModal, setShowModal] = useState(false);
const tooltip = t("diff.fullscreen.open");
const content = (
<>
<Button
title={tooltipStyle === "htmlTitle" ? tooltip : undefined}
className="button"
onClick={() => setShowModal(true)}
>
<i className="fas fa-search-plus" />
</Button>
{showModal && (
<FullscreenModal
title={modalTitle}
closeFunction={() => setShowModal(false)}
body={modalBody}
active={showModal}
/>
)}
</>
);
if (tooltipStyle === "htmlTitle") {
return <>{content}</>;
}
return (
<Tooltip message={tooltip} location="top">
{content}
</Tooltip>
);
};
export default OpenInFullscreenButton;

View File

@@ -33,4 +33,5 @@ export { default as SubmitButton } from "./SubmitButton";
export { default as DownloadButton } from "./DownloadButton"; export { default as DownloadButton } from "./DownloadButton";
export { default as ButtonGroup } from "./ButtonGroup"; export { default as ButtonGroup } from "./ButtonGroup";
export { default as ButtonAddons } from "./ButtonAddons"; export { default as ButtonAddons } from "./ButtonAddons";
export { default as OpenInFullscreenButton } from "./OpenInFullscreenButton";
export { default as RemoveEntryOfTableButton } from "./RemoveEntryOfTableButton"; export { default as RemoveEntryOfTableButton } from "./RemoveEntryOfTableButton";

View File

@@ -0,0 +1,52 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import * as React from "react";
import { FC, ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { Modal } from "./Modal";
import Button from "../buttons/Button";
import styled from "styled-components";
type Props = {
title: string;
closeFunction: () => void;
body: ReactNode;
active: boolean;
};
const FullSizedModal = styled(Modal)`
& .modal-card {
width: 98%;
max-height: 97vh;
}
`;
const FullscreenModal: FC<Props> = ({ title, closeFunction, body, active }) => {
const [t] = useTranslation("repos");
const footer = <Button label={t("diff.fullscreen.close")} action={closeFunction} color="grey" />;
return <FullSizedModal title={title} closeFunction={closeFunction} body={body} footer={footer} active={active} />;
};
export default FullscreenModal;

View File

@@ -53,7 +53,7 @@ export const Modal: FC<Props> = ({ title, closeFunction, body, footer, active, c
const modalElement = ( const modalElement = (
<div className={classNames("modal", className, isActive)}> <div className={classNames("modal", className, isActive)}>
<div className="modal-background" /> <div className="modal-background" onClick={closeFunction} />
<div className="modal-card"> <div className="modal-card">
<header className={classNames("modal-card-head", `has-background-${headColor}`)}> <header className={classNames("modal-card-head", `has-background-${headColor}`)}>
<p className="modal-card-title is-marginless">{title}</p> <p className="modal-card-title is-marginless">{title}</p>

View File

@@ -26,3 +26,4 @@
export { default as ConfirmAlert, confirmAlert } from "./ConfirmAlert"; export { default as ConfirmAlert, confirmAlert } from "./ConfirmAlert";
export { default as Modal } from "./Modal"; export { default as Modal } from "./Modal";
export { default as FullscreenModal } from "./FullscreenModal";

View File

@@ -33,7 +33,7 @@ import Icon from "../Icon";
import { Change, ChangeEvent, DiffObjectProps, File, Hunk as HunkType } from "./DiffTypes"; import { Change, ChangeEvent, DiffObjectProps, File, Hunk as HunkType } from "./DiffTypes";
import TokenizedDiffView from "./TokenizedDiffView"; import TokenizedDiffView from "./TokenizedDiffView";
import DiffButton from "./DiffButton"; import DiffButton from "./DiffButton";
import { MenuContext } from "@scm-manager/ui-components"; import { MenuContext, OpenInFullscreenButton } from "@scm-manager/ui-components";
import DiffExpander, { ExpandableHunk } from "./DiffExpander"; import DiffExpander, { ExpandableHunk } from "./DiffExpander";
import HunkExpandLink from "./HunkExpandLink"; import HunkExpandLink from "./HunkExpandLink";
import { Modal } from "../modals"; import { Modal } from "../modals";
@@ -91,6 +91,15 @@ const ChangeTypeTag = styled(Tag)`
margin-left: 0.75rem; margin-left: 0.75rem;
`; `;
const MarginlessModalContent = styled.div`
margin: -1.25rem;
& .panel-block {
flex-direction: column;
align-items: stretch;
}
`;
class DiffFile extends React.Component<Props, State> { class DiffFile extends React.Component<Props, State> {
static defaultProps: Partial<Props> = { static defaultProps: Partial<Props> = {
defaultCollapse: false, defaultCollapse: false,
@@ -406,12 +415,8 @@ class DiffFile extends React.Component<Props, State> {
const { file, collapsed, sideBySide, diffExpander, expansionError } = this.state; const { file, collapsed, sideBySide, diffExpander, expansionError } = this.state;
const viewType = sideBySide ? "split" : "unified"; const viewType = sideBySide ? "split" : "unified";
let body = null;
let icon = "angle-right";
if (!collapsed) {
const fileAnnotations = fileAnnotationFactory ? fileAnnotationFactory(file) : null; const fileAnnotations = fileAnnotationFactory ? fileAnnotationFactory(file) : null;
icon = "angle-down"; const innerContent = (
body = (
<div className="panel-block is-paddingless"> <div className="panel-block is-paddingless">
{fileAnnotations} {fileAnnotations}
<TokenizedDiffView className={viewType} viewType={viewType} file={file}> <TokenizedDiffView className={viewType} viewType={viewType} file={file}>
@@ -423,10 +428,21 @@ class DiffFile extends React.Component<Props, State> {
</TokenizedDiffView> </TokenizedDiffView>
</div> </div>
); );
let icon = "angle-right";
let body = null;
if (!collapsed) {
icon = "angle-down";
body = innerContent;
} }
const collapseIcon = this.hasContent(file) ? <Icon name={icon} color="inherit" /> : null; const collapseIcon = this.hasContent(file) ? <Icon name={icon} color="inherit" /> : null;
const fileControls = fileControlFactory ? fileControlFactory(file, this.setCollapse) : null; const fileControls = fileControlFactory ? fileControlFactory(file, this.setCollapse) : null;
const sideBySideToggle = file.hunks && file.hunks.length && ( const openInFullscreen = file?.hunks?.length ? (
<OpenInFullscreenButton
modalTitle={file.type === "delete" ? file.oldPath : file.newPath}
modalBody={<MarginlessModalContent>{innerContent}</MarginlessModalContent>}
/>
) : null;
const sideBySideToggle = file?.hunks?.length && (
<MenuContext.Consumer> <MenuContext.Consumer>
{({ setCollapsed }) => ( {({ setCollapsed }) => (
<DiffButton <DiffButton
@@ -447,6 +463,7 @@ class DiffFile extends React.Component<Props, State> {
<ButtonWrapper className={classNames("level-right", "is-flex")}> <ButtonWrapper className={classNames("level-right", "is-flex")}>
<ButtonGroup> <ButtonGroup>
{sideBySideToggle} {sideBySideToggle}
{openInFullscreen}
{fileControls} {fileControls}
</ButtonGroup> </ButtonGroup>
</ButtonWrapper> </ButtonWrapper>
@@ -465,7 +482,11 @@ class DiffFile extends React.Component<Props, State> {
} }
return ( return (
<DiffFilePanel className={classNames("panel", "is-size-6")} collapsed={(file && file.isBinary) || collapsed} id={this.getAnchorId(file)}> <DiffFilePanel
className={classNames("panel", "is-size-6")}
collapsed={(file && file.isBinary) || collapsed}
id={this.getAnchorId(file)}
>
{errorModal} {errorModal}
<div className="panel-heading"> <div className="panel-heading">
<FlexWrapLevel className="level"> <FlexWrapLevel className="level">

View File

@@ -269,7 +269,11 @@
"expandLastBottomByLines": "Bis zu {{count}} weitere Zeilen laden", "expandLastBottomByLines": "Bis zu {{count}} weitere Zeilen laden",
"expandLastBottomComplete": "Alle verbleibenden Zeilen laden", "expandLastBottomComplete": "Alle verbleibenden Zeilen laden",
"expanding": "Zeilen werden geladen ...", "expanding": "Zeilen werden geladen ...",
"expansionFailed": "Fehler beim Laden der zusätzlichen Zeilen" "expansionFailed": "Fehler beim Laden der zusätzlichen Zeilen",
"fullscreen": {
"open": "In Vollbildansicht öffnen",
"close": "Schließen"
}
}, },
"fileUpload": { "fileUpload": {
"clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.", "clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.",

View File

@@ -276,7 +276,11 @@
"expandLastBottomByLines": "load up to {{count}} more lines", "expandLastBottomByLines": "load up to {{count}} more lines",
"expandLastBottomComplete": "load all remaining lines", "expandLastBottomComplete": "load all remaining lines",
"expanding": "loading lines ...", "expanding": "loading lines ...",
"expansionFailed": "Error while loading additional lines" "expansionFailed": "Error while loading additional lines",
"fullscreen": {
"open": "Open in Fullscreen",
"close": "Close"
}
}, },
"fileUpload": { "fileUpload": {
"clickHere": "Click here to select your file", "clickHere": "Click here to select your file",

View File

@@ -21,77 +21,38 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
import React from "react"; import React, { FC, useEffect, useState } from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { apiClient, ErrorNotification, Loading, SyntaxHighlighter } from "@scm-manager/ui-components"; import { apiClient, ErrorNotification, Loading, SyntaxHighlighter } from "@scm-manager/ui-components";
import { File, Link } from "@scm-manager/ui-types"; import { File, Link } from "@scm-manager/ui-types";
type Props = WithTranslation & { type Props = {
file: File; file: File;
language: string; language: string;
}; };
type State = { const SourcecodeViewer: FC<Props> = ({ file, language }) => {
content: string; const [content, setContent] = useState("");
error?: Error; const [error, setError] = useState<Error | undefined>(undefined);
loaded: boolean; const [loading, setLoading] = useState(true);
currentFileRevision: string; const [currentFileRevision, setCurrentFileRevision] = useState("");
};
class SourcecodeViewer extends React.Component<Props, State> { useEffect(() => {
constructor(props: Props) {
super(props);
this.state = {
content: "",
loaded: false,
currentFileRevision: ""
};
}
componentDidMount() {
this.fetchContentIfChanged();
}
componentDidUpdate() {
this.fetchContentIfChanged();
}
private fetchContentIfChanged() {
const { file } = this.props;
const { currentFileRevision } = this.state;
if (file.revision !== currentFileRevision) { if (file.revision !== currentFileRevision) {
this.fetchContent();
}
}
fetchContent = () => {
const { file } = this.props;
getContent((file._links.self as Link).href) getContent((file._links.self as Link).href)
.then(content => { .then(content => {
this.setState({ setContent(content);
content, setCurrentFileRevision(file.revision);
loaded: true, setLoading(false);
currentFileRevision: file.revision
});
}) })
.catch(error => { .catch(setError);
this.setState({ }
error, }, [currentFileRevision, file]);
loaded: true
});
});
};
render() {
const { content, error, loaded } = this.state;
const language = this.props.language;
if (error) { if (error) {
return <ErrorNotification error={error} />; return <ErrorNotification error={error} />;
} }
if (!loaded) { if (loading) {
return <Loading />; return <Loading />;
} }
@@ -100,8 +61,7 @@ class SourcecodeViewer extends React.Component<Props, State> {
} }
return <SyntaxHighlighter language={getLanguage(language)} value={content} />; return <SyntaxHighlighter language={getLanguage(language)} value={content} />;
} };
}
export function getLanguage(language: string) { export function getLanguage(language: string) {
return language.toLowerCase(); return language.toLowerCase();
@@ -111,4 +71,4 @@ export function getContent(url: string) {
return apiClient.get(url).then(response => response.text()); return apiClient.get(url).then(response => response.text());
} }
export default withTranslation("repos")(SourcecodeViewer); export default SourcecodeViewer;

View File

@@ -21,14 +21,14 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
import React from "react"; import React, { ReactNode } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { WithTranslation, withTranslation } from "react-i18next"; import { WithTranslation, withTranslation } from "react-i18next";
import classNames from "classnames"; import classNames from "classnames";
import styled from "styled-components"; import styled from "styled-components";
import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { File, Repository } from "@scm-manager/ui-types"; import { File, Repository } from "@scm-manager/ui-types";
import { DateFromNow, ErrorNotification, FileSize, Icon } from "@scm-manager/ui-components"; import { DateFromNow, ErrorNotification, FileSize, Icon, OpenInFullscreenButton } from "@scm-manager/ui-components";
import { getSources } from "../modules/sources"; import { getSources } from "../modules/sources";
import FileButtonAddons from "../components/content/FileButtonAddons"; import FileButtonAddons from "../components/content/FileButtonAddons";
import SourcesView from "./SourcesView"; import SourcesView from "./SourcesView";
@@ -82,6 +82,12 @@ const LighterGreyBackgroundTable = styled.table`
background-color: #fbfbfb; background-color: #fbfbfb;
`; `;
const BorderLessDiv = styled.div`
margin: -1.25rem;
border: none;
box-shadow: none;
`;
export type SourceViewSelection = "source" | "history" | "annotations"; export type SourceViewSelection = "source" | "history" | "annotations";
class Content extends React.Component<Props, State> { class Content extends React.Component<Props, State> {
@@ -106,7 +112,7 @@ class Content extends React.Component<Props, State> {
}); });
}; };
showHeader() { showHeader(content: ReactNode) {
const { repository, file, revision } = this.props; const { repository, file, revision } = this.props;
const { selected, collapsed } = this.state; const { selected, collapsed } = this.state;
const icon = collapsed ? "angle-right" : "angle-down"; const icon = collapsed ? "angle-right" : "angle-down";
@@ -129,6 +135,11 @@ class Content extends React.Component<Props, State> {
</div> </div>
<div className="buttons is-grouped"> <div className="buttons is-grouped">
{selector} {selector}
<OpenInFullscreenButton
modalTitle={file?.name}
modalBody={<BorderLessDiv className="panel">{content}</BorderLessDiv>}
tooltipStyle="htmlTitle"
/>
<ExtensionPoint <ExtensionPoint
name="repos.sources.content.actionbar" name="repos.sources.content.actionbar"
props={{ props={{
@@ -211,24 +222,18 @@ class Content extends React.Component<Props, State> {
const { file, revision, repository, path, breadcrumb } = this.props; const { file, revision, repository, path, breadcrumb } = this.props;
const { selected, errorFromExtension } = this.state; const { selected, errorFromExtension } = this.state;
const header = this.showHeader();
let content; let content;
switch (selected) { switch (selected) {
case "source": case "source":
content = ( content = <SourcesView revision={revision} file={file} repository={repository} path={path} />;
<SourcesView revision={revision} file={file} repository={repository} path={path}/>
);
break; break;
case "history": case "history":
content = ( content = <HistoryView file={file} repository={repository} />;
<HistoryView file={file} repository={repository}/>
);
break; break;
case "annotations": case "annotations":
content = ( content = <AnnotateView file={file} repository={repository} />;
<AnnotateView file={file} repository={repository} />
);
} }
const header = this.showHeader(content);
const moreInformation = this.showMoreInformation(); const moreInformation = this.showMoreInformation();
return ( return (

View File

@@ -251,6 +251,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
builder.add(getGroupAutocompletePermission()); builder.add(getGroupAutocompletePermission());
builder.add(getChangeOwnPasswordPermission(user)); builder.add(getChangeOwnPasswordPermission(user));
builder.add(getApiKeyPermission(user)); builder.add(getApiKeyPermission(user));
builder.add(getPublicKeyPermission(user));
} }
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(ImmutableSet.of(Role.USER)); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(ImmutableSet.of(Role.USER));
@@ -267,6 +268,10 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
return UserPermissions.changePassword(user).asShiroString(); return UserPermissions.changePassword(user).asShiroString();
} }
private String getPublicKeyPermission(User user) {
return UserPermissions.changePublicKeys(user).asShiroString();
}
private String getApiKeyPermission(User user) { private String getApiKeyPermission(User user) {
return UserPermissions.changeApiKeys(user).asShiroString(); return UserPermissions.changeApiKeys(user).asShiroString();
} }

View File

@@ -167,8 +167,8 @@ public class DefaultAuthorizationCollectorTest {
AuthorizationInfo authInfo = collector.collect(); AuthorizationInfo authInfo = collector.collect();
assertThat(authInfo.getRoles(), Matchers.contains(Role.USER)); assertThat(authInfo.getRoles(), Matchers.contains(Role.USER));
assertThat(authInfo.getStringPermissions(), hasSize(5)); assertThat(authInfo.getStringPermissions(), hasSize(6));
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "user:read:trillian", "user:changeApiKeys:trillian")); assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "user:read:trillian", "user:changeApiKeys:trillian", "user:changePublicKeys:trillian"));
assertThat(authInfo.getObjectPermissions(), nullValue()); assertThat(authInfo.getObjectPermissions(), nullValue());
} }
@@ -212,7 +212,7 @@ public class DefaultAuthorizationCollectorTest {
AuthorizationInfo authInfo = collector.collect(); AuthorizationInfo authInfo = collector.collect();
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
assertThat(authInfo.getObjectPermissions(), nullValue()); assertThat(authInfo.getObjectPermissions(), nullValue());
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian", "user:changeApiKeys:trillian")); assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian", "user:changeApiKeys:trillian", "user:changePublicKeys:trillian"));
} }
/** /**
@@ -244,7 +244,7 @@ public class DefaultAuthorizationCollectorTest {
AuthorizationInfo authInfo = collector.collect(); AuthorizationInfo authInfo = collector.collect();
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
assertThat(authInfo.getObjectPermissions(), nullValue()); assertThat(authInfo.getObjectPermissions(), nullValue());
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian", "user:changeApiKeys:trillian")); assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian", "user:changeApiKeys:trillian", "user:changePublicKeys:trillian"));
} }
/** /**
@@ -288,7 +288,9 @@ public class DefaultAuthorizationCollectorTest {
"repository:system:one", "repository:system:one",
"repository:group:two", "repository:group:two",
"user:read:trillian", "user:read:trillian",
"user:changeApiKeys:trillian")); "user:changeApiKeys:trillian",
"user:changePublicKeys:trillian"
));
} }
/** /**
@@ -335,7 +337,7 @@ public class DefaultAuthorizationCollectorTest {
AuthorizationInfo authInfo = collector.collect(); AuthorizationInfo authInfo = collector.collect();
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
assertThat(authInfo.getObjectPermissions(), nullValue()); assertThat(authInfo.getObjectPermissions(), nullValue());
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian", "user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "user:changeApiKeys:trillian")); assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian", "user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "user:changeApiKeys:trillian", "user:changePublicKeys:trillian"));
} }
private void authenticate(User user, String group, String... groups) { private void authenticate(User user, String group, String... groups) {