data: removes data from your bucket, library, and slates when you remove data

This commit is contained in:
jimmylee 2020-08-12 01:22:28 -07:00
parent aa271efdcb
commit 658dacfcac
9 changed files with 392 additions and 214 deletions

View File

@ -1,6 +1,5 @@
import "isomorphic-fetch";
import * as State from "~/common/state";
import * as Strings from "~/common/strings";
const REQUEST_HEADERS = {
@ -21,6 +20,13 @@ const returnJSON = async (route, options) => {
return json;
};
export const health = async (data) => {
return await returnJSON(`/api/_`, {
...DEFAULT_OPTIONS,
body: JSON.stringify({ data: { success: true } }),
});
};
export const sendFilecoin = async (data) => {
if (Strings.isEmpty(data.source)) {
return null;
@ -80,13 +86,6 @@ export const checkCIDStatus = async (data) => {
});
};
export const health = async (data) => {
return await returnJSON(`/api/_`, {
...DEFAULT_OPTIONS,
body: JSON.stringify({ data: { success: true } }),
});
};
export const createSlate = async (data) => {
return await returnJSON(`/api/slates/create`, {
...DEFAULT_OPTIONS,
@ -127,3 +126,10 @@ export const deleteAPIKey = async (data) => {
body: JSON.stringify({ data }),
});
};
export const deleteBucketItem = async (data) => {
return await returnJSON(`/api/data/remove`, {
...DEFAULT_OPTIONS,
body: JSON.stringify({ data }),
});
};

198
components/core/DataView.js Normal file
View File

@ -0,0 +1,198 @@
import * as React from "react";
import * as Constants from "~/common/constants";
import * as Strings from "~/common/strings";
import * as System from "~/components/system";
import * as Actions from "~/common/actions";
import { css } from "@emotion/react";
import Section from "~/components/core/Section";
import MediaObject from "~/components/core/MediaObject";
const COLUMNS_SCHEMA = [
{ key: "cid", name: "CID", width: "100%" },
{
key: "size",
name: "Size",
width: "84px",
},
{ key: "type", name: "Type", type: "TEXT_TAG", width: "172px" },
{
key: "networks",
name: "Networks",
type: "NETWORK_TYPE",
},
{
key: "storage",
name: "Storage Deal Status",
width: "148px",
type: "STORAGE_DEAL_STATUS",
},
];
const STYLES_LINK = css`
font-family: ${Constants.font.semiBold};
font-weight: 400;
cursor: pointer;
transition: 200ms ease all;
:hover {
color: ${Constants.system.brand};
}
`;
const STYLES_LABEL = css`
letter-spacing: 0.1px;
font-size: 12px;
text-transform: uppercase;
font-family: ${Constants.font.semiBold};
font-weight: 400;
color: ${Constants.system.black};
`;
const STYLES_SECTION = css`
margin: 12px 0 16px 0;
`;
const delay = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
export default class DataView extends React.Component {
state = {
selectedRowId: null,
};
async componentDidMount() {
await this._handleUpdate();
}
_handleUpdate = async () => {
// NOTE(jim): Hack to handle some race conditions.
await delay(200);
System.dispatchCustomEvent({
name: "slate-global-create-carousel",
detail: {
slides: this.props.items.map((each) => {
const cid = each.ipfs.replace("/ipfs/", "");
return {
id: each.id,
cid,
component: <MediaObject key={each.id} data={each} />,
};
}),
},
});
};
_handleSelect = (index) => {
System.dispatchCustomEvent({
name: "slate-global-open-carousel",
detail: { index },
});
};
_handleMakeDeal = (data) => {
this.props.onAction({ type: "SIDEBAR", value: "SIDEBAR_FILE_STORAGE_DEAL", data });
};
_handleDelete = async (cid) => {
this.setState({ loading: true });
if (!window.confirm("Are you sure you want to delete this? It will be removed from your Slates too.")) {
this.setState({ loading: false });
return null;
}
const response = await Actions.deleteBucketItem({ cid });
console.log(response);
if (!response) {
this.setState({ loading: false });
alert("TODO: Broken response error");
return;
}
if (response.error) {
this.setState({ loading: false });
alert("TODO: Bucket delete error");
return;
}
await this.props.onRehydrate();
await this._handleUpdate();
this.setState({ loading: false });
};
_handleClick = (e) => {
this.setState({ [e.target.name]: e.target.value });
};
render() {
const columns = COLUMNS_SCHEMA;
const rows = this.props.items.map((each, index) => {
const cid = each.ipfs.replace("/ipfs/", "");
const isOnNetwork = each.networks && each.networks.includes("FILECOIN");
return {
...each,
cid: (
<span css={STYLES_LINK} onClick={() => this._handleSelect(index)}>
{cid}
</span>
),
size: <span>{Strings.bytesToSize(each.size)}</span>,
children: (
<div>
<React.Fragment>
<div css={STYLES_LABEL}>Actions</div>
<div css={STYLES_SECTION}>
{this.state.loading || isOnNetwork ? null : (
<System.ButtonPrimary
loading={this.state.loading}
style={{ marginRight: 16 }}
onClick={() => this._handleMakeDeal(each)}>
Store on Filecoin
</System.ButtonPrimary>
)}
<System.ButtonSecondary loading={this.state.loading} onClick={() => this._handleDelete(cid)}>
Delete
</System.ButtonSecondary>
</div>
</React.Fragment>
{each.error ? (
<React.Fragment>
<div css={STYLES_LABEL} style={{ marginTop: 24 }}>
Errors
</div>
<div css={STYLES_SECTION}>{each.error}</div>
</React.Fragment>
) : null}
</div>
),
};
});
const data = {
columns,
rows,
};
return (
<Section
onAction={this.props.onAction}
title={`${Strings.bytesToSize(this.props.viewer.stats.bytes)} uploaded`}
style={{ minWidth: "880px" }}
buttons={this.props.buttons}>
<System.Table
data={data}
selectedRowId={this.state.selectedRowId}
name="selectedRowId"
onAction={this.props.onAction}
onNavigateTo={this.props.onNavigateTo}
onChange={this._handleChange}
onClick={this._handleClick}
/>
</Section>
);
}
}

View File

@ -11,6 +11,7 @@ const STYLES_BACKGROUND = css`
right: 0;
bottom: 0;
top: 0;
padding: 88px 24px 88px 24px;
width: 100%;
height: 100%;
display: flex;
@ -60,6 +61,37 @@ const STYLES_BUTTON = css`
color: ${Constants.system.white};
cursor: pointer;
margin: auto;
text-decoration: none;
:hover {
background-color: ${Constants.system.black};
}
`;
const STYLES_LINK = css`
font-family: ${Constants.font.code};
font-size: 10px;
user-select: none;
height: 32px;
top: 8px;
left: 16px;
padding: 0 16px 0 16px;
border-radius: 32px;
position: absolute;
display: inline-flex;
align-items: center;
justify-content: center;
z-index: ${Constants.zindex.modal};
background: ${Constants.system.pitchBlack};
transition: 200ms ease all;
color: ${Constants.system.white};
cursor: pointer;
margin: auto;
text-decoration: none;
:visited {
color: ${Constants.system.white};
}
:hover {
background-color: ${Constants.system.black};
@ -110,13 +142,17 @@ export class GlobalCarousel extends React.Component {
_handleSetLoading = (e) => this.setState({ loading: e.detail.loading });
_handleOpen = (e) => this.setState({ visible: true, index: e.detail.index || 0, loading: false });
_handleOpen = (e) => {
this.setState({ visible: true, index: e.detail.index || 0, loading: false });
};
_handleClose = () => this.setState({ visible: false, index: 0, loading: false });
_handleCreate = (e) => {
this.setState({
slides: e.detail.slides,
visible: false,
index: 0,
});
};
@ -147,24 +183,25 @@ export class GlobalCarousel extends React.Component {
return (
<div css={STYLES_BACKGROUND} style={this.props.style}>
{current.cid ? (
<a css={STYLES_LINK} href={`https://hub.textile.io/ipfs/${current.cid}`} target="_blank">
OPEN {current.cid}
</a>
) : null}
{current.onDelete ? (
<span css={STYLES_BUTTON} onClick={() => current.onDelete(current.id)} style={{ top: 56, right: 16 }}>
{this.state.loading ? <LoaderSpinner style={{ height: 16, width: 16 }} /> : "Delete Object"}
</span>
) : null}
<span css={STYLES_BOX} onClick={this._handleClose} style={{ top: 8, right: 16 }}>
<SVG.Dismiss height="20px" />
</span>
<span css={STYLES_BOX} onClick={this._handlePrevious} style={{ top: 0, left: 16, bottom: 0 }}>
<SVG.ChevronLeft height="20px" />
</span>
<span css={STYLES_BOX} onClick={this._handleNext} style={{ top: 0, right: 16, bottom: 0 }}>
<SVG.ChevronRight height="20px" />
</span>
{current.component}
</div>
);

View File

@ -66,6 +66,7 @@ const STYLES_TABLE_ROW = css`
`;
const STYLES_TABLE_SELECTED_ROW = css`
background-color: ${Constants.system.foreground};
box-sizing: border-box;
display: block;
border-bottom: 1px solid ${Constants.system.gray};

View File

@ -70,9 +70,7 @@ const COMPONENTS_TRANSACTION_DIRECTION = {
const COMPONENTS_TRANSACTION_STATUS = {
"0": <Tag>Qualified</Tag>,
"1": (
<Tag style={{ background: Constants.system.green }}>Sealed On Filecoin</Tag>
),
"1": <Tag style={{ background: Constants.system.green }}>Sealed On Filecoin</Tag>,
"2": <LoaderSpinner style={{ width: 20, height: 20 }} />,
};
@ -170,10 +168,7 @@ export const TableColumn = (props) => {
) : null;
return (
<span
css={props.top ? STYLES_TOP_COLUMN : STYLES_COLUMN}
style={props.style}
>
<span css={props.top ? STYLES_TOP_COLUMN : STYLES_COLUMN} style={props.style}>
<span css={STYLES_CONTENT} style={props.contentStyle}>
{props.children}
</span>
@ -186,14 +181,7 @@ export const TableColumn = (props) => {
// TODO(jim): We probably won't use this Table component for long.
// Once we have components for all the necessary flows. We will probably
// make bespoke components for each experience.
export const TableContent = ({
type,
text,
action,
data = {},
onNavigateTo,
onAction,
}) => {
export const TableContent = ({ type, text, action, data = {}, onNavigateTo, onAction }) => {
const { status, online } = data;
if (text === null || text === undefined) {
@ -202,23 +190,13 @@ export const TableContent = ({
switch (type) {
case "DEAL_CATEGORY":
return (
<React.Fragment>{text == 1 ? "Storage" : "Retrieval"}</React.Fragment>
);
return <React.Fragment>{text == 1 ? "Storage" : "Retrieval"}</React.Fragment>;
case "BUTTON":
return (
<Link
onClick={() => onAction({ type: "SIDEBAR", value: action, data })}
>
{text}
</Link>
);
return <Link onClick={() => onAction({ type: "SIDEBAR", value: action, data })}>{text}</Link>;
case "TRANSACTION_DIRECTION":
return COMPONENTS_TRANSACTION_DIRECTION[text];
case "TRANSACTION_STATUS":
return (
<React.Fragment>{COMPONENTS_TRANSACTION_STATUS[text]} </React.Fragment>
);
return <React.Fragment>{COMPONENTS_TRANSACTION_STATUS[text]} </React.Fragment>;
case "OBJECT_TYPE":
return COMPONENTS_OBJECT_TYPE(text);
case "ICON":
@ -231,16 +209,12 @@ export const TableContent = ({
case "DEAL_STATUS_RETRIEVAL":
return RETRIEVAL_DEAL_STATES[`${text}`];
case "DEAL_STATUS":
return data["deal_category"] === 1
? STORAGE_DEAL_STATES[`${text}`]
: RETRIEVAL_DEAL_STATES[`${text}`];
return data["deal_category"] === 1 ? STORAGE_DEAL_STATES[`${text}`] : RETRIEVAL_DEAL_STATES[`${text}`];
case "STORAGE_DEAL_STATUS":
return (
<React.Fragment>
{COMPONENTS_TRANSACTION_STATUS[`${text}`]}
{data.error ? (
<Tag style={{ background: Constants.system.red }}>Failed Deal</Tag>
) : null}
{data.error ? <Tag style={{ background: Constants.system.red }}>Previously Failed</Tag> : null}
</React.Fragment>
);
case "BANDWIDTH_UPLOAD":
@ -258,15 +232,9 @@ export const TableContent = ({
</React.Fragment>
);
case "MINER_AVAILABILITY":
return text == 1 ? (
<Tag style={{ background: Constants.system.green }}>Online</Tag>
) : null;
return text == 1 ? <Tag style={{ background: Constants.system.green }}>Online</Tag> : null;
case "DEAL_AUTO_RENEW":
return text == 1 ? (
<Tag style={{ background: Constants.system.brand }}>True</Tag>
) : (
<Tag>False</Tag>
);
return text == 1 ? <Tag style={{ background: Constants.system.brand }}>True</Tag> : <Tag>False</Tag>;
case "NOTIFICATION_ERROR":
return (
<Tag style={{ background: Constants.system.red }}>
@ -275,18 +243,10 @@ export const TableContent = ({
);
case "NETWORK_TYPE":
return text.map((each) => {
return (
<Tag key={each} style={{ background: Constants.system.brand }}>
{each}
</Tag>
);
return <Tag key={each}>{each}</Tag>;
});
case "SLATE_PUBLIC_TEXT_TAG":
return !text ? (
<Tag>Private</Tag>
) : (
<Tag style={{ background: Constants.system.green }}>Public</Tag>
);
return !text ? <Tag>Private</Tag> : <Tag style={{ background: Constants.system.green }}>Public</Tag>;
case "TEXT_TAG":
return <Tag>{text}</Tag>;
case "FILE_DATE":
@ -306,9 +266,7 @@ export const TableContent = ({
return text;
}
return (
<Link onClick={() => onNavigateTo({ id: data.id }, data)}>{text}</Link>
);
return <Link onClick={() => onNavigateTo({ id: data.id }, data)}>{text}</Link>;
case "FILE_LINK":
if (!data) {
return text;

View File

@ -17,20 +17,14 @@ export default async (req, res) => {
const id = Utilities.getIdFromCookie(req);
if (!id) {
return res
.status(500)
.json({ decorator: "SERVER_DELETE_SLATE", error: true });
return res.status(500).json({ decorator: "SERVER_GET_BUCKET_DATA", error: true });
}
const user = await Data.getUserById({
id,
});
const {
buckets,
bucketKey,
bucketName,
} = await Utilities.getBucketAPIFromUserToken(user.data.tokens.api);
const { buckets, bucketKey, bucketName } = await Utilities.getBucketAPIFromUserToken(user.data.tokens.api);
const r = await buckets.list();
const items = await buckets.listIpfsPath(r[0].path);

91
pages/api/data/remove.js Normal file
View File

@ -0,0 +1,91 @@
import * as MW from "~/node_common/middleware";
import * as Data from "~/node_common/data";
import * as Utilities from "~/node_common/utilities";
import * as Strings from "~/common/strings";
const initCORS = MW.init(MW.CORS);
const initAuth = MW.init(MW.RequireCookieAuthentication);
export default async (req, res) => {
initCORS(req, res);
initAuth(req, res);
if (Strings.isEmpty(req.body.data.cid)) {
return res.status(500).json({ decorator: "SERVER_REMOVE_DATA_NO_CID", error: true });
}
const id = Utilities.getIdFromCookie(req);
if (!id) {
return res.status(403).json({ decorator: "SERVER_REMOVE_DATA_NOT_ALLOWED", error: true });
}
const user = await Data.getUserById({
id,
});
const { buckets, bucketKey, bucketName } = await Utilities.getBucketAPIFromUserToken(user.data.tokens.api);
const r = await buckets.list();
const items = await buckets.listIpfsPath(r[0].path);
let entity;
for (let i = 0; i < items.itemsList.length; i++) {
if (items.itemsList[i].cid === req.body.data.cid) {
entity = items.itemsList[i];
break;
}
}
if (!entity) {
return res.status(500).json({ decorator: "SERVER_REMOVE_DATA_NO_CID", error: true });
}
// remove from your bucket
let bucketRemoval;
try {
// NOTE(jim):
// We use name instead of path because the second argument is for
// a subpath, not the full path.
bucketRemoval = await buckets.removePath(bucketKey, entity.name);
} catch (e) {
console.log(e);
return res.status(500).json({ decorator: "SERVER_REMOVE_DATA_NO_LINK", error: true });
}
// NOTE(jim):
// Goes through all of your slates and removes all data references.
const slates = await Data.getSlatesByUserId({ userId: id });
for (let i = 0; i < slates.length; i++) {
const slate = slates[i];
await Data.updateSlateById({
id: slate.id,
updated_at: new Date(),
data: {
...slate.data,
objects: slate.data.objects.filter((o) => !o.url.includes(req.body.data.cid)),
},
});
}
// NOTE(jim):
// Removes the file reference from your library
const response = await Data.updateUserById({
id: user.id,
data: {
...user.data,
library: [
{
...user.data.library[0],
children: user.data.library[0].children.filter((o) => !o.ipfs.includes(req.body.data.cid)),
},
],
},
});
return res.status(200).send({
decorator: "SERVER_REMOVE_DATA",
success: true,
bucketItems: items.itemsList,
});
};

View File

@ -1,12 +1,11 @@
import * as React from "react";
import * as Actions from "~/common/actions";
import * as System from "~/components/system";
import * as Strings from "~/common/strings";
import { css } from "@emotion/react";
import Section from "~/components/core/Section";
import ScenePage from "~/components/core/ScenePage";
import DataView from "~/components/core/DataView";
import DataMeter from "~/components/core/DataMeter";
const POLLING_INTERVAL = 10000;
@ -62,50 +61,6 @@ export default class SceneFilesFolder extends React.Component {
}
render() {
let rows = this.props.viewer.library[0].children.map((each) => {
return {
...each,
button: each.networks && each.networks.includes("FILECOIN") ? null : "Store on Filecoin",
};
});
const data = {
columns: [
{ key: "name", name: "Data", type: "FILE_LINK", width: "328px" },
{
key: "size",
name: "Size",
width: "84px",
type: "FILE_SIZE",
},
{ key: "type", name: "Type", type: "TEXT_TAG", width: "172px" },
{
key: "networks",
name: "Networks",
type: "NETWORK_TYPE",
},
{
key: "storage",
name: "Storage Deal Status",
width: "148px",
type: "STORAGE_DEAL_STATUS",
},
{
key: "button",
hideLabel: true,
type: "BUTTON",
action: "SIDEBAR_FILE_STORAGE_DEAL",
width: "132px",
},
{
key: "error",
hideLabel: true,
width: "188px",
},
],
rows,
};
return (
<ScenePage>
<System.DescriptionGroup
@ -116,25 +71,20 @@ export default class SceneFilesFolder extends React.Component {
<DataMeter stats={this.props.viewer.stats} style={{ margin: "48px 0 24px 0" }} />
<System.H1 style={{ marginTop: 48 }}>{this.props.current.name}</System.H1>
<Section
onAction={this.props.onAction}
title={`${Strings.bytesToSize(this.props.viewer.stats.bytes)} uploaded`}
style={{ minWidth: "1200px" }}
<DataView
buttons={[
{
name: "Upload data",
type: "SIDEBAR",
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
},
]}>
<System.Table
key={this.props.current.folderId}
data={data}
onAction={this.props.onAction}
onNavigateTo={this.props.onNavigateTo}
onChange={this._handleChange}
/>
</Section>
]}
viewer={this.props.viewer}
items={this.props.viewer.library[0].children}
onAction={this.props.onAction}
onRehydrate={this.props.onRehydrate}
/>
</ScenePage>
);
}

View File

@ -6,17 +6,9 @@ import { css } from "@emotion/react";
import Section from "~/components/core/Section";
import ScenePage from "~/components/core/ScenePage";
import DataView from "~/components/core/DataView";
export default class SceneHome extends React.Component {
state = {
data: null,
transaction: null,
};
_handleChange = (e) => {
this.setState({ [e.target.name]: e.target.value });
};
render() {
// TODO(jim): Refactor later.
const slates = {
@ -46,55 +38,7 @@ export default class SceneHome extends React.Component {
};
// TODO(jim): Refactor later.
const slateButtons = [
{ name: "Create slate", type: "SIDEBAR", value: "SIDEBAR_CREATE_SLATE" },
];
// TODO(jim): Refactor later.
const data = {
columns: [
{ key: "name", name: "Data", type: "FILE_LINK", width: "328px" },
{
key: "size",
name: "Size",
width: "140px",
type: "FILE_SIZE",
},
{ key: "type", name: "Type", type: "TEXT_TAG", width: "172px" },
{
key: "date",
name: "Date uploaded",
width: "160px",
type: "FILE_DATE",
},
{
key: "networks",
name: "Network",
type: "NETWORK_TYPE",
width: "100%",
},
],
rows: this.props.viewer.library[0].children.map((each) => {
return {
...each,
button: "Store on Filecoin",
};
}),
};
// TODO(jim): Refactor later.
const dataButtons = [
{
name: "View files",
type: "NAVIGATE",
value: this.props.viewer.library[0].id,
},
{
name: "Upload data",
type: "SIDEBAR",
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
},
];
const slateButtons = [{ name: "Create slate", type: "SIDEBAR", value: "SIDEBAR_CREATE_SLATE" }];
// TODO(jim): Refactor later.
const wallet = {
@ -122,12 +66,9 @@ export default class SceneHome extends React.Component {
description="No! Consider this page just a functionality test. Home will have Filecoin network analytics and updates from the people you engage with."
/>
<System.H1 style={{ marginTop: 48 }}>Home</System.H1>
{this.props.viewer.addresses[0] ? (
<Section
title="Wallet addresses"
buttons={walletButtons}
onAction={this.props.onAction}
>
<Section title="Wallet addresses" buttons={walletButtons} onAction={this.props.onAction}>
<System.Table
data={wallet}
name="transaction"
@ -137,11 +78,7 @@ export default class SceneHome extends React.Component {
</Section>
) : null}
<Section
title="Slates"
buttons={slateButtons}
onAction={this.props.onAction}
>
<Section title="Slates" buttons={slateButtons} onAction={this.props.onAction}>
<System.Table
data={slates}
name="slate"
@ -151,18 +88,24 @@ export default class SceneHome extends React.Component {
</Section>
{this.props.viewer.library[0] ? (
<Section
title="Recent data"
buttons={dataButtons}
<DataView
buttons={[
{
name: "View files",
type: "NAVIGATE",
value: this.props.viewer.library[0].id,
},
{
name: "Upload data",
type: "SIDEBAR",
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
},
]}
viewer={this.props.viewer}
items={this.props.viewer.library[0].children}
onAction={this.props.onAction}
>
<System.Table
data={data}
name="data"
onAction={this.props.onAction}
onNavigateTo={this.props.onNavigateTo}
/>
</Section>
onRehydrate={this.props.onRehydrate}
/>
) : null}
</ScenePage>
);