Add version info (#80)

Add version info overlay. Fixes #78
This commit is contained in:
mrdrogdrog 2020-05-29 13:08:59 +02:00 committed by GitHub
parent 5baef25b21
commit 0e8d2f1639
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 188 additions and 63 deletions

View file

@ -22,5 +22,10 @@
"privacy": "test", "privacy": "test",
"termsOfUse": "test", "termsOfUse": "test",
"imprint": "test" "imprint": "test"
},
"version": {
"version": "mock",
"sourceCodeUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"issueTrackerUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
} }
} }

View file

@ -115,7 +115,7 @@
"deleteUser": "Delete user", "deleteUser": "Delete user",
"exportUserData": "Export user data", "exportUserData": "Export user data",
"Help us translating on %s": "Help us translating on %s", "Help us translating on %s": "Help us translating on %s",
"sourceCode": "Source Code", "sourceCode": "Read the source code",
"Register": "Register", "Register": "Register",
"poweredBy": "Powered by <0></0>", "poweredBy": "Powered by <0></0>",
"Help us translating": "Help us translating", "Help us translating": "Help us translating",
@ -128,5 +128,12 @@
"username": "Username", "username": "Username",
"cards": "Cards", "cards": "Cards",
"table": "Table", "table": "Table",
"errorOpenIdLogin": "Invalid OpenID provided" "errorOpenIdLogin": "Invalid OpenID provided",
"versionInfo": "Version info",
"successfullyCopied": "Copied!",
"serverVersion": "Server version",
"clientVersion": "Client version",
"youAreUsing": "You are using",
"close": "Close",
"issueTracker": "Found a bug? Fill an issue!"
} }

View file

@ -1,35 +1,36 @@
import { Trans, useTranslation } from 'react-i18next'
import { TranslatedLink } from './translated-link'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { ExternalLink } from './external-link' import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../redux' import { ApplicationState } from '../../../../redux'
import { ExternalLink } from '../../../links/external-link'
import { TranslatedExternalLink } from '../../../links/translated-external-link'
import { VersionInfo } from '../version-info/version-info'
export const PoweredByLinks: React.FC = () => { export const PoweredByLinks: React.FC = () => {
useTranslation() useTranslation()
const defaultLinks = const defaultLinks =
{ {
releases: '/s/release-notes', releases: '/n/release-notes'
sourceCode: 'https://github.com/codimd/server/tree/41b13e71b6b1d499238c04b15d65e3bd76442f1d'
} }
const config = useSelector((state: ApplicationState) => state.backendConfig) const config = useSelector((state: ApplicationState) => state.backendConfig)
return ( return (
<p> <p>
<Trans i18nKey="poweredBy" components={[<ExternalLink href="https://codimd.org" text="CodiMD"/>]}/> <Trans i18nKey="poweredBy">
<ExternalLink href="https://codimd.org" text="CodiMD"/>
</Trans>
{ {
Object.entries({ ...defaultLinks, ...(config.specialLinks) }).map(([i18nKey, href]) => Object.entries({ ...defaultLinks, ...(config.specialLinks) }).map(([i18nKey, href]) =>
<Fragment key={i18nKey}> <Fragment key={i18nKey}>
&nbsp;|&nbsp; &nbsp;|&nbsp;
<TranslatedLink <TranslatedExternalLink href={href} i18nKey={i18nKey}/>
href={href}
i18nKey={i18nKey}
/>
</Fragment> </Fragment>
) )
} }
&nbsp;|&nbsp;
<VersionInfo/>
</p> </p>
) )
} }

View file

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { ExternalLink } from './external-link' import { ExternalLink } from '../../../links/external-link'
const SocialLink: React.FC = () => { const SocialLink: React.FC = () => {
useTranslation() useTranslation()

View file

@ -1,21 +0,0 @@
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'
export interface TranslatedLinkProps {
href: string;
i18nKey: string;
}
const TranslatedLink: React.FC<TranslatedLinkProps> = ({ href, i18nKey }) => {
useTranslation()
return (
<a href={href}
target="_blank"
rel="noopener noreferrer"
className="text-light">
<Trans i18nKey={i18nKey}/>
</a>
)
}
export { TranslatedLink }

View file

@ -0,0 +1,51 @@
import React, { Fragment, useState } from 'react'
import { Button, Col, Modal, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { ApplicationState } from '../../../../redux'
import frontendVersion from '../../../../version.json'
import { TranslatedExternalLink } from '../../../links/translated-external-link'
import { VersionInputField } from './version-input-field'
export const VersionInfo: React.FC = () => {
const [show, setShow] = useState(false)
const handleClose = () => setShow(false)
const handleShow = () => setShow(true)
const { t } = useTranslation()
const serverVersion = useSelector((state: ApplicationState) => state.backendConfig.version)
const column = (title: string, version: string, sourceCodeLink: string, issueTrackerLink: string) => (
<Col md={6} className={'flex-column'}>
<h5>{title}</h5>
<VersionInputField version={version}/>
{sourceCodeLink
? <TranslatedExternalLink i18nKey={'sourceCode'} className={'btn btn-sm btn-primary d-block mb-2'} href={sourceCodeLink}/> : null}
{issueTrackerLink
? <TranslatedExternalLink i18nKey={'issueTracker'} className={'btn btn-sm btn-primary d-block mb-2'} href={issueTrackerLink}/> : null}
</Col>
)
return (
<Fragment>
<Link to={'#'} className={'text-light'} onClick={handleShow}><Trans i18nKey={'versionInfo'}/></Link>
<Modal show={show} onHide={handleClose} animation={true}>
<Modal.Body className="text-dark">
<h3><Trans i18nKey={'youAreUsing'}/></h3>
<Row>
{column(t('serverVersion'), serverVersion.version, serverVersion.sourceCodeUrl, serverVersion.issueTrackerUrl)}
{column(t('clientVersion'), frontendVersion.version, frontendVersion.sourceCodeUrl, frontendVersion.issueTrackerUrl)}
</Row>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleClose}>
<Trans i18nKey={'close'}/>
</Button>
</Modal.Footer>
</Modal>
</Fragment>
)
}

View file

@ -0,0 +1,43 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React, { Fragment, useRef, useState } from 'react'
import { Button, FormControl, InputGroup, Overlay, Tooltip } from 'react-bootstrap'
import { Trans } from 'react-i18next'
export interface VersionInputFieldProps {
version: string
}
export const VersionInputField: React.FC<VersionInputFieldProps> = ({ version }) => {
const inputField = useRef<HTMLInputElement>(null)
const [showCopiedTooltip, setShowCopiedTooltip] = useState(false)
const copyToClipboard = (content: string) => {
navigator.clipboard.writeText(content).then(() => {
setShowCopiedTooltip(true)
setTimeout(() => { setShowCopiedTooltip(false) }, 2000)
}).catch(() => {
console.error("couldn't copy")
})
}
return (
<Fragment>
<Overlay target={inputField} show={showCopiedTooltip} placement="top">
{(props) => (
<Tooltip id={'copied_' + version} {...props}>
<Trans i18nKey={'successfullyCopied'}/>
</Tooltip>
)}
</Overlay>
<InputGroup className="mb-3">
<FormControl readOnly={true} ref={inputField} className={'text-center'} value={version} />
<InputGroup.Append>
<Button variant="outline-secondary" onClick={() => copyToClipboard(version)} title={'Copy'}>
<FontAwesomeIcon icon={'copy'}/>
</Button>
</InputGroup.Append>
</InputGroup>
</Fragment>
)
}

View file

@ -1,19 +1,20 @@
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconProp } from '../../../../utils/iconProp' import { IconProp } from '../../utils/iconProp'
export interface ExternalLinkProp { export interface ExternalLinkProp {
href: string; href: string;
text: string; text: string;
icon?: IconProp; icon?: IconProp;
className?: string
} }
export const ExternalLink: React.FC<ExternalLinkProp> = ({ href, text, icon }) => { export const ExternalLink: React.FC<ExternalLinkProp> = ({ href, text, icon, className = 'text-light' }) => {
return ( return (
<a href={href} <a href={href}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-light"> className={className}>
{ {
icon icon
? <Fragment> ? <Fragment>

View file

@ -0,0 +1,20 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { IconProp } from '../../utils/iconProp'
import { ExternalLink } from './external-link'
export interface TranslatedLinkProps {
i18nKey: string;
href: string;
icon?: IconProp;
className?: string
}
const TranslatedExternalLink: React.FC<TranslatedLinkProps> = ({ i18nKey, ...props }) => {
const { t } = useTranslation()
return (
<ExternalLink text={t(i18nKey)} {...props}/>
)
}
export { TranslatedExternalLink }

View file

@ -6,6 +6,7 @@ import {
faClock, faClock,
faCloudDownloadAlt, faCloudDownloadAlt,
faComment, faComment,
faCopy,
faDownload, faDownload,
faFileAlt, faFileAlt,
faGlobe, faGlobe,
@ -37,5 +38,5 @@ export const setUpFontAwesome: () => void = () => {
library.add(faBolt, faPlus, faChartBar, faTv, faFileAlt, faCloudDownloadAlt, library.add(faBolt, faPlus, faChartBar, faTv, faFileAlt, faCloudDownloadAlt,
faTrash, faSignOutAlt, faComment, faDiscourse, faMastodon, faGlobe, faTrash, faSignOutAlt, faComment, faDiscourse, faMastodon, faGlobe,
faThumbtack, faClock, faTimes, faGithub, faGitlab, faGoogle, faFacebook, faThumbtack, faClock, faTimes, faGithub, faGitlab, faGoogle, faFacebook,
faDropbox, faTwitter, faUsers, faAddressCard, faSort, faDownload, faUpload, faTrash, faSync, faSortUp, faSortDown) faDropbox, faTwitter, faUsers, faAddressCard, faSort, faDownload, faUpload, faTrash, faSync, faSortUp, faSortDown, faCopy)
} }

View file

@ -25,6 +25,11 @@ export const initialState: BackendConfigState = {
privacy: '', privacy: '',
termsOfUse: '', termsOfUse: '',
imprint: '' imprint: ''
},
version: {
version: '',
sourceCodeUrl: '',
issueTrackerUrl: ''
} }
} }

View file

@ -3,43 +3,50 @@ import { Action } from 'redux'
export const SET_BACKEND_CONFIG_ACTION_TYPE = 'backend-config/set' export const SET_BACKEND_CONFIG_ACTION_TYPE = 'backend-config/set'
export interface BackendConfigState { export interface BackendConfigState {
allowAnonymous: boolean, allowAnonymous: boolean,
authProviders: AuthProvidersState, authProviders: AuthProvidersState,
customAuthNames: CustomAuthNames, customAuthNames: CustomAuthNames,
specialLinks: SpecialLinks, specialLinks: SpecialLinks,
version: BackendVersion,
}
export interface BackendVersion {
version: string,
sourceCodeUrl: string
issueTrackerUrl: string
} }
export interface AuthProvidersState { export interface AuthProvidersState {
facebook: boolean, facebook: boolean,
github: boolean, github: boolean,
twitter: boolean, twitter: boolean,
gitlab: boolean, gitlab: boolean,
dropbox: boolean, dropbox: boolean,
ldap: boolean, ldap: boolean,
google: boolean, google: boolean,
saml: boolean, saml: boolean,
oauth2: boolean, oauth2: boolean,
email: boolean, email: boolean,
openid: boolean, openid: boolean,
} }
export interface CustomAuthNames { export interface CustomAuthNames {
ldap: string; ldap: string;
oauth2: string; oauth2: string;
saml: string; saml: string;
} }
export interface SpecialLinks { export interface SpecialLinks {
privacy: string, privacy: string,
termsOfUse: string, termsOfUse: string,
imprint: string, imprint: string,
} }
export interface SetBackendConfigAction extends Action { export interface SetBackendConfigAction extends Action {
type: string; type: string;
payload: { payload: {
state: BackendConfigState; state: BackendConfigState;
}; };
} }
export type BackendConfigActions = SetBackendConfigAction; export type BackendConfigActions = SetBackendConfigAction;

5
src/version.json Normal file
View file

@ -0,0 +1,5 @@
{
"version": "0.0",
"sourceCodeUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"issueTrackerUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}