Skip to content

Commit

Permalink
[explorer] Reinstate test suite, fix latency issues and split view/fe…
Browse files Browse the repository at this point in the history
…tch logic (MystenLabs#1070)

* bring all changes to explorer from explorer-rest branch

* remove commented out code

* remove unused vars in rpc.ts

* remove more dead code in rpc

* fix sui addr regex in rpc lib

* fix indentation for isValidHttpUrl

* clarify TODOs in rpc

* use unattached var in constructor

* fix equality check with SUI_ADDRESS_LEN

* remove unused type param U in modifyForDemo

* make setShowDescription call a const ()=>

* make setShowProperties call a const ()=>

* make setShowConnectedEntities call a const ()=>

* convert onClick functions from arrow func to function()

* remove return statements from void functions

* jsx-no-bind back to error

* Delete rpc_basictest.ts

* remove console.logs and dead code

* remove console log in address result

* remove utility file console logs

* convert functions to useCallback() memoized versions

* export isValidSuiIdBytes

* restore copyright in head and footer.tsx

* restore copyright to App.tsx

* removes conversion scripts folder

* resolves lint messages including error that hooks were conditionally called

* prevents display of Address Results pages with no objects

* remove SuiParentChildRef

* remove modifyForDemo

* add var for data.data.contents

* move string utils into their own file

* cleanup ObjectResult imports

* split utility funcs file into search and mock

* change searchUtil formatting

* improve searchUtil TODO

* remove duplicate hexToAscii function

* move isValidHttpUrl to stringUtils

* change unused map vars to _

* move rpc settings handling into separate file, rpc -> SuiRpcClient

* fix imports with new rpc file

* move isSuiAddressHex into file and fix length

* SuiAddressBytes -> AddressBytes

* remove isValidSuiIdBytes

* SuiAddressHexStr -> AddressHexStr

* fix rpc setting import

* remove two unused rpc types

* update MoveVec type, remove unused const

* allow any[] type in moveCall

* demo_types -> demoTypes

* prettier changes

* static transactions data displays only when tests are run

* static address data displays only when tests are run

* search says Please Wait while data is loading

* objectId and category being objects have same effect thus rationalized

* longtext clickable links say Please Wait while data is loading

* split Object data extraction from view

* standardize data fetching for object and address result

* ObjectResult Types in one place

* static object data displays only when tests are run

* removes warnings from tests

* creates placeholder for smart contract data

* reapply naming convention from explorer-merge

* reapply removal of rpc

* resolves missing link when clicking through levels of ownership of monster

* implements 'No Image was Found' rather than 'Please Wait' when image is missing

* reintroduce refactoring from explorer-merge

* comments out CSS code for smart contract display

* updates README to provide guidance

* reintroduce license details

* removes dollar sign from README

Co-authored-by: Stella Cannefax <[email protected]>
  • Loading branch information
apburnie and Stella Cannefax authored Mar 28, 2022
1 parent a17dc2b commit 59297d6
Show file tree
Hide file tree
Showing 29 changed files with 1,005 additions and 755 deletions.
24 changes: 22 additions & 2 deletions explorer/client/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Sui Explorer Client
# SuiExplorer Client

## Development
# Set Up

**Requirements**: Node 14.0.0 or later version

Expand All @@ -10,6 +10,26 @@ In the project directory, run:

Before running any of the following scripts `npm i` must run in order to install the necessary dependencies.

# How to Switch Environment

The purpose of the SuiExplorer Client is to present data extracted from a real or theoretical Sui Network.

What the 'Sui Network' is varies according to the environment variable `REACT_APP_DATA`.

When running most of the below npm commands, the SuiExplorer Client will extract and present data from the Sui Network connected to the URL https://demo-rpc.sui.io.

If the environment variable `REACT_APP_DATA` is set to `static`, then the SuiExplorer will instead pull data from a local, static JSON dataset that can be found at `./src/utils/static/mock_data.json`.

For example, suppose we wish to locally run the website using the static JSON dataset and not the API, then we would run the following:

```bash
REACT_APP_DATA=static npm start
```

Note that the command `npm run test` is the exception. Here the SuiExplorer will instead use the static JSON dataset. The tests have been written to specifically check the UI and not the API connection.

## NPM Commands and what they do

### `npm start`

Runs the app in the development mode.
Expand Down
2 changes: 1 addition & 1 deletion explorer/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"start": "react-scripts start",
"start:dev": "concurrently \"npm:start\" \"npm:prettier:fix:watch\"",
"build": "react-scripts build",
"test": "react-scripts test",
"test": "REACT_APP_DATA=static react-scripts test",
"eslint:check": "eslint --max-warnings=0 .eslintrc.js \"./src/**/*.{js,jsx,ts,tsx}\"",
"eslint:fix": "npm run eslint:check -- --fix",
"prettier:check": "prettier -c --ignore-unknown .",
Expand Down
149 changes: 136 additions & 13 deletions explorer/client/src/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,64 @@ import {

import App from '../app/App';

function expectHome() {
expect(screen.getByTestId('home-page')).toBeInTheDocument();
}
const expectHome = async () => {
const el = await screen.findByTestId('home-page');
expect(el).toBeInTheDocument();
};

function searchText(text: string) {
const searchText = (text: string) => {
fireEvent.change(screen.getByPlaceholderText(/Search by ID/i), {
target: { value: text },
});
fireEvent.submit(screen.getByRole('form', { name: /search form/i }));
}
};

const expectTransactionStatus = async (
result: 'fail' | 'success' | 'pending'
) => {
const el = await screen.findByTestId('transaction-status');
expect(el).toHaveTextContent(result);
};

const expectReadOnlyStatus = (result: 'True' | 'False') => {
const el = screen.getByTestId('read-only-text');
expect(el).toHaveTextContent(result);
};

const successTransactionID = 'txCreateSuccess';
const failTransactionID = 'txFails';
const pendingTransactionID = 'txSendPending';

const problemTransactionID = 'txProblem';

const successObjectID = 'CollectionObject';
const problemObjectID = 'ProblemObject';

const noDataID = 'nonsenseQuery';

const readOnlyObject = 'ComponentObject';
const notReadOnlyObject = 'CollectionObject';

const addressID = 'receiverAddress';

const problemAddressID = 'problemAddress';

describe('End-to-end Tests', () => {
it('renders the home page', () => {
it('renders the home page', async () => {
render(<App />, { wrapper: MemoryRouter });
expectHome();
await expectHome();
});

describe('Redirects to Home Page', () => {
it('redirects to home for every unknown path', () => {
it('redirects to home for every unknown path', async () => {
render(
<MemoryRouter initialEntries={['/anything']}>
<App />
</MemoryRouter>
);
expectHome();
await expectHome();
});
it('redirects to home for unknown path by replacing the history', () => {
it('redirects to home for unknown path by replacing the history', async () => {
const history = createMemoryHistory({
initialEntries: ['/anything'],
});
Expand All @@ -45,17 +76,109 @@ describe('End-to-end Tests', () => {
<App />
</HistoryRouter>
);
expectHome();
await expectHome();
expect(history.index).toBe(0);
});
});

describe('Displays data on transactions', () => {
it('when transaction was a success', async () => {
render(<App />, { wrapper: MemoryRouter });
searchText(successTransactionID);
await expectTransactionStatus('success');
expect(
await screen.findByText('Transaction ID')
).toBeInTheDocument();
expect(await screen.findByText('From')).toBeInTheDocument();
expect(await screen.findByText('Event')).toBeInTheDocument();
expect(await screen.findByText('Object')).toBeInTheDocument();
expect(await screen.findByText('To')).toBeInTheDocument();
});
it('when transaction was a failure', () => {
render(<App />, { wrapper: MemoryRouter });
searchText(failTransactionID);
expectTransactionStatus('fail');
});

it('when transaction was pending', () => {
render(<App />, { wrapper: MemoryRouter });
searchText(pendingTransactionID);
expectTransactionStatus('pending');
});

it('when transaction data has missing info', () => {
render(<App />, { wrapper: MemoryRouter });
searchText(problemTransactionID);
expect(
screen.getByText(
'There was an issue with the data on the following transaction'
)
).toBeInTheDocument();
});
});

describe('Displays data on objects', () => {
it('when object was a success', () => {
render(<App />, { wrapper: MemoryRouter });
searchText(successObjectID);
expect(screen.getByText('Object ID')).toBeInTheDocument();
});

it('when object is read only', () => {
render(<App />, { wrapper: MemoryRouter });
searchText(readOnlyObject);
expectReadOnlyStatus('True');
});

it('when object is not read only', () => {
render(<App />, { wrapper: MemoryRouter });
searchText(notReadOnlyObject);
expectReadOnlyStatus('False');
});

it('when object data has missing info', () => {
render(<App />, { wrapper: MemoryRouter });
searchText(problemObjectID);
expect(
screen.getByText(
'There was an issue with the data on the following object'
)
).toBeInTheDocument();
});
});

describe('Displays data on addresses', () => {
it('when address has required fields', () => {
render(<App />, { wrapper: MemoryRouter });
searchText(addressID);
expect(screen.getByText('Address ID')).toBeInTheDocument();
expect(screen.getByText('Owned Objects')).toBeInTheDocument();
});
it('when address has missing fields', () => {
render(<App />, { wrapper: MemoryRouter });
searchText(problemAddressID);
expect(
screen.getByText(
'There was an issue with the data on the following address'
)
).toBeInTheDocument();
});
});

it('handles an ID with no associated data point', () => {
render(<App />, { wrapper: MemoryRouter });
searchText(noDataID);
expect(
screen.getByText('Data on the following query could not be found')
).toBeInTheDocument();
});

describe('Returns Home', () => {
it('when Home Button is clicked', () => {
it('when Home Button is clicked', async () => {
render(<App />, { wrapper: MemoryRouter });
searchText('Mysten Labs');
fireEvent.click(screen.getByRole('link', { name: /home button/i }));
expectHome();
await expectHome();
});
});
});
3 changes: 0 additions & 3 deletions explorer/client/src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import Footer from '../components/footer/Footer';
import Search from '../components/search/Search';
import AppRoutes from '../pages/config/AppRoutes';
Expand Down
3 changes: 0 additions & 3 deletions explorer/client/src/components/footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { Link } from 'react-router-dom';

import ExternalLink from '../external-link/ExternalLink';
Expand Down
3 changes: 0 additions & 3 deletions explorer/client/src/components/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { Link } from 'react-router-dom';

import styles from './Header.module.css';
Expand Down
25 changes: 11 additions & 14 deletions explorer/client/src/components/longtext/Longtext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ function Longtext({
| 'transactions'
| 'addresses'
| 'ethAddress'
| 'unknown'
| 'objectId';
| 'unknown';
isLink?: boolean;
}) {
const [isCopyIcon, setCopyIcon] = useState(true);
const [pleaseWait, setPleaseWait] = useState(false);
const navigate = useNavigate();

const handleCopyEvent = useCallback(() => {
Expand All @@ -36,7 +36,9 @@ function Longtext({

let icon;

if (isCopyIcon) {
if (pleaseWait) {
icon = <span className={styles.copied}>&#8987; Please Wait</span>;
} else if (isCopyIcon) {
icon = (
<span className={styles.copy} onClick={handleCopyEvent}>
<ContentCopyIcon />
Expand All @@ -46,19 +48,14 @@ function Longtext({
icon = <span className={styles.copied}>&#10003; Copied</span>;
}

const navigateUnknown = useCallback(
() => navigateWithUnknown(text, navigate),
[text, navigate]
);
const navigateUnknown = useCallback(() => {
setPleaseWait(true);
navigateWithUnknown(text, navigate).then(() => setPleaseWait(false));
}, [text, navigate]);

let textComponent;
if (isLink) {
if (category === 'objectId') {
textComponent = (
<span className={styles.longtext}>
<a href={'/objects/' + text}>{text}</a>
</span>
);
} else if (category === 'unknown') {
if (category === 'unknown') {
textComponent = (
<span className={styles.longtext} onClick={navigateUnknown}>
{text}
Expand Down
4 changes: 4 additions & 0 deletions explorer/client/src/components/search/Search.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
leading-8 flex-initial mr-[5vw] ml-0;
}

.disabled {
@apply bg-offblack hover:bg-offblack text-white cursor-text;
}

.searchtext {
@apply border-none rounded-l-md font-mono leading-8 flex-1 ml-[5vw]
text-xs mr-0 bg-offwhite;
Expand Down
18 changes: 15 additions & 3 deletions explorer/client/src/components/search/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@ function Search() {
const [input, setInput] = useState('');
const navigate = useNavigate();

const [pleaseWaitMode, setPleaseWaitMode] = useState(false);

const handleSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
navigateWithUnknown(input, navigate);
setInput('');
setPleaseWaitMode(true);
navigateWithUnknown(input, navigate).then(() => {
setInput('');
setPleaseWaitMode(false);
});
},
[input, navigate, setInput]
);
Expand All @@ -41,7 +46,14 @@ function Search() {
onChange={handleTextChange}
type="text"
/>
<input type="submit" value="Search" className={styles.searchbtn} />
<input
type="submit"
value={pleaseWaitMode ? 'Please Wait' : 'Search'}
disabled={pleaseWaitMode}
className={`${styles.searchbtn} ${
pleaseWaitMode && styles.disabled
}`}
/>
</form>
);
}
Expand Down
11 changes: 7 additions & 4 deletions explorer/client/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,24 @@ body {
@apply bg-offwhite m-0 text-offblack;
}

.ace-active-line {
/* TODO - The below CSS alters how smart contract code is displayed in the Ace Editor
.ace_active-line {
background-color: rgb(203 213 225) !important;
}
.ace-gutter-layer {
.ace_gutter-layer {
color: black !important;
}
.ace-editor {
.ace_editor {
width: 80vw !important;
font-family: 'Ubuntu Mono', 'Courier New', monospace;
}
@media only screen and (min-width: 1024px) {
.ace-editor {
.ace_editor {
width: 35rem !important;
}
}
*/
Loading

0 comments on commit 59297d6

Please sign in to comment.