2019-04-17 10:44:48 +00:00
|
|
|
import { orderBy } from "lodash";
|
2019-04-17 14:44:23 +00:00
|
|
|
import moment from "moment";
|
2019-07-18 15:20:09 +00:00
|
|
|
import * as numeral from "numeral";
|
2019-07-23 12:20:34 +00:00
|
|
|
import React from "react";
|
2019-04-17 10:44:48 +00:00
|
|
|
import { connect } from "react-redux";
|
2019-04-17 14:44:23 +00:00
|
|
|
import sanitize from "sanitize-html";
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2018-09-03 18:15:28 +00:00
|
|
|
import {
|
2019-04-17 10:44:48 +00:00
|
|
|
AnchorButton,
|
|
|
|
Button,
|
2019-07-18 20:05:16 +00:00
|
|
|
Callout,
|
2019-04-17 10:44:48 +00:00
|
|
|
Classes,
|
|
|
|
Code,
|
|
|
|
Divider,
|
|
|
|
H2,
|
|
|
|
HTMLTable,
|
2019-07-18 15:20:09 +00:00
|
|
|
Icon,
|
2019-04-17 10:44:48 +00:00
|
|
|
NonIdealState,
|
|
|
|
Position,
|
2019-07-23 12:20:34 +00:00
|
|
|
Spinner,
|
2019-04-17 10:44:48 +00:00
|
|
|
Tab,
|
|
|
|
Tabs,
|
|
|
|
Tooltip
|
|
|
|
} from "@blueprintjs/core";
|
|
|
|
import { IconNames } from "@blueprintjs/icons";
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-07-23 12:20:34 +00:00
|
|
|
import { push } from "connected-react-router";
|
2019-07-21 18:05:07 +00:00
|
|
|
import { Link } from "react-router-dom";
|
2019-07-23 12:20:34 +00:00
|
|
|
import { Dispatch } from "redux";
|
2019-07-21 18:05:07 +00:00
|
|
|
import styled from "styled-components";
|
|
|
|
import { IAppState, IGraph, IInstanceDetails } from "../../redux/types";
|
|
|
|
import { domainMatchSelector } from "../../util";
|
|
|
|
import { ErrorState } from "../molecules/";
|
|
|
|
|
2019-07-23 12:20:34 +00:00
|
|
|
const InstanceScreenContainer = styled.div`
|
|
|
|
margin-bottom: auto;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
flex: 1;
|
2019-07-21 18:05:07 +00:00
|
|
|
`;
|
2019-07-23 12:20:34 +00:00
|
|
|
const HeadingContainer = styled.div`
|
|
|
|
display: flex;
|
|
|
|
flex-direction: row;
|
|
|
|
align-items: center;
|
2019-07-21 18:05:07 +00:00
|
|
|
width: 100%;
|
|
|
|
`;
|
2019-07-23 12:20:34 +00:00
|
|
|
const StyledHeadingH2 = styled(H2)`
|
|
|
|
margin: 0;
|
|
|
|
`;
|
|
|
|
const StyledCloseButton = styled(Button)`
|
|
|
|
justify-self: flex-end;
|
|
|
|
`;
|
|
|
|
const StyledHeadingTooltip = styled(Tooltip)`
|
|
|
|
margin-left: 5px;
|
|
|
|
flex-grow: 1;
|
2019-07-21 18:05:07 +00:00
|
|
|
`;
|
|
|
|
const StyledHTMLTable = styled(HTMLTable)`
|
|
|
|
width: 100%;
|
|
|
|
`;
|
|
|
|
const StyledLinkToFdNetwork = styled.div`
|
|
|
|
text-align: center;
|
2019-07-23 12:20:34 +00:00
|
|
|
margin-top: auto;
|
2019-07-21 18:05:07 +00:00
|
|
|
`;
|
2019-07-23 12:20:34 +00:00
|
|
|
const StyledTabs = styled(Tabs)`
|
|
|
|
width: 100%;
|
|
|
|
`;
|
|
|
|
interface IInstanceScreenProps {
|
2019-04-17 10:44:48 +00:00
|
|
|
graph?: IGraph;
|
|
|
|
instanceName: string | null;
|
|
|
|
instanceLoadError: boolean;
|
|
|
|
instanceDetails: IInstanceDetails | null;
|
|
|
|
isLoadingInstanceDetails: boolean;
|
2019-07-23 12:20:34 +00:00
|
|
|
navigateToRoot: () => void;
|
2018-09-01 17:24:05 +00:00
|
|
|
}
|
2019-07-23 12:20:34 +00:00
|
|
|
interface IInstanceScreenState {
|
2019-07-18 20:05:16 +00:00
|
|
|
neighbors?: string[];
|
|
|
|
isProcessingNeighbors: boolean;
|
2018-09-04 19:29:37 +00:00
|
|
|
}
|
2019-07-23 12:20:34 +00:00
|
|
|
class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInstanceScreenState> {
|
|
|
|
public constructor(props: IInstanceScreenProps) {
|
2019-04-17 10:44:48 +00:00
|
|
|
super(props);
|
2019-07-23 12:20:34 +00:00
|
|
|
this.state = { isProcessingNeighbors: false };
|
|
|
|
}
|
|
|
|
|
|
|
|
public render() {
|
|
|
|
let content;
|
|
|
|
if (this.props.isLoadingInstanceDetails || this.state.isProcessingNeighbors) {
|
|
|
|
content = this.renderLoadingState();
|
|
|
|
} else if (!this.props.instanceDetails) {
|
|
|
|
return this.renderEmptyState();
|
|
|
|
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("personal instance") > -1) {
|
|
|
|
content = this.renderPersonalInstanceErrorState();
|
|
|
|
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("robots.txt") > -1) {
|
|
|
|
content = this.renderRobotsTxtState();
|
|
|
|
} else if (this.props.instanceDetails.status !== "success") {
|
|
|
|
content = this.renderMissingDataState();
|
|
|
|
} else if (this.props.instanceLoadError) {
|
|
|
|
return (content = <ErrorState />);
|
|
|
|
} else {
|
|
|
|
content = this.renderTabs();
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<InstanceScreenContainer>
|
|
|
|
<HeadingContainer>
|
|
|
|
<StyledHeadingH2>{this.props.instanceName}</StyledHeadingH2>
|
|
|
|
<StyledHeadingTooltip content="Open link in new tab" position={Position.TOP} className={Classes.DARK}>
|
|
|
|
<AnchorButton icon={IconNames.LINK} minimal={true} onClick={this.openInstanceLink} />
|
|
|
|
</StyledHeadingTooltip>
|
|
|
|
<StyledCloseButton icon={IconNames.CROSS} onClick={this.props.navigateToRoot} />
|
|
|
|
</HeadingContainer>
|
|
|
|
<Divider />
|
|
|
|
{content}
|
|
|
|
</InstanceScreenContainer>
|
|
|
|
);
|
2019-07-18 20:05:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public componentDidMount() {
|
|
|
|
this.processEdgesToFindNeighbors();
|
|
|
|
}
|
|
|
|
|
2019-07-23 14:08:43 +00:00
|
|
|
public componentDidUpdate(prevProps: IInstanceScreenProps) {
|
|
|
|
const isNewInstance = prevProps.instanceName !== this.props.instanceName;
|
|
|
|
const receivedNewEdges = !!this.props.graph && !this.state.isProcessingNeighbors && !this.state.neighbors;
|
|
|
|
if (isNewInstance || receivedNewEdges) {
|
2019-07-18 20:05:16 +00:00
|
|
|
this.processEdgesToFindNeighbors();
|
|
|
|
}
|
2019-04-17 10:44:48 +00:00
|
|
|
}
|
2018-09-04 19:29:37 +00:00
|
|
|
|
2019-07-18 20:05:16 +00:00
|
|
|
private processEdgesToFindNeighbors = () => {
|
|
|
|
const { graph, instanceName } = this.props;
|
|
|
|
if (!graph || !instanceName) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.setState({ isProcessingNeighbors: true });
|
|
|
|
const edges = graph.edges.filter(e => [e.data.source, e.data.target].indexOf(instanceName!) > -1);
|
|
|
|
const neighbors: any[] = [];
|
|
|
|
edges.forEach(e => {
|
|
|
|
if (e.data.source === instanceName) {
|
|
|
|
neighbors.push({ neighbor: e.data.target, weight: e.data.weight });
|
|
|
|
} else {
|
|
|
|
neighbors.push({ neighbor: e.data.source, weight: e.data.weight });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.setState({ neighbors, isProcessingNeighbors: false });
|
|
|
|
};
|
|
|
|
|
2019-07-19 18:19:53 +00:00
|
|
|
private renderTabs = () => {
|
|
|
|
const hasNeighbors = this.state.neighbors && this.state.neighbors.length > 0;
|
|
|
|
|
2019-07-23 12:20:34 +00:00
|
|
|
const insularCallout =
|
|
|
|
this.props.graph && !this.state.isProcessingNeighbors && !hasNeighbors ? (
|
|
|
|
<Callout icon={IconNames.INFO_SIGN} title="Insular instance">
|
|
|
|
<p>This instance doesn't have any neighbors that we know of, so it's hidden from the graph.</p>
|
|
|
|
</Callout>
|
|
|
|
) : (
|
|
|
|
undefined
|
|
|
|
);
|
2019-07-19 18:19:53 +00:00
|
|
|
return (
|
2019-07-23 12:20:34 +00:00
|
|
|
<>
|
2019-07-19 18:19:53 +00:00
|
|
|
{insularCallout}
|
2019-07-23 12:20:34 +00:00
|
|
|
<StyledTabs>
|
2019-07-19 18:19:53 +00:00
|
|
|
{this.props.instanceDetails!.description && (
|
2019-04-17 10:44:48 +00:00
|
|
|
<Tab id="description" title="Description" panel={this.renderDescription()} />
|
|
|
|
)}
|
|
|
|
{this.shouldRenderStats() && <Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
|
|
|
|
<Tab id="neighbors" title="Neighbors" panel={this.renderNeighbors()} />
|
|
|
|
<Tab id="peers" title="Known peers" panel={this.renderPeers()} />
|
2019-07-23 12:20:34 +00:00
|
|
|
</StyledTabs>
|
2019-07-21 18:05:07 +00:00
|
|
|
<StyledLinkToFdNetwork>
|
2019-07-23 12:20:34 +00:00
|
|
|
<AnchorButton
|
2019-07-21 18:05:07 +00:00
|
|
|
href={`https://fediverse.network/${this.props.instanceName}`}
|
2019-07-23 12:20:34 +00:00
|
|
|
minimal={true}
|
|
|
|
rightIcon={IconNames.SHARE}
|
2019-07-21 18:05:07 +00:00
|
|
|
target="_blank"
|
2019-07-23 12:20:34 +00:00
|
|
|
text="See more statistics at fediverse.network"
|
|
|
|
/>
|
2019-07-21 18:05:07 +00:00
|
|
|
</StyledLinkToFdNetwork>
|
2019-07-23 12:20:34 +00:00
|
|
|
</>
|
2019-04-17 10:44:48 +00:00
|
|
|
);
|
|
|
|
};
|
2018-09-04 19:29:37 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private shouldRenderStats = () => {
|
|
|
|
const details = this.props.instanceDetails;
|
|
|
|
return details && (details.version || details.userCount || details.statusCount || details.domainCount);
|
|
|
|
};
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderDescription = () => {
|
|
|
|
const description = this.props.instanceDetails!.description;
|
|
|
|
if (!description) {
|
|
|
|
return;
|
2018-09-03 18:15:28 +00:00
|
|
|
}
|
2019-04-17 10:44:48 +00:00
|
|
|
return <p className={Classes.RUNNING_TEXT} dangerouslySetInnerHTML={{ __html: sanitize(description) }} />;
|
|
|
|
};
|
2018-09-03 18:15:28 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderVersionAndCounts = () => {
|
2019-07-18 15:20:09 +00:00
|
|
|
if (!this.props.instanceDetails) {
|
|
|
|
throw new Error("Did not receive instance details as expected!");
|
|
|
|
}
|
|
|
|
const { version, userCount, statusCount, domainCount, lastUpdated, insularity } = this.props.instanceDetails;
|
2019-04-17 10:44:48 +00:00
|
|
|
return (
|
2019-07-23 12:20:34 +00:00
|
|
|
<StyledHTMLTable small={true} striped={true}>
|
|
|
|
<tbody>
|
|
|
|
<tr>
|
|
|
|
<td>Version</td>
|
|
|
|
<td>{<Code>{version}</Code> || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td>Users</td>
|
|
|
|
<td>{(userCount && numeral.default(userCount).format("0,0")) || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td>Statuses</td>
|
|
|
|
<td>{(statusCount && numeral.default(statusCount).format("0,0")) || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td>
|
|
|
|
Insularity{" "}
|
|
|
|
<Tooltip
|
|
|
|
content={
|
|
|
|
<span>
|
|
|
|
The percentage of mentions that are directed
|
|
|
|
<br />
|
|
|
|
toward users on the same instance.
|
|
|
|
</span>
|
|
|
|
}
|
|
|
|
position={Position.TOP}
|
|
|
|
className={Classes.DARK}
|
|
|
|
>
|
|
|
|
<Icon icon={IconNames.HELP} iconSize={Icon.SIZE_STANDARD} />
|
|
|
|
</Tooltip>
|
|
|
|
</td>
|
|
|
|
<td>{(insularity && numeral.default(insularity).format("0.0%")) || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td>Known peers</td>
|
|
|
|
<td>{(domainCount && numeral.default(domainCount).format("0,0")) || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td>Last updated</td>
|
|
|
|
<td>{moment(lastUpdated + "Z").fromNow() || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
</tbody>
|
|
|
|
</StyledHTMLTable>
|
2019-04-17 10:44:48 +00:00
|
|
|
);
|
|
|
|
};
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderNeighbors = () => {
|
|
|
|
if (!this.props.graph || !this.props.instanceName) {
|
|
|
|
return;
|
2018-09-01 17:24:05 +00:00
|
|
|
}
|
2019-07-18 10:21:12 +00:00
|
|
|
const edges = this.props.graph.edges.filter(
|
|
|
|
e => [e.data.source, e.data.target].indexOf(this.props.instanceName!) > -1
|
|
|
|
);
|
2019-04-17 10:44:48 +00:00
|
|
|
const neighbors: any[] = [];
|
|
|
|
edges.forEach(e => {
|
2019-07-18 10:21:12 +00:00
|
|
|
if (e.data.source === this.props.instanceName) {
|
|
|
|
neighbors.push({ neighbor: e.data.target, weight: e.data.weight });
|
2019-04-17 10:44:48 +00:00
|
|
|
} else {
|
2019-07-18 10:21:12 +00:00
|
|
|
neighbors.push({ neighbor: e.data.source, weight: e.data.weight });
|
2019-04-17 10:44:48 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
const neighborRows = orderBy(neighbors, ["weight"], ["desc"]).map((neighborDetails: any, idx: number) => (
|
|
|
|
<tr key={idx}>
|
|
|
|
<td>
|
2019-07-21 18:05:07 +00:00
|
|
|
<Link
|
|
|
|
to={`/instance/${neighborDetails.neighbor}`}
|
|
|
|
className={`${Classes.BUTTON} ${Classes.MINIMAL}`}
|
|
|
|
role="button"
|
|
|
|
>
|
2019-04-17 10:44:48 +00:00
|
|
|
{neighborDetails.neighbor}
|
2019-07-21 18:05:07 +00:00
|
|
|
</Link>
|
2019-04-17 10:44:48 +00:00
|
|
|
</td>
|
|
|
|
<td>{neighborDetails.weight.toFixed(4)}</td>
|
|
|
|
</tr>
|
|
|
|
));
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<p className={Classes.TEXT_MUTED}>
|
|
|
|
The mention ratio is the average of how many times the two instances mention each other per status. A mention
|
|
|
|
ratio of 1 would mean that every single status contained a mention of a user on the other instance.
|
|
|
|
</p>
|
2019-07-21 18:05:07 +00:00
|
|
|
<StyledHTMLTable small={true} striped={true} interactive={false}>
|
2019-04-17 10:44:48 +00:00
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<th>Instance</th>
|
|
|
|
<th>Mention ratio</th>
|
2018-09-03 19:30:11 +00:00
|
|
|
</tr>
|
2019-04-17 10:44:48 +00:00
|
|
|
</thead>
|
|
|
|
<tbody>{neighborRows}</tbody>
|
2019-07-21 18:05:07 +00:00
|
|
|
</StyledHTMLTable>
|
2019-04-17 10:44:48 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
2018-09-03 19:30:11 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderPeers = () => {
|
|
|
|
const peers = this.props.instanceDetails!.peers;
|
|
|
|
if (!peers || peers.length === 0) {
|
|
|
|
return;
|
2018-09-01 17:24:05 +00:00
|
|
|
}
|
2019-04-17 10:44:48 +00:00
|
|
|
const peerRows = peers.map(instance => (
|
2019-07-21 18:05:07 +00:00
|
|
|
<tr key={instance.name}>
|
2019-04-17 10:44:48 +00:00
|
|
|
<td>
|
2019-07-21 18:05:07 +00:00
|
|
|
<Link to={`/instance/${instance.name}`} className={`${Classes.BUTTON} ${Classes.MINIMAL}`} role="button">
|
2019-04-17 10:44:48 +00:00
|
|
|
{instance.name}
|
2019-07-21 18:05:07 +00:00
|
|
|
</Link>
|
2019-04-17 10:44:48 +00:00
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
));
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<p className={Classes.TEXT_MUTED}>
|
|
|
|
All the instances, past and present, that {this.props.instanceName} knows about.
|
|
|
|
</p>
|
2019-07-21 18:05:07 +00:00
|
|
|
<StyledHTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table">
|
2019-04-17 10:44:48 +00:00
|
|
|
<tbody>{peerRows}</tbody>
|
2019-07-21 18:05:07 +00:00
|
|
|
</StyledHTMLTable>
|
2019-04-17 10:44:48 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderEmptyState = () => {
|
|
|
|
return (
|
|
|
|
<NonIdealState
|
|
|
|
icon={IconNames.CIRCLE}
|
|
|
|
title="No instance selected"
|
|
|
|
description="Select an instance from the graph or the top-right dropdown to see its details."
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-07-23 12:20:34 +00:00
|
|
|
private renderLoadingState = () => <NonIdealState icon={<Spinner />} />;
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderPersonalInstanceErrorState = () => {
|
|
|
|
return (
|
|
|
|
<NonIdealState
|
|
|
|
icon={IconNames.BLOCKED_PERSON}
|
|
|
|
title="No data"
|
|
|
|
description="This instance has fewer than 10 users. It was not crawled in order to protect their privacy, but if it's your instance you can opt in."
|
|
|
|
action={
|
2019-07-19 18:19:53 +00:00
|
|
|
<AnchorButton icon={IconNames.CONFIRM} href="https://cursed.technology/@fediversespace" target="_blank">
|
|
|
|
Message @fediversespace to opt in
|
2019-04-17 10:44:48 +00:00
|
|
|
</AnchorButton>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
2018-09-03 18:15:28 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderMissingDataState = () => {
|
|
|
|
return (
|
2019-07-23 12:20:34 +00:00
|
|
|
<>
|
2019-07-18 20:05:16 +00:00
|
|
|
<NonIdealState
|
|
|
|
icon={IconNames.ERROR}
|
|
|
|
title="No data"
|
|
|
|
description="This instance could not be crawled. Either it was down or it's an instance type we don't support yet."
|
|
|
|
/>
|
|
|
|
<span className="sidebar-hidden-instance-status" style={{ display: "none" }}>
|
|
|
|
{this.props.instanceDetails && this.props.instanceDetails.status}
|
|
|
|
</span>
|
2019-07-23 12:20:34 +00:00
|
|
|
</>
|
2019-04-19 14:29:45 +00:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2019-07-20 17:43:37 +00:00
|
|
|
private renderRobotsTxtState = () => {
|
|
|
|
return (
|
|
|
|
<NonIdealState
|
2019-07-20 18:08:56 +00:00
|
|
|
icon={
|
|
|
|
<span role="img" aria-label="robot">
|
|
|
|
🤖
|
|
|
|
</span>
|
|
|
|
}
|
2019-07-20 17:43:37 +00:00
|
|
|
title="No data"
|
|
|
|
description="This instance was not crawled because its robots.txt did not allow us to."
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private openInstanceLink = () => {
|
|
|
|
window.open("https://" + this.props.instanceName, "_blank");
|
|
|
|
};
|
2018-09-01 17:24:05 +00:00
|
|
|
}
|
|
|
|
|
2019-07-21 18:05:07 +00:00
|
|
|
const mapStateToProps = (state: IAppState) => {
|
|
|
|
const match = domainMatchSelector(state);
|
|
|
|
return {
|
|
|
|
graph: state.data.graph,
|
|
|
|
instanceDetails: state.currentInstance.currentInstanceDetails,
|
|
|
|
instanceLoadError: state.currentInstance.error,
|
|
|
|
instanceName: match && match.params.domain,
|
|
|
|
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails
|
|
|
|
};
|
|
|
|
};
|
2019-07-23 12:20:34 +00:00
|
|
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
|
|
|
navigateToRoot: () => dispatch(push("/"))
|
|
|
|
});
|
|
|
|
const InstanceScreen = connect(
|
|
|
|
mapStateToProps,
|
|
|
|
mapDispatchToProps
|
|
|
|
)(InstanceScreenImpl);
|
|
|
|
export default InstanceScreen;
|