deals: adds back storage deal page, simplifies file upload sidebar, adds loading state

This commit is contained in:
@wwwjim 2020-07-23 03:05:13 -07:00
parent d11fe19bab
commit 9d5de9acbe
12 changed files with 283 additions and 130 deletions

View File

@ -25,6 +25,14 @@ export const generate = (library) => [
name: "Wallet",
pageTitle: "Your wallet and addresses",
decorator: "WALLET",
children: [
{
id: 6,
name: "Deal history",
pageTitle: "Your deal history",
decorator: "DEALS",
},
],
},
...constructFilesTreeForNavigation(library),
{

View File

@ -71,6 +71,8 @@ export const getInitialState = (props) => {
data,
settings,
username,
storageList,
retrievalList,
} = props;
return {
@ -110,8 +112,9 @@ export const getInitialState = (props) => {
payment_channels_active: [],
payment_channels_redeemed: [],
data_transfers: [],
storageList,
retrievalList,
peers: transformPeers(peersList),
deals: [],
addresses: transformAddresses(addrsList, info),
library,
};

View File

@ -0,0 +1,92 @@
import * as React from "react";
import * as Strings from "~/common/strings";
import * as Constants from "~/common/constants";
import * as SVG from "~/components/system/svg";
import * as System from "~/components/system";
import { css } from "@emotion/react";
const STYLES_FILE_HIDDEN = css`
height: 1px;
width: 1px;
opacity: 0;
visibility: hidden;
position: fixed;
top: -1px;
left: -1px;
`;
const STYLES_FOCUS = css`
font-size: ${Constants.typescale.lvl1};
font-family: ${Constants.font.medium};
overflow-wrap: break-word;
width: 100%;
strong {
font-family: ${Constants.font.semiBold};
font-weight: 400;
}
`;
const STYLES_SUBTEXT = css`
margin-top: 8px;
font-size: 12px;
`;
const STYLES_ITEM = css`
margin-top: 16px;
`;
const STYLES_IMAGE_PREVIEW = css`
display: block;
width: 100%;
margin-top: 48px;
`;
export default class SidebarAddFileToBucket extends React.Component {
_handleUpload = async (e) => {
e.persist();
let file = e.target.files[0];
if (!file) {
alert("Something went wrong");
return;
}
await this.props.onSetFile({ file });
};
render() {
return (
<React.Fragment>
<System.P style={{ fontFamily: Constants.font.semiBold }}>
Upload a file to Slate
</System.P>
<input
css={STYLES_FILE_HIDDEN}
type="file"
id="file"
onChange={this._handleUpload}
/>
<System.ButtonPrimaryFull
type="label"
htmlFor="file"
style={{ marginTop: 24 }}
loading={this.props.fileLoading}
>
Add file
</System.ButtonPrimaryFull>
{!this.props.fileLoading ? (
<System.ButtonSecondaryFull
style={{ marginTop: 16 }}
onClick={this.props.onCancel}
>
Cancel
</System.ButtonSecondaryFull>
) : null}
</React.Fragment>
);
}
}

View File

@ -187,8 +187,25 @@ const STYLES_BUTTON_SECONDARY_FULL = css`
`;
export const ButtonSecondaryFull = (props) => {
if (props.loading) {
return (
<button css={STYLES_BUTTON_SECONDARY_FULL} style={props.style}>
<LoaderSpinner style={{ height: 16, width: 16 }} />
</button>
);
}
if (props.type === "label") {
return <label css={STYLES_BUTTON_SECONDARY_FULL} {...props} />;
return (
<label
css={STYLES_BUTTON_SECONDARY_FULL}
style={props.style}
onClick={props.onClick}
children={props.children}
type={props.label}
htmlFor={props.htmlFor}
/>
);
}
return <button css={STYLES_BUTTON_SECONDARY_FULL} {...props} />;

View File

@ -20,8 +20,6 @@ export const getViewer = async ({ id }) => {
// NOTE(jim): Essential for getting the right Powergate data for a user.
try {
PG.setToken(user.data.tokens.pg);
data = {
id: user.id,
data: { photo: user.data.photo },
@ -30,6 +28,8 @@ export const getViewer = async ({ id }) => {
},
username: user.username,
library: user.data.library,
storageList: [],
retrievalList: [],
peersList: null,
messageList: null,
status: null,
@ -37,10 +37,10 @@ export const getViewer = async ({ id }) => {
info: null,
};
const updates = await Utilities.refresh({ PG });
const updatesWithToken = await Utilities.refreshWithToken({
PG,
});
PG.setToken(user.data.tokens.pg);
const updates = await Utilities.refresh();
const updatesWithToken = await Utilities.refreshWithToken();
data = await Utilities.updateStateData(data, {
...updates,

View File

@ -1,13 +1,12 @@
import * as Environment from "~/node_common/environment";
import * as Constants from "./constants";
import * as Converter from "~/vendor/bytes-base64-converter.js";
import * as Strings from "~/common/strings";
import FS from "fs-extra";
import JWT from "jsonwebtoken";
import PG from "~/node_common/powergate";
import { Buckets } from "@textile/hub";
import { Libp2pCryptoIdentity } from "@textile/threads-core";
import { ffsOptions } from "@textile/powergate-client";
const BUCKET_NAME = "data";
@ -61,23 +60,8 @@ export const getBucketAPIFromUserToken = async (token) => {
return { buckets, bucketKey: root.key, bucketName: BUCKET_NAME };
};
// NOTE(jim): Requires @textile/hub
export const addFileFromFilePath = async ({ buckets, bucketKey, filePath }) => {
const file = await FS.readFileSync(filePath).buffer;
const fileName = getFileName(filePath);
const push = await buckets.pushPath(bucketKey, fileName, file);
const metadata = await buckets.pullPath(bucketKey, fileName);
const { value } = await metadata.next();
return createFile({
id: fileName,
file: Converter.bytesToBase64(value),
data: { size: 0 },
});
};
// NOTE(jim): Requires Powergate, does not require token.
export const refresh = async ({ PG }) => {
export const refresh = async () => {
const Health = await PG.health.check();
const status = Health.status ? Health.status : null;
const messageList = Health.messageList ? Health.messageList : null;
@ -89,34 +73,33 @@ export const refresh = async ({ PG }) => {
};
// NOTE(jim): Requires Powergate & authentication
export const refreshWithToken = async ({ PG }) => {
export const refreshWithToken = async () => {
const Addresses = await PG.ffs.addrs();
const addrsList = Addresses.addrsList ? Addresses.addrsList : null;
const NetworkInfo = await PG.ffs.info();
const info = NetworkInfo.info ? NetworkInfo.info : null;
return { addrsList, info };
};
const includeFinal = ffsOptions.withIncludeFinal(true);
const includePending = ffsOptions.withIncludePending(true);
const fromAddresses = ffsOptions.withFromAddresses(
info.defaultStorageConfig.cold.filecoin.addr
);
export const emitState = async ({ state, client, PG }) => {
const { peersList, messageList, status } = await refresh({ PG });
const { addrsList, info } = await refreshWithToken({ PG });
const s = await PG.ffs.listStorageDealRecords(
includeFinal,
includePending,
fromAddresses
);
const data = await updateStateData(state, {
peersList,
messageList,
status,
const r = await PG.ffs.listRetrievalDealRecords();
return {
addrsList,
info,
state,
});
if (client) {
client.send(JSON.stringify({ action: "UPDATE_VIEWER", data }));
}
return data;
storageList: s.recordsList,
retrievalList: r.recordsList,
};
};
export const getFileName = (s) => {
@ -128,28 +111,6 @@ export const getFileName = (s) => {
return target.substr(target.lastIndexOf("/") + 1);
};
export const createFile = ({ id, data, file }) => {
return {
decorator: "FILE",
id: id,
icon: "PNG",
file: getFileName(id),
miner: null,
job_id: null,
cid: null,
date: new Date(),
size: data.size,
amount: 0,
remaining: null,
data: data,
deal_category: 1,
retrieval_status: 0,
storage_status: 0,
file_data: file,
errors: [],
};
};
export const createFolder = ({ id, name }) => {
return {
decorator: "FOLDER",
@ -170,45 +131,3 @@ export const updateStateData = async (state, newState) => {
...newState,
};
};
// TODO(jim): Refactor this so we repeat this less often.
export const refreshLibrary = async ({ state, PG, FFS }) => {
let write = false;
for (let i = 0; i < state.library.length; i++) {
for (let j = 0; j < state.library[i].children.length; j++) {
if (state.library[i].children[j].job_id) {
if (state.library[i].children[j].storage_status === 1) {
console.log(
"[ prototype ] update file",
state.library[i].children[j]
);
state.library[i].children[j].storage_status = 2;
write = true;
continue;
}
PG.ffs.watchJobs((job) => {
console.log("[ prototype ] job status", job.status);
// NOTE(jim): FFS is undefined?
if (job.status >= 5) {
console.log(
"[ prototype ] update file",
state.library[i].children[j]
);
state.library[i].children[j].storage_status = 6;
write = true;
}
}, state.library[i].children[j].job_id);
}
}
}
if (write) {
FS.writeFileSync(
"./.data/library.json",
JSON.stringify({ library: state.library })
);
}
return { ...state };
};

View File

@ -28,7 +28,9 @@ export default async (req, res) => {
}
if (!files.image) {
return res.status(500).send({ decorator: "SERVER_UPLOAD", error: true });
return res
.status(500)
.send({ decorator: "SERVER_UPLOAD_NOT_IMAGE", error: true });
}
const file = files.image;

View File

@ -34,6 +34,7 @@ import SidebarAddMiner from "~/components/sidebars/SidebarAddMiner";
import SidebarAddPeer from "~/components/sidebars/SidebarAddPeer";
import SidebarNotifications from "~/components/sidebars/SidebarNotifications";
import SidebarRedeemPaymentChannel from "~/components/sidebars/SidebarRedeemPaymentChannel";
import SidebarAddFileToBucket from "~/components/sidebars/SidebarAddFileToBucket";
import ApplicationNavigation from "~/components/core/ApplicationNavigation";
import ApplicationHeader from "~/components/core/ApplicationHeader";
@ -86,7 +87,6 @@ export default class ApplicationPage extends React.Component {
currentIndex: 0,
data: null,
sidebar: null,
file: null,
};
async componentDidMount() {
@ -104,6 +104,8 @@ export default class ApplicationPage extends React.Component {
}
_handleSetFile = async ({ file }) => {
this.setState({ fileLoading: true });
let data = new FormData();
data.append("image", file);
@ -118,11 +120,17 @@ export default class ApplicationPage extends React.Component {
const response = await fetch(`/api/data/${file.name}`, options);
const json = await response.json();
if (json && json.data) {
this.setState({ file: json.data });
if (!json && json.data) {
this.setState({ fileLoading: false });
}
if (!json.data) {
this.setState({ fileLoading: false });
}
await this.rehydrate();
this.setState({ sidebar: null, fileLoading: false });
};
_handleDragEnter = (e) => {
@ -143,6 +151,13 @@ export default class ApplicationPage extends React.Component {
_handleDrop = async (e) => {
e.preventDefault();
this.setState({ fileLoading: true });
this._handleAction({
type: "SIDEBAR",
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
});
if (e.dataTransfer.items) {
for (var i = 0; i < e.dataTransfer.items.length; i++) {
if (e.dataTransfer.items[i].kind === "file") {
@ -154,7 +169,7 @@ export default class ApplicationPage extends React.Component {
}
}
this._handleAction({ type: "SIDEBAR", value: "SIDEBAR_FILE_STORAGE_DEAL" });
this.setState({ fileLoading: false });
};
rehydrate = async () => {
@ -367,6 +382,7 @@ export default class ApplicationPage extends React.Component {
SIDEBAR_CREATE_WALLET_ADDRESS: <SidebarCreateWalletAddress />,
SIDEBAR_DELETE_WALLET_ADDRESS: <SidebarDeleteWalletAddress />,
SIDEBAR_REDEEM_PAYMENT_CHANNEL: <SidebarRedeemPaymentChannel />,
SIDEBAR_ADD_FILE_TO_BUCKET: <SidebarAddFileToBucket />,
};
scenes = {
@ -453,8 +469,8 @@ export default class ApplicationPage extends React.Component {
let sidebarElement;
if (this.state.sidebar) {
sidebarElement = React.cloneElement(this.state.sidebar, {
file: this.state.file,
viewer: this.state.viewer,
fileLoading: this.state.fileLoading,
selected: this.state.selected,
onSelectedChange: this._handleSelectedChange,
onSubmit: this._handleSubmit,

View File

@ -2,13 +2,29 @@ import * as React from "react";
import * as Strings from "~/common/strings";
import * as Constants from "~/common/constants";
import * as System from "~/components/system";
import * as SchemaTable from "~/common/schema-table";
import { css } from "@emotion/react";
import Section from "~/components/core/Section";
import ScenePage from "~/components/core/ScenePage";
const STYLES_NESTED_TABLE = css`
display: grid;
grid-template-columns: 160px 1fr;
`;
const NestedTable = (data) => {
let values = [];
for (let entries of Object.entries(data)) {
if (entries[0] !== "rootCid") {
iterator += 1;
values.push(<div key={iterator}>{entries[0]}</div>);
values.push(<div key={iterator}>{entries[1]}</div>);
}
}
return <div css={STYLES_NESTED_TABLE}>{values}</div>;
};
export default class SceneDeals extends React.Component {
state = {};
@ -19,14 +35,83 @@ export default class SceneDeals extends React.Component {
render() {
return (
<ScenePage>
<Section onAction={this.props.onAction} onNavigateTo={this.props.onNavigateTo} title="All deals" buttons={[]}>
<System.H1>Deals</System.H1>
<Section title="Storage Deals">
<System.Table
onAction={this.props.onAction}
onNavigateTo={this.props.onNavigateTo}
data={{ columns: SchemaTable.Deals, rows: this.props.viewer.deals }}
selectedRowId={this.state.table_deals}
onChange={this._handleChange}
name="table_deals"
data={{
columns: [
{
key: "address",
name: "Address",
width: "196px",
},
{
key: "rootCid",
name: "Root CID",
width: "196px",
},
{
key: "status",
name: "Status",
type: "STORAGE_DEAL_STATUS",
width: "104px",
},
{
key: "time",
name: "Time",
width: "100%",
},
],
rows: this.props.viewer.storageList.map((each) => {
return {
id: each.rootCid,
address: each.addr,
rootCid: each.rootCid,
status: each.pending ? "2" : "1",
time: each.time,
children: NestedTable(each.dealInfo),
};
}),
}}
selectedRowId={this.state.selectedRowId}
onClick={this._handleClick}
/>
</Section>
<Section title="Retrieval Deals">
<System.Table
data={{
columns: [
{
key: "address",
name: "Address",
width: "248px",
},
{
key: "rootCid",
name: "Root CID",
width: "248px",
},
{
key: "time",
name: "Time",
width: "100%",
},
],
rows: this.props.viewer.retrievalList.map((each) => {
return {
id: each.dealInfo.rootCid,
address: each.addr,
rootCid: each.dealInfo.rootCid,
time: each.time,
children: NestedTable(each.dealInfo),
};
}),
}}
selectedRowId={this.state.selectedRowId}
onClick={this._handleClick}
name={this.props.name}
/>
</Section>
</ScenePage>

View File

@ -54,7 +54,7 @@ export default class SceneFilesFolder extends React.Component {
{
name: "Upload to IPFS",
type: "SIDEBAR",
value: "SIDEBAR_FILE_STORAGE_DEAL",
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
},
]}
>

View File

@ -68,7 +68,7 @@ export default class SceneHome extends React.Component {
{
name: "Upload to IPFS",
type: "SIDEBAR",
value: "SIDEBAR_FILE_STORAGE_DEAL",
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
},
]}
>

View File

@ -83,12 +83,18 @@ export default class SceneStorageMarket extends React.Component {
<System.H1>2,534 FIL</System.H1>
<System.P>
Last Storage Order / GB / Month{" "}
<strong style={{ color: Constants.system.green, fontWeight: 400 }}>+143.24 (6.08)%</strong>
<strong style={{ color: Constants.system.green, fontWeight: 400 }}>
+143.24 (6.08)%
</strong>
</System.P>
<div css={STYLES_CHART_SECTION}>
<div css={STYLES_LEFT}>
<Section onAction={this.props.onAction} onNavigateTo={this.props.onNavigateTo} title="Storage market">
<Section
onAction={this.props.onAction}
onNavigateTo={this.props.onNavigateTo}
title="Storage market"
>
<div css={STYLES_OPTIONS}>
<span css={STYLES_OPTION}>1 Day</span>
<span css={STYLES_OPTION}>1 Week</span>
@ -122,7 +128,11 @@ export default class SceneStorageMarket extends React.Component {
</Section>
</div>
<div css={STYLES_RIGHT}>
<Section onAction={this.props.onAction} onNavigateTo={this.props.onNavigateTo} title="Statistics">
<Section
onAction={this.props.onAction}
onNavigateTo={this.props.onNavigateTo}
title="Statistics"
>
<div css={STYLES_ITEM}>
<div css={STYLES_FOCUS}>8,422 FIL/GB/Month</div>
<div css={STYLES_SUBTEXT}>Market Storage Price</div>
@ -169,14 +179,15 @@ export default class SceneStorageMarket extends React.Component {
{
name: "Make storage deal",
type: "SIDEBAR",
value: "SIDEBAR_FILE_STORAGE_DEAL",
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
},
{
name: "Export",
type: "DOWNLOAD",
value: "CSV_STORAGE_DEALS",
},
]}>
]}
>
<System.Table
data={{
columns: [