mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 00:15:44 +01:00
Merge branch 'feature/user_converter' of github.com:scm-manager/scm-manager into feature/user_converter
This commit is contained in:
@@ -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
|
||||||
|
|||||||
44
Jenkinsfile
vendored
44
Jenkinsfile
vendored
@@ -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') {
|
||||||
build job: 'scm-manager/next-scm.cloudogu.com', propagate: false, wait: false, parameters: [
|
// we don't use developmentBranch, because we only want the lastest version of develop branch on
|
||||||
string(name: 'changeset', value: commitHash),
|
// next-scm. We don't want a support branch or something similar on the presentation environment.
|
||||||
string(name: 'imageTag', value: imageVersion)
|
if ("develop".equals(env.BRANCH_NAME)) {
|
||||||
]
|
build job: 'scm-manager/next-scm.cloudogu.com', propagate: false, wait: false, parameters: [
|
||||||
|
string(name: 'changeset', value: commitHash),
|
||||||
|
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"
|
||||||
sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin develop --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 :${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
79
scm-ui/ui-components/src/buttons/OpenInFullscreenButton.tsx
Normal file
79
scm-ui/ui-components/src/buttons/OpenInFullscreenButton.tsx
Normal 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;
|
||||||
@@ -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";
|
||||||
|
|||||||
52
scm-ui/ui-components/src/modals/FullscreenModal.tsx
Normal file
52
scm-ui/ui-components/src/modals/FullscreenModal.tsx
Normal 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;
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {FC} from "react";
|
import { FC } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import usePortalRootElement from "../usePortalRootElement";
|
import usePortalRootElement from "../usePortalRootElement";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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,27 +415,34 @@ 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;
|
const fileAnnotations = fileAnnotationFactory ? fileAnnotationFactory(file) : null;
|
||||||
|
const innerContent = (
|
||||||
|
<div className="panel-block is-paddingless">
|
||||||
|
{fileAnnotations}
|
||||||
|
<TokenizedDiffView className={viewType} viewType={viewType} file={file}>
|
||||||
|
{(hunks: HunkType[]) =>
|
||||||
|
hunks?.map((hunk, n) => {
|
||||||
|
return this.renderHunk(file, diffExpander.getHunk(n), n);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TokenizedDiffView>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
let icon = "angle-right";
|
let icon = "angle-right";
|
||||||
|
let body = null;
|
||||||
if (!collapsed) {
|
if (!collapsed) {
|
||||||
const fileAnnotations = fileAnnotationFactory ? fileAnnotationFactory(file) : null;
|
|
||||||
icon = "angle-down";
|
icon = "angle-down";
|
||||||
body = (
|
body = innerContent;
|
||||||
<div className="panel-block is-paddingless">
|
|
||||||
{fileAnnotations}
|
|
||||||
<TokenizedDiffView className={viewType} viewType={viewType} file={file}>
|
|
||||||
{(hunks: HunkType[]) =>
|
|
||||||
hunks?.map((hunk, n) => {
|
|
||||||
return this.renderHunk(file, diffExpander.getHunk(n), n);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TokenizedDiffView>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
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">
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -21,87 +21,47 @@
|
|||||||
* 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();
|
getContent((file._links.self as Link).href)
|
||||||
|
.then(content => {
|
||||||
|
setContent(content);
|
||||||
|
setCurrentFileRevision(file.revision);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(setError);
|
||||||
}
|
}
|
||||||
|
}, [currentFileRevision, file]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorNotification error={error} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchContent = () => {
|
if (loading) {
|
||||||
const { file } = this.props;
|
return <Loading />;
|
||||||
getContent((file._links.self as Link).href)
|
|
||||||
.then(content => {
|
|
||||||
this.setState({
|
|
||||||
content,
|
|
||||||
loaded: true,
|
|
||||||
currentFileRevision: file.revision
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.setState({
|
|
||||||
error,
|
|
||||||
loaded: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { content, error, loaded } = this.state;
|
|
||||||
const language = this.props.language;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <ErrorNotification error={error} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!loaded) {
|
|
||||||
return <Loading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <SyntaxHighlighter language={getLanguage(language)} value={content} />;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (!content) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user