feat(trading): upgrade banner (#3883)

This commit is contained in:
Art 2023-05-22 21:53:40 +02:00 committed by GitHub
parent a51ab4d98d
commit a0eb381ab1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1797 additions and 86 deletions

View File

@ -5,6 +5,9 @@ import { ProtocolUpgradeProposalDetailInfo } from '../components/protocol-upgrad
import { getNormalisedVotingPower } from '../../staking/shared';
import type { NodesFragmentFragment } from '../../staking/home/__generated__/Nodes';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { useVegaRelease } from '@vegaprotocol/environment';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
export interface ProtocolUpgradeProposalProps {
proposal: ProtocolUpgradeProposalFieldsFragment;
@ -42,6 +45,9 @@ export const ProtocolUpgradeProposal = ({
lastBlockHeight,
consensusValidators,
}: ProtocolUpgradeProposalProps) => {
const { t } = useTranslation();
const releaseInfo = useVegaRelease(proposal.vegaReleaseTag);
const consensusApprovals = useMemo(
() => getConsensusApprovals(consensusValidators || [], proposal),
[consensusValidators, proposal]
@ -78,6 +84,14 @@ export const ProtocolUpgradeProposal = ({
totalConsensusValidators={consensusValidators.length}
/>
)}
{releaseInfo && releaseInfo.htmlUrl && (
<div className="mb-10">
<ExternalLink href={releaseInfo.htmlUrl}>
{t('Explore release on GitHub')}
</ExternalLink>
</div>
)}
</section>
);
};

View File

@ -14,3 +14,5 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
# TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.20.7-core-0.71.4

View File

@ -1 +1,2 @@
export * from './announcement-banner';
export * from './upgrade-banner';

View File

@ -0,0 +1,77 @@
import { useMemo, useState } from 'react';
import {
ReleasesFeed,
useEnvironment,
useReleases,
Networks,
} from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import {
CopyWithTooltip,
ExternalLink,
Intent,
NotificationBanner,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
const CANONICAL_URL = 'https://vega.trading';
type UpgradeBannerProps = {
showVersionChange: boolean;
};
export const UpgradeBanner = ({ showVersionChange }: UpgradeBannerProps) => {
const [visible, setVisible] = useState(true);
const { data } = useReleases(ReleasesFeed.FrontEnd);
const { APP_VERSION, VEGA_ENV } = useEnvironment();
/**
* Filtering out the release candidates.
*/
const latest = useMemo(() => {
const valid =
VEGA_ENV === Networks.MAINNET
? data?.filter((r) => !/-rc$/i.test(r.tagName))
: data;
return valid && valid.length > 0 ? valid[0] : undefined;
}, [VEGA_ENV, data]);
if (!visible || !latest || latest.tagName === APP_VERSION) return null;
const versionChange = (
<span>
<span className="line-through text-vega-light-300 dark:text-vega-dark-300">
{APP_VERSION}
</span>{' '}
<VegaIcon size={14} name={VegaIconNames.ARROW_RIGHT} />{' '}
<span className="text-vega-orange-500 dark:text-vega-yellow-500">
<ExternalLink href={latest.htmlUrl}>{latest.tagName}</ExternalLink>
</span>
</span>
);
return (
<NotificationBanner
intent={Intent.Warning}
onClose={() => {
setVisible(false);
}}
>
<div className="uppercase ">
{t('Upgrade to the latest version of Console')}{' '}
{showVersionChange && versionChange}
</div>
<div data-testid="bookmark-message">
{t('Bookmark')}{' '}
<ExternalLink href={CANONICAL_URL}>{t('vega.trading')}</ExternalLink>
<CopyWithTooltip text={CANONICAL_URL}>
<button title={t('Copy %s', CANONICAL_URL)} className="text-white">
<span className="sr-only">{t('Copy %s', CANONICAL_URL)}</span>
<VegaIcon size={14} name={VegaIconNames.COPY} />
</button>
</CopyWithTooltip>{' '}
{'to always see the latest version.'}
</div>
</NotificationBanner>
);
};

View File

@ -31,7 +31,7 @@ import ToastsManager from './toasts-manager';
import { HashRouter, useLocation, useSearchParams } from 'react-router-dom';
import { Connectors } from '../lib/vega-connectors';
import { ViewingBanner } from '../components/viewing-banner';
import { AnnouncementBanner } from '../components/banner';
import { AnnouncementBanner, UpgradeBanner } from '../components/banner';
import { AppLoader, DynamicLoader } from '../components/app-loader';
import { Navbar } from '../components/navbar';
import { ENV } from '../lib/config';
@ -99,6 +99,7 @@ function AppBody({ Component }: AppProps) {
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
<ViewingBanner />
<UpgradeBanner showVersionChange={true} />
</div>
<main data-testid={location.pathname}>
<Component />

View File

@ -4,3 +4,4 @@ export * from './use-node-health';
export * from './use-node-switcher-store';
export * from './use-vega-releases';
export * from './use-vega-release';
export * from './use-releases';

File diff suppressed because one or more lines are too long

View File

@ -296,6 +296,7 @@ function compileEnvVars() {
GIT_ORIGIN_URL: process.env['GIT_ORIGIN_URL'],
ANNOUNCEMENTS_CONFIG_URL: process.env['NX_ANNOUNCEMENTS_CONFIG_URL'],
VEGA_INCIDENT_URL: process.env['NX_VEGA_INCIDENT_URL'],
APP_VERSION: process.env['NX_APP_VERSION'],
};
return env;

View File

@ -0,0 +1,48 @@
import { renderHook, waitFor } from '@testing-library/react';
import fetchMock from 'fetch-mock';
import { GITHUB_VEGA_FRONTEND_RELEASES_DATA } from './mocks/github-releases';
import {
GITHUB_VEGA_FRONTEND_RELEASES,
ReleasesFeed,
useReleases,
} from './use-releases';
describe('useReleases', () => {
afterEach(() => {
fetchMock.reset();
});
it('should return an empty list when request is unsuccessful', async () => {
fetchMock.get(GITHUB_VEGA_FRONTEND_RELEASES, 404);
const { result } = renderHook(() => {
return useReleases(ReleasesFeed.FrontEnd);
});
expect(result.current.loading).toBeTruthy();
await waitFor(() => {
expect(result.current.loading).toBeFalsy();
expect(result.current.data).toEqual([]);
});
});
it('should return a list of releases', async () => {
fetchMock.get(
GITHUB_VEGA_FRONTEND_RELEASES,
GITHUB_VEGA_FRONTEND_RELEASES_DATA
);
const { result } = renderHook(() => useReleases(ReleasesFeed.FrontEnd));
expect(result.current.loading).toBeTruthy();
await waitFor(() => {
expect(result.current.loading).toBeFalsy();
expect(result.current).not.toEqual([]);
const data = result.current.data;
expect(data?.[0].tagName).toEqual('v0.20.8-core-0.71.4');
expect(data?.[0].htmlUrl).toEqual(
'https://github.com/vegaprotocol/frontend-monorepo/releases/tag/v0.20.8-core-0.71.4'
);
expect(data?.[1].tagName).toEqual('v0.20.6-core-0.71.4-3');
expect(data?.[2].tagName).toEqual('v0.20.6-core-0.71.4');
});
});
});

View File

@ -0,0 +1,121 @@
import { localLoggerFactory } from '@vegaprotocol/utils';
import { useCallback, useEffect, useState } from 'react';
import z from 'zod';
export const GITHUB_VEGA_RELEASES =
'https://api.github.com/repos/vegaprotocol/vega/releases';
export const GITHUB_VEGA_DEV_RELEASES =
'https://api.github.com/repos/vegaprotocol/vega-dev-releases/releases';
export const GITHUB_VEGA_FRONTEND_RELEASES =
'https://api.github.com/repos/vegaprotocol/frontend-monorepo/releases';
export enum ReleasesFeed {
Vega = GITHUB_VEGA_RELEASES,
VegaDev = GITHUB_VEGA_DEV_RELEASES,
FrontEnd = GITHUB_VEGA_FRONTEND_RELEASES,
}
const GithubReleaseSchema = z.object({
url: z.string(),
html_url: z.string(),
id: z.number(),
tag_name: z.string(),
name: z.string().nullable(),
draft: z.boolean(),
created_at: z.string().datetime(),
});
const GithubReleasesSchema = z.array(GithubReleaseSchema);
type ReleaseInfo = {
id: number;
name: string;
tagName: string;
htmlUrl: string;
url: string;
isDraft: boolean;
createdAt: Date;
isDevRelease: boolean;
};
const toReleaseInfo = (
releaseData: z.infer<typeof GithubReleaseSchema>,
isDevRelease: boolean
): ReleaseInfo => ({
id: releaseData.id,
name: releaseData.name || '',
tagName: releaseData.tag_name,
htmlUrl: releaseData.html_url,
url: releaseData.url,
isDraft: releaseData.draft,
createdAt: new Date(releaseData.created_at),
isDevRelease,
});
export type ReleasesState = {
loading: boolean;
data: ReleaseInfo[] | null;
error: Error | null | undefined;
};
export const fetchReleases = async (
feed: ReleasesFeed,
cache: RequestCache = 'default'
) => {
const response = await fetch(String(feed), {
cache,
});
if (response.ok) {
const json = await response.json();
const data = GithubReleasesSchema.parse(json);
return data.map((d) => toReleaseInfo(d, feed === ReleasesFeed.VegaDev));
}
return [];
};
export const useReleases = (feed: ReleasesFeed, cache?: RequestCache) => {
const [state, setState] = useState<ReleasesState>({
loading: true,
data: null,
error: null,
});
const logger = localLoggerFactory({ application: 'github' });
const fetchData = useCallback(() => {
let mounted = true;
fetchReleases(feed, cache)
.then((releases) => {
if (mounted) {
setState({
loading: false,
data: releases.sort(
(a, b) => b.createdAt.valueOf() - a.createdAt.valueOf()
),
error: null,
});
}
})
.catch((err) => {
if (mounted) {
setState({
loading: false,
data: null,
error: err,
});
}
logger.error('get releases from GitHub API', err);
});
return () => {
mounted = false;
};
}, [cache, feed, logger]);
useEffect(() => {
fetchData();
}, [fetchData]);
return state;
};

View File

@ -1,9 +1,6 @@
import { renderHook, waitFor } from '@testing-library/react';
import fetchMock from 'fetch-mock';
import {
GITHUB_VEGA_DEV_RELEASES,
GITHUB_VEGA_RELEASES,
} from './use-vega-releases';
import { GITHUB_VEGA_DEV_RELEASES, GITHUB_VEGA_RELEASES } from './use-releases';
import {
GITHUB_VEGA_RELEASES_DATA,
GITHUB_VEGA_DEV_RELEASES_DATA,

View File

@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { useVegaReleases } from './use-vega-releases';
export const useVegaRelease = (tag: string, includeDevReleases = false) => {
const { data } = useVegaReleases();
const { data } = useVegaReleases(includeDevReleases, 'force-cache');
return useMemo(() => {
return data?.find((r) => r.tagName === tag);
}, [data, tag]);

View File

@ -1,14 +1,11 @@
import { renderHook, waitFor } from '@testing-library/react';
import fetchMock from 'fetch-mock';
import {
GITHUB_VEGA_DEV_RELEASES,
GITHUB_VEGA_RELEASES,
useVegaReleases,
} from './use-vega-releases';
import { useVegaReleases } from './use-vega-releases';
import {
GITHUB_VEGA_RELEASES_DATA,
GITHUB_VEGA_DEV_RELEASES_DATA,
} from './mocks/github-releases';
import { GITHUB_VEGA_DEV_RELEASES, GITHUB_VEGA_RELEASES } from './use-releases';
describe('useVegaReleases', () => {
afterEach(() => {

View File

@ -1,82 +1,18 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { useCallback, useEffect, useState } from 'react';
import z from 'zod';
export const GITHUB_VEGA_RELEASES =
'https://api.github.com/repos/vegaprotocol/vega/releases';
export const GITHUB_VEGA_DEV_RELEASES =
'https://api.github.com/repos/vegaprotocol/vega-dev-releases/releases';
const GithubReleaseSchema = z.object({
url: z.string(),
html_url: z.string(),
id: z.number(),
tag_name: z.string(),
name: z.string().nullable(),
draft: z.boolean(),
created_at: z.string().datetime(),
});
const GithubReleasesSchema = z.array(GithubReleaseSchema);
type ReleaseInfo = {
id: number;
name: string;
tagName: string;
htmlUrl: string;
url: string;
isDraft: boolean;
createdAt: Date;
isDevRelease: boolean;
};
const toReleaseInfo = (
releaseData: z.infer<typeof GithubReleaseSchema>,
isDevRelease: boolean
): ReleaseInfo => ({
id: releaseData.id,
name: releaseData.name || '',
tagName: releaseData.tag_name,
htmlUrl: releaseData.html_url,
url: releaseData.url,
isDraft: releaseData.draft,
createdAt: new Date(releaseData.created_at),
isDevRelease,
});
enum ReleaseFeed {
// @ts-ignore TS18033 - allowed in 5.0
Vega = GITHUB_VEGA_RELEASES,
// @ts-ignore TS18033
VegaDev = GITHUB_VEGA_DEV_RELEASES,
}
const fetchReleases = async (feed: ReleaseFeed) => {
const response = await fetch(String(feed), {
cache: 'force-cache',
});
if (response.ok) {
const json = await response.json();
const data = GithubReleasesSchema.parse(json);
return data.map((d) => toReleaseInfo(d, feed === ReleaseFeed.VegaDev));
}
return [];
};
type State = {
loading: boolean;
data: ReleaseInfo[] | null;
error: Error | null | undefined;
};
import type { ReleasesState } from './use-releases';
import { ReleasesFeed } from './use-releases';
import { fetchReleases } from './use-releases';
/**
* Retrieves a list of vega releases from github.
* First element is the newest.
*/
export const useVegaReleases = (includeDevReleases = false) => {
const [state, setState] = useState<State>({
export const useVegaReleases = (
includeDevReleases = false,
cache?: RequestCache
) => {
const [state, setState] = useState<ReleasesState>({
loading: true,
data: null,
error: null,
@ -87,8 +23,11 @@ export const useVegaReleases = (includeDevReleases = false) => {
Promise.all(
includeDevReleases
? [fetchReleases(ReleaseFeed.Vega), fetchReleases(ReleaseFeed.VegaDev)]
: [fetchReleases(ReleaseFeed.Vega)]
? [
fetchReleases(ReleasesFeed.Vega, cache),
fetchReleases(ReleasesFeed.VegaDev, cache),
]
: [fetchReleases(ReleasesFeed.Vega, cache)]
)
.then(([vega, vegaDev]) => {
if (mounted) {
@ -114,7 +53,7 @@ export const useVegaReleases = (includeDevReleases = false) => {
return () => {
mounted = false;
};
}, [includeDevReleases, setState]);
}, [cache, includeDevReleases]);
useEffect(() => {
fetchData();

View File

@ -51,6 +51,7 @@ const schemaObject = {
ETH_WALLET_MNEMONIC: z.optional(z.string()),
ANNOUNCEMENTS_CONFIG_URL: z.optional(z.string()),
VEGA_INCIDENT_URL: z.optional(z.string()),
APP_VERSION: z.optional(z.string()),
};
// combine schema above with custom rule to ensure either