Skip to content

Commit

Permalink
[explorer]: update redirect (MystenLabs#16472)
Browse files Browse the repository at this point in the history
## Description 

Describe the changes or additions included in this PR.

## Test Plan 

How did you test the new or updated feature?

---
If your changes are not user-facing and do not break anything, you can
skip the following section. Otherwise, please briefly describe what has
changed under the Release Notes section.

### Type of Change (Check all that apply)

- [ ] protocol change
- [ ] user-visible impact
- [ ] breaking change for a client SDKs
- [ ] breaking change for FNs (FN binary must upgrade)
- [ ] breaking change for validators or node operators (must upgrade
binaries)
- [ ] breaking change for on-chain data layout
- [ ] necessitate either a data wipe or data migration

### Release notes
  • Loading branch information
plam-ml authored Mar 1, 2024
1 parent 9050365 commit 65fa5fb
Show file tree
Hide file tree
Showing 15 changed files with 334 additions and 253 deletions.
4 changes: 0 additions & 4 deletions .github/actions/ts-e2e/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,6 @@ runs:
run: pnpm dlx concurrently --kill-others --success command-1 "$E2E_RUN_LOCAL_NET_CMD" 'pnpm --filter @mysten/sui.js --filter @mysten/graphql-transport test:e2e'
shell: bash

- name: Run Explorer e2e tests
run: pnpm --filter sui-explorer playwright test
shell: bash

- uses: actions/upload-artifact@v3
if: always()
with:
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,6 @@ jobs:
if: ${{ needs.diff.outputs.isTypescriptSDK == 'true' || needs.diff.outputs.isExplorer == 'true' || needs.diff.outputs.isRust == 'true'}}
run: pnpm turbo --filter=sui-explorer build

- name: Run Explorer e2e tests
# need to run Explorer e2e when its upstream(TS SDK and Rust) or itself is changed
if: ${{ needs.diff.outputs.isTypescriptSDK == 'true' || needs.diff.outputs.isExplorer == 'true' || needs.diff.outputs.isRust == 'true'}}
run: pnpm --filter sui-explorer playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
Expand Down
Binary file added apps/explorer/src/assets/explorer-suiscan.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/explorer/src/assets/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/explorer/src/assets/explorer-suivision.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/explorer/src/assets/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
164 changes: 158 additions & 6 deletions apps/explorer/src/components/Layout/PageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,31 @@
// SPDX-License-Identifier: Apache-2.0

import { useFeatureIsOn } from '@growthbook/growthbook-react';
import { useAppsBackend, useElementDimensions } from '@mysten/core';
import { LoadingIndicator } from '@mysten/ui';
import { useAppsBackend, useElementDimensions, useLocalStorage } from '@mysten/core';
import { Heading, LoadingIndicator, Text } from '@mysten/ui';
import { useQuery } from '@tanstack/react-query';
import clsx from 'clsx';
import { type ReactNode, useRef } from 'react';
import { type ReactNode, useEffect, useRef } from 'react';

import Footer from '../footer/Footer';
import Header from '../header/Header';
import { useNetworkContext } from '~/context';
import { Banner } from '~/ui/Banner';
import { Network } from '~/utils/api/DefaultRpcClient';
import suiscanImg from '~/assets/explorer-suiscan.jpg';
import suivisionImg from '~/assets/explorer-suivision.jpg';
import suiscanImg2x from '~/assets/[email protected]';
import suivisionImg2x from '~/assets/[email protected]';
import { ButtonOrLink } from '~/ui/utils/ButtonOrLink';
import { Image } from '~/ui/image/Image';
import { ArrowRight12, Sui, SuiLogoTxt } from '@mysten/icons';
import { useRedirectExplorerUrl } from '~/hooks/useRedirectExplorerUrl';
import { ampli } from '~/utils/analytics/ampli';

enum RedirectExplorer {
SUISCAN = 'suiscan',
SUIVISION = 'suivision',
}

export type PageLayoutProps = {
gradient?: {
Expand All @@ -26,7 +40,144 @@ export type PageLayoutProps = {

const DEFAULT_HEADER_HEIGHT = 68;

function useRedirectExplorerOrder() {
const [isSuiVisionFirst, setSuiVisionOrder] = useLocalStorage<boolean | undefined>(
'is-suivision-first',
undefined,
);

useEffect(() => {
if (typeof isSuiVisionFirst === 'undefined') {
setSuiVisionOrder(new Date().getMilliseconds() % 2 === 0);
}
}, [isSuiVisionFirst, setSuiVisionOrder]);

return isSuiVisionFirst
? [RedirectExplorer.SUIVISION, RedirectExplorer.SUISCAN]
: [RedirectExplorer.SUISCAN, RedirectExplorer.SUIVISION];
}

function ImageLink({ type }: { type: RedirectExplorer }) {
const { suiscanUrl, suivisionUrl } = useRedirectExplorerUrl();

const href = type === RedirectExplorer.SUISCAN ? suiscanUrl : suivisionUrl;
const src = type === RedirectExplorer.SUISCAN ? suiscanImg : suivisionImg;
const srcSet =
type === RedirectExplorer.SUISCAN
? `${suiscanImg} 1x, ${suiscanImg2x} 2x`
: `${suivisionImg} 1x, ${suivisionImg2x} 2x`;

return (
<div className="relative overflow-hidden rounded-3xl border border-gray-45 transition duration-300 ease-in-out hover:shadow-lg">
<ButtonOrLink
onClick={() => {
ampli.redirectToExternalExplorer({
name: type,
url: href,
});
}}
href={href}
target="_blank"
rel="noopener noreferrer"
>
<Image src={src} srcSet={srcSet} />
</ButtonOrLink>
<div className="absolute bottom-10 left-1/2 right-0 flex -translate-x-1/2 sm:w-80">
<ButtonOrLink
className="flex w-full items-center justify-center gap-2 rounded-3xl bg-sui-dark px-3 py-2"
onClick={() => {
ampli.redirectToExternalExplorer({
name: type,
url: href,
});
}}
href={href}
target="_blank"
rel="noopener noreferrer"
>
<Text variant="body/semibold" color="white">
{type === RedirectExplorer.SUISCAN ? 'Visit Suiscan.xyz' : 'Visit Suivision.xyz'}
</Text>
<ArrowRight12 className="h-3 w-3 -rotate-45 text-white" />
</ButtonOrLink>
</div>
</div>
);
}

function RedirectContent() {
const redirectExplorers = useRedirectExplorerOrder();

return (
<section className="flex flex-col justify-center gap-10 sm:flex-row">
{redirectExplorers.map((type) => (
<ImageLink key={type} type={type} />
))}
</section>
);
}

function HeaderLink({ type }: { type: RedirectExplorer }) {
const { suiscanUrl, suivisionUrl } = useRedirectExplorerUrl();
const href = type === RedirectExplorer.SUISCAN ? suiscanUrl : suivisionUrl;
const openWithLabel =
type === RedirectExplorer.SUISCAN ? 'Open on Suiscan.xyz' : 'Open on Suivision.xyz';

return (
<ButtonOrLink
href={href}
target="_blank"
className="flex items-center gap-2 border-b border-gray-100 py-1 text-heading5 font-semibold"
onClick={() => {
ampli.redirectToExternalExplorer({
name: type,
url: href,
});
}}
>
{openWithLabel} <ArrowRight12 className="h-4 w-4 -rotate-45" />
</ButtonOrLink>
);
}

export function RedirectHeader() {
const { hasMatch } = useRedirectExplorerUrl();
const redirectExplorers = useRedirectExplorerOrder();

return (
<section
className="mb-20 flex flex-col items-center justify-center gap-5 px-5 py-12 text-center"
style={{
background: 'linear-gradient(159deg, #FAF8D2 50.65%, #F7DFD5 86.82%)',
}}
>
<div className="flex items-center gap-1">
<Sui className={clsx(hasMatch ? 'h-7.5 w-5' : 'h-11 w-9')} />
<SuiLogoTxt className={clsx(hasMatch ? 'h-5 w-7.5' : 'h-7 w-11')} />
</div>

{hasMatch ? (
<div className="flex flex-col gap-2">
<Text variant="body/medium">
The link that brought you here is no longer available on suiexplorer.com
</Text>
<div className="flex flex-col items-center justify-center gap-5 sm:flex-row">
{redirectExplorers.map((type) => (
<HeaderLink key={type} type={type} />
))}
</div>
</div>
) : (
<Heading variant="heading3/semibold">
Experience two amazing blockchain explorers on Sui!
</Heading>
)}
</section>
);
}

export function PageLayout({ gradient, content, loading, isError }: PageLayoutProps) {
const enableExplorerRedirect = useFeatureIsOn('explorer-redirect');
const [network] = useNetworkContext();
const { request } = useAppsBackend();
const outageOverride = useFeatureIsOn('network-outage-override');
Expand Down Expand Up @@ -61,8 +212,9 @@ export function PageLayout({ gradient, content, loading, isError }: PageLayoutPr
<div className="break-normal">{networkDegradeBannerCopy}</div>
</Banner>
)}
<Header />
{!enableExplorerRedirect && <Header />}
</section>
{enableExplorerRedirect && <RedirectHeader />}
{loading && (
<div className="absolute left-1/2 right-0 top-1/2 flex -translate-x-1/2 -translate-y-1/2 transform justify-center">
<LoadingIndicator variant="lg" />
Expand All @@ -78,7 +230,7 @@ export function PageLayout({ gradient, content, loading, isError }: PageLayoutPr
: {}
}
>
{isGradientVisible ? (
{isGradientVisible && !enableExplorerRedirect ? (
<section
style={{
paddingTop: `${headerHeight}px`,
Expand All @@ -103,7 +255,7 @@ export function PageLayout({ gradient, content, loading, isError }: PageLayoutPr
) : null}
{!loading && (
<section className="mx-auto max-w-[1440px] p-5 pb-20 sm:py-8 md:p-10 md:pb-20">
{content}
{enableExplorerRedirect ? <RedirectContent /> : content}
</section>
)}
</main>
Expand Down
135 changes: 135 additions & 0 deletions apps/explorer/src/hooks/useRedirectExplorerUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
import { matchRoutes, useLocation, useParams } from 'react-router-dom';
import { useNetworkContext } from '~/context';
import { Network } from '~/utils/api/DefaultRpcClient';
import { useMemo } from 'react';
import { useGetObject } from '../../../core';
import { translate } from '~/pages/object-result/ObjectResultType';

const SUISCAN_URL_MAINNET = 'https://suiscan.xyz';
const SUISCAN_URL_TESTNET = 'https://suiscan.xyz/testnet';
const SUISCAN_URL_DEVNET = 'https://suiscan.xyz/devnet';
const SUIVISION_URL_MAINNET = 'https://suivision.xyz';
const SUIVISION_URL_TESTNET = 'https://testnet.suivision.xyz';
const SUIVISION_URL_DEVNET = 'https://suivision.xyz';

enum Routes {
object = '/object/:id',
checkpoint = '/checkpoint/:id',
txblock = '/txblock/:id',
epoch = '/epoch/:id',
address = '/address/:id',
validator = '/validator/:id',
validators = '/validators',
}

function useMatchPath() {
const location = useLocation();
const someRoutes = [
{ path: Routes.object },
{ path: Routes.checkpoint },
{ path: Routes.txblock },
{ path: Routes.epoch },
{ path: Routes.address },
{ path: Routes.validator },
{ path: Routes.validators },
];
const matches = matchRoutes(someRoutes, location);
return matches?.[0]?.route.path;
}

export function useRedirectUrl(isPackage?: boolean) {
const [network] = useNetworkContext();
const { id } = useParams();

const matchPath = useMatchPath();
const hasMatch = Boolean(matchPath);

const baseUrl = useMemo(() => {
switch (network) {
case Network.DEVNET:
return {
suiscan: SUISCAN_URL_DEVNET,
suivision: SUIVISION_URL_DEVNET,
};
case Network.TESTNET:
return {
suiscan: SUISCAN_URL_TESTNET,
suivision: SUIVISION_URL_TESTNET,
};
default:
return {
suiscan: SUISCAN_URL_MAINNET,
suivision: SUIVISION_URL_MAINNET,
};
}
}, [network]);

const redirectPathname = useMemo(() => {
switch (matchPath) {
case Routes.object:
return {
suiscan: `/object/${id}`,
suivision: isPackage ? `/package/${id}` : `/object/${id}`,
};
case Routes.checkpoint:
return {
suiscan: `/checkpoint/${id}`,
suivision: `/checkpoint/${id}`,
};
case Routes.txblock:
return {
suiscan: `/tx/${id}`,
suivision: `/txblock/${id}`,
};
case Routes.epoch:
return {
suiscan: `/epoch/${id}`,
suivision: `/epoch/${id}`,
};
case Routes.address:
return {
suiscan: `/address/${id}`,
suivision: `/address/${id}`,
};
case Routes.validator:
return {
suiscan: `/validator/${id}`,
suivision: `/validator/${id}`,
};
case Routes.validators:
return {
suiscan: `/validators`,
suivision: `/validators`,
};
default: {
return {
suiscan: '/',
suivision: '/',
};
}
}
}, [id, matchPath, isPackage]);

return {
suivisionUrl: `${baseUrl.suivision}${redirectPathname.suivision}`,
suiscanUrl: `${baseUrl.suiscan}${redirectPathname.suiscan}`,
hasMatch,
};
}

function useRedirectObject() {
const { id } = useParams();
const { data, isError } = useGetObject(id);
const resp = data && !isError ? translate(data) : null;
const isPackage = resp ? resp.objType === 'Move Package' : false;

return useRedirectUrl(isPackage);
}

export function useRedirectExplorerUrl() {
const matchPath = useMatchPath();
const useRedirectHook = matchPath === Routes.object ? useRedirectObject : useRedirectUrl;
return useRedirectHook();
}
Loading

0 comments on commit 65fa5fb

Please sign in to comment.