mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): show version number for oss (#5950)
This commit is contained in:
parent
522dd02f44
commit
a0b19513bb
15 changed files with 210 additions and 155 deletions
5
.changeset/rare-hornets-sneeze.md
Normal file
5
.changeset/rare-hornets-sneeze.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@logto/console": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
show version number in the topbar
|
5
packages/console/src/assets/icons/cube.svg
Normal file
5
packages/console/src/assets/icons/cube.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M20.4901 7.52045C20.4844 7.49408 20.4844 7.46681 20.4901 7.44045C20.4852 7.41737 20.4852 7.39353 20.4901 7.37045V7.28045L20.4301 7.13045C20.4057 7.08952 20.3754 7.05244 20.3401 7.02045L20.2501 6.94045H20.2001L16.2601 4.45045L12.5401 2.15045C12.454 2.08218 12.3555 2.03124 12.2501 2.00045H12.1701C12.0807 1.98553 11.9894 1.98553 11.9001 2.00045H11.8001C11.6839 2.02614 11.5725 2.07001 11.4701 2.13045L4.00007 6.78045L3.91007 6.85045L3.82007 6.93045L3.72007 7.00045L3.67007 7.06045L3.61007 7.21045V7.30045V7.36045C3.60035 7.42676 3.60035 7.49413 3.61007 7.56045V16.2904C3.60973 16.4604 3.6527 16.6276 3.73494 16.7763C3.81717 16.9251 3.93596 17.0504 4.08007 17.1404L11.5801 21.7804L11.7301 21.8404H11.8101C11.9792 21.8941 12.1609 21.8941 12.3301 21.8404H12.4101L12.5601 21.7804L20.0001 17.2104C20.1442 17.1204 20.263 16.9951 20.3452 16.8463C20.4274 16.6976 20.4704 16.5304 20.4701 16.3604V7.63045C20.4701 7.63045 20.4901 7.56045 20.4901 7.52045ZM12.0001 4.17045L13.7801 5.27045L8.19007 8.73045L6.40007 7.63045L12.0001 4.17045ZM11.0001 19.1704L5.50007 15.8104V9.42045L11.0001 12.8204V19.1704ZM12.0001 11.0604L10.0901 9.91045L15.6801 6.44045L17.6001 7.63045L12.0001 11.0604ZM18.5001 15.7804L13.0001 19.2004V12.8204L18.5001 9.42045V15.7804Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -1,38 +0,0 @@
|
||||||
@use '@/scss/underscore' as _;
|
|
||||||
|
|
||||||
.helpButton {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: _.unit(1);
|
|
||||||
border-radius: 6px;
|
|
||||||
border: none;
|
|
||||||
background-color: none;
|
|
||||||
transition: background-color 0.2s ease-in-out;
|
|
||||||
user-select: none;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
gap: _.unit(1);
|
|
||||||
margin-left: _.unit(-1);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-hover-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: var(--color-focused-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
|
|
||||||
> path {
|
|
||||||
fill: var(--color-neutral-variant-50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font: var(--font-label-2);
|
|
||||||
color: var(--color-neutral-variant-30);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
import { useState, useRef } from 'react';
|
|
||||||
|
|
||||||
import ContactIcon from '@/assets/icons/contact-us.svg';
|
|
||||||
import DynamicT from '@/ds-components/DynamicT';
|
|
||||||
import { onKeyDownHandler } from '@/utils/a11y';
|
|
||||||
|
|
||||||
import ContactModal from './ContactModal';
|
|
||||||
import * as styles from './index.module.scss';
|
|
||||||
|
|
||||||
function Contact() {
|
|
||||||
const [isContactOpen, setIsContactOpen] = useState(false);
|
|
||||||
const anchorRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
ref={anchorRef}
|
|
||||||
tabIndex={0}
|
|
||||||
className={styles.helpButton}
|
|
||||||
role="button"
|
|
||||||
onKeyDown={onKeyDownHandler(() => {
|
|
||||||
setIsContactOpen(true);
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
setIsContactOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ContactIcon className={styles.icon} />
|
|
||||||
<span>
|
|
||||||
<DynamicT forKey="topbar.help" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ContactModal
|
|
||||||
isOpen={isContactOpen}
|
|
||||||
onCancel={() => {
|
|
||||||
setIsContactOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Contact;
|
|
|
@ -1,43 +0,0 @@
|
||||||
@use '@/scss/underscore' as _;
|
|
||||||
|
|
||||||
.documentNavButton {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: _.unit(1);
|
|
||||||
border-radius: 6px;
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
transition: background-color 0.2s ease-in-out;
|
|
||||||
user-select: none;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: _.unit(-1);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-hover-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: var(--color-focused-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
|
|
||||||
> path {
|
|
||||||
fill: var(--color-neutral-variant-50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font: var(--font-label-2);
|
|
||||||
color: var(--color-neutral-variant-30);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.textLink {
|
|
||||||
&:not(:disabled):hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import DocumentIcon from '@/assets/icons/document-nav-button.svg';
|
|
||||||
import DynamicT from '@/ds-components/DynamicT';
|
|
||||||
import TextLink from '@/ds-components/TextLink';
|
|
||||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
|
||||||
|
|
||||||
import * as styles from './index.module.scss';
|
|
||||||
|
|
||||||
function DocumentNavButton() {
|
|
||||||
const { documentationSiteUrl } = useDocumentationUrl();
|
|
||||||
return (
|
|
||||||
<TextLink
|
|
||||||
href={documentationSiteUrl}
|
|
||||||
targetBlank="noopener"
|
|
||||||
className={classNames(styles.textLink, styles.documentNavButton)}
|
|
||||||
icon={<DocumentIcon className={styles.icon} />}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<DynamicT forKey="topbar.docs" />
|
|
||||||
</span>
|
|
||||||
</TextLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DocumentNavButton;
|
|
|
@ -28,3 +28,51 @@
|
||||||
margin-right: _.unit(4);
|
margin-right: _.unit(4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: _.unit(1);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
user-select: none;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: _.unit(-1);
|
||||||
|
text-decoration: none;
|
||||||
|
gap: _.unit(1);
|
||||||
|
font: var(--font-label-2);
|
||||||
|
color: var(--color-neutral-variant-30);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-hover-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use this selector to override the hover effect in `<TextLink />`
|
||||||
|
&:not(:disabled):hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: var(--color-focused-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
> path {
|
||||||
|
fill: var(--color-neutral-variant-50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.newVersionDot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--color-error);
|
||||||
|
margin-left: _.unit(0.5);
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +1,26 @@
|
||||||
|
import { isKeyInObject, trySafe } from '@silverhand/essentials';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import ContactIcon from '@/assets/icons/contact-us.svg';
|
||||||
|
import CubeIcon from '@/assets/icons/cube.svg';
|
||||||
|
import DocumentIcon from '@/assets/icons/document-nav-button.svg';
|
||||||
import CloudLogo from '@/assets/images/cloud-logo.svg';
|
import CloudLogo from '@/assets/images/cloud-logo.svg';
|
||||||
import Logo from '@/assets/images/logo.svg';
|
import Logo from '@/assets/images/logo.svg';
|
||||||
|
import { githubReleasesLink } from '@/consts';
|
||||||
import { isCloud } from '@/consts/env';
|
import { isCloud } from '@/consts/env';
|
||||||
|
import DynamicT from '@/ds-components/DynamicT';
|
||||||
import Spacer from '@/ds-components/Spacer';
|
import Spacer from '@/ds-components/Spacer';
|
||||||
|
import TextLink from '@/ds-components/TextLink';
|
||||||
|
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||||
|
import { onKeyDownHandler } from '@/utils/a11y';
|
||||||
|
|
||||||
import Contact from './Contact';
|
import ContactModal from './ContactModal';
|
||||||
import DocumentNavButton from './DocumentNavButton';
|
|
||||||
import TenantSelector from './TenantSelector';
|
import TenantSelector from './TenantSelector';
|
||||||
import UserInfo from './UserInfo';
|
import UserInfo from './UserInfo';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
import { currentVersion, isGreaterThanCurrentVersion } from './utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
readonly className?: string;
|
readonly className?: string;
|
||||||
|
@ -35,11 +45,97 @@ function Topbar({ className, hideTenantSelector, hideTitle }: Props) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<DocumentNavButton />
|
<DocumentButton />
|
||||||
<Contact />
|
<HelpButton />
|
||||||
|
{!isCloud && <VersionButton />}
|
||||||
<UserInfo />
|
<UserInfo />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Topbar;
|
export default Topbar;
|
||||||
|
|
||||||
|
function DocumentButton() {
|
||||||
|
const { documentationSiteUrl } = useDocumentationUrl();
|
||||||
|
return (
|
||||||
|
<TextLink
|
||||||
|
href={documentationSiteUrl}
|
||||||
|
targetBlank="noopener"
|
||||||
|
className={styles.button}
|
||||||
|
icon={<DocumentIcon className={styles.icon} />}
|
||||||
|
>
|
||||||
|
<DynamicT forKey="topbar.docs" />
|
||||||
|
</TextLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HelpButton() {
|
||||||
|
const [isContactOpen, setIsContactOpen] = useState(false);
|
||||||
|
const anchorRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={anchorRef}
|
||||||
|
tabIndex={0}
|
||||||
|
className={styles.button}
|
||||||
|
role="button"
|
||||||
|
onKeyDown={onKeyDownHandler(() => {
|
||||||
|
setIsContactOpen(true);
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
setIsContactOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContactIcon className={styles.icon} />
|
||||||
|
<span>
|
||||||
|
<DynamicT forKey="topbar.help" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ContactModal
|
||||||
|
isOpen={isContactOpen}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsContactOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VersionButton() {
|
||||||
|
const [isNewVersionAvailable, setIsNewVersionAvailable] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void trySafe(
|
||||||
|
async () => {
|
||||||
|
const response = await fetch('https://numbers.logto.io/pull.json');
|
||||||
|
const json = await response.json();
|
||||||
|
if (
|
||||||
|
!isKeyInObject(json, 'latestRelease') ||
|
||||||
|
typeof json.latestRelease !== 'string' ||
|
||||||
|
!json.latestRelease.startsWith('v')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isGreaterThanCurrentVersion(json.latestRelease)) {
|
||||||
|
setIsNewVersionAvailable(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.warn('Failed to check for new version', error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextLink
|
||||||
|
href={githubReleasesLink}
|
||||||
|
targetBlank="noopener"
|
||||||
|
className={styles.button}
|
||||||
|
icon={<CubeIcon className={styles.icon} />}
|
||||||
|
>
|
||||||
|
v{currentVersion}
|
||||||
|
{isNewVersionAvailable && <div className={styles.newVersionDot} />}
|
||||||
|
</TextLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
28
packages/console/src/components/Topbar/utils.test.ts
Normal file
28
packages/console/src/components/Topbar/utils.test.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { version as currentVersion } from '../../../../core/package.json';
|
||||||
|
|
||||||
|
import { isGreaterThanCurrentVersion } from './utils';
|
||||||
|
|
||||||
|
describe('isGreaterThanCurrentVersion', () => {
|
||||||
|
it('should return true if the target version is greater than the current Logto version', () => {
|
||||||
|
expect(isGreaterThanCurrentVersion('v10.0')).toBe(true);
|
||||||
|
expect(isGreaterThanCurrentVersion('10.0')).toBe(true);
|
||||||
|
expect(isGreaterThanCurrentVersion('v1.999.0')).toBe(true);
|
||||||
|
expect(isGreaterThanCurrentVersion('1.999.0')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if the target version is less than the current Logto version', () => {
|
||||||
|
expect(isGreaterThanCurrentVersion('v0.1.1')).toBe(false);
|
||||||
|
expect(isGreaterThanCurrentVersion('0.1.1')).toBe(false);
|
||||||
|
expect(isGreaterThanCurrentVersion('v1.15')).toBe(false);
|
||||||
|
expect(isGreaterThanCurrentVersion('1.15')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if the target version is malformed', () => {
|
||||||
|
expect(isGreaterThanCurrentVersion('vv8')).toBe(false);
|
||||||
|
expect(isGreaterThanCurrentVersion('foo')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if the target version is equal to the current Logto version', () => {
|
||||||
|
expect(isGreaterThanCurrentVersion(currentVersion)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
21
packages/console/src/components/Topbar/utils.ts
Normal file
21
packages/console/src/components/Topbar/utils.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { version } from '../../../../core/package.json';
|
||||||
|
|
||||||
|
export { version as currentVersion } from '../../../../core/package.json';
|
||||||
|
|
||||||
|
/** Check if the target version is greater than the current Logto version. */
|
||||||
|
export const isGreaterThanCurrentVersion = (target: string) => {
|
||||||
|
const latestComponents = (target.startsWith('v') ? target.slice(1) : target).split('.');
|
||||||
|
const currentComponents = version.split('.');
|
||||||
|
|
||||||
|
for (const [index, component] of latestComponents.entries()) {
|
||||||
|
const current = currentComponents[index];
|
||||||
|
if (!current || Number(current) < Number(component)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number(current) > Number(component)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
|
@ -2,6 +2,7 @@ export const discordLink = 'https://discord.gg/UEPaF3j5e6';
|
||||||
export const githubOrgLink = 'https://github.com/logto-io';
|
export const githubOrgLink = 'https://github.com/logto-io';
|
||||||
export const githubLink = 'https://github.com/logto-io/logto';
|
export const githubLink = 'https://github.com/logto-io/logto';
|
||||||
export const githubIssuesLink = 'https://github.com/logto-io/logto/issues';
|
export const githubIssuesLink = 'https://github.com/logto-io/logto/issues';
|
||||||
|
export const githubReleasesLink = 'https://github.com/logto-io/logto/releases';
|
||||||
export const contactEmail = 'contact@logto.io';
|
export const contactEmail = 'contact@logto.io';
|
||||||
export const contactEmailLink = `mailto:${contactEmail}`;
|
export const contactEmailLink = `mailto:${contactEmail}`;
|
||||||
export const reservationLink = 'https://cal.com/logto/30min';
|
export const reservationLink = 'https://cal.com/logto/30min';
|
||||||
|
|
|
@ -37,6 +37,7 @@ export default function koaSecurityHeaders<StateT, ContextT, ResponseBodyT>(
|
||||||
const adminOrigins = isCloud ? cloudUrlSet.origins : adminUrlSet.origins;
|
const adminOrigins = isCloud ? cloudUrlSet.origins : adminUrlSet.origins;
|
||||||
const coreOrigins = urlSet.origins;
|
const coreOrigins = urlSet.origins;
|
||||||
const developmentOrigins = conditionalArray(!isProduction && 'ws:');
|
const developmentOrigins = conditionalArray(!isProduction && 'ws:');
|
||||||
|
const logtoOrigin = 'https://*.logto.io';
|
||||||
|
|
||||||
// We use react-monaco-editor for code editing in the admin console. It loads the monaco editor asynchronously from a CDN.
|
// We use react-monaco-editor for code editing in the admin console. It loads the monaco editor asynchronously from a CDN.
|
||||||
// Allow the CDN src in the CSP.
|
// Allow the CDN src in the CSP.
|
||||||
|
@ -116,7 +117,7 @@ export default function koaSecurityHeaders<StateT, ContextT, ResponseBodyT>(
|
||||||
...conditionalArray(!isProduction && ["'unsafe-eval'", "'unsafe-inline'"]),
|
...conditionalArray(!isProduction && ["'unsafe-eval'", "'unsafe-inline'"]),
|
||||||
...monacoEditorCDNSource,
|
...monacoEditorCDNSource,
|
||||||
],
|
],
|
||||||
connectSrc: ["'self'", ...adminOrigins, ...coreOrigins, ...developmentOrigins],
|
connectSrc: ["'self'", logtoOrigin, ...adminOrigins, ...coreOrigins, ...developmentOrigins],
|
||||||
// Allow Main Flow origin loaded in preview iframe
|
// Allow Main Flow origin loaded in preview iframe
|
||||||
frameSrc: ["'self'", ...adminOrigins, ...coreOrigins],
|
frameSrc: ["'self'", ...adminOrigins, ...coreOrigins],
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue