diff --git a/common/actions.js b/common/actions.js index 7fb01360..5ce4cf76 100644 --- a/common/actions.js +++ b/common/actions.js @@ -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 }), + }); +}; diff --git a/components/core/DataView.js b/components/core/DataView.js new file mode 100644 index 00000000..6d6071c7 --- /dev/null +++ b/components/core/DataView.js @@ -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: , + }; + }), + }, + }); + }; + + _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: ( + this._handleSelect(index)}> + {cid} + + ), + size: {Strings.bytesToSize(each.size)}, + children: ( +
+ +
Actions
+
+ {this.state.loading || isOnNetwork ? null : ( + this._handleMakeDeal(each)}> + Store on Filecoin + + )} + + this._handleDelete(cid)}> + Delete + +
+
+ {each.error ? ( + +
+ Errors +
+
{each.error}
+
+ ) : null} +
+ ), + }; + }); + + const data = { + columns, + rows, + }; + + return ( +
+ +
+ ); + } +} diff --git a/components/system/components/GlobalCarousel.js b/components/system/components/GlobalCarousel.js index e09c3fd4..edf4529b 100644 --- a/components/system/components/GlobalCarousel.js +++ b/components/system/components/GlobalCarousel.js @@ -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 (
+ {current.cid ? ( + + OPEN {current.cid} + + ) : null} {current.onDelete ? ( current.onDelete(current.id)} style={{ top: 56, right: 16 }}> {this.state.loading ? : "Delete Object"} ) : null} - - - - {current.component}
); diff --git a/components/system/components/Table.js b/components/system/components/Table.js index eb113ccf..dfaaa4ee 100644 --- a/components/system/components/Table.js +++ b/components/system/components/Table.js @@ -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}; diff --git a/components/system/components/fragments/TableComponents.js b/components/system/components/fragments/TableComponents.js index 8b50c180..cccff065 100644 --- a/components/system/components/fragments/TableComponents.js +++ b/components/system/components/fragments/TableComponents.js @@ -70,9 +70,7 @@ const COMPONENTS_TRANSACTION_DIRECTION = { const COMPONENTS_TRANSACTION_STATUS = { "0": Qualified, - "1": ( - Sealed On Filecoin - ), + "1": Sealed On Filecoin, "2": , }; @@ -170,10 +168,7 @@ export const TableColumn = (props) => { ) : null; return ( - + {props.children} @@ -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 ( - {text == 1 ? "Storage" : "Retrieval"} - ); + return {text == 1 ? "Storage" : "Retrieval"}; case "BUTTON": - return ( - onAction({ type: "SIDEBAR", value: action, data })} - > - {text} - - ); + return onAction({ type: "SIDEBAR", value: action, data })}>{text}; case "TRANSACTION_DIRECTION": return COMPONENTS_TRANSACTION_DIRECTION[text]; case "TRANSACTION_STATUS": - return ( - {COMPONENTS_TRANSACTION_STATUS[text]} - ); + return {COMPONENTS_TRANSACTION_STATUS[text]} ; 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 ( {COMPONENTS_TRANSACTION_STATUS[`${text}`]} - {data.error ? ( - Failed Deal - ) : null} + {data.error ? Previously Failed : null} ); case "BANDWIDTH_UPLOAD": @@ -258,15 +232,9 @@ export const TableContent = ({ ); case "MINER_AVAILABILITY": - return text == 1 ? ( - Online - ) : null; + return text == 1 ? Online : null; case "DEAL_AUTO_RENEW": - return text == 1 ? ( - True - ) : ( - False - ); + return text == 1 ? True : False; case "NOTIFICATION_ERROR": return ( @@ -275,18 +243,10 @@ export const TableContent = ({ ); case "NETWORK_TYPE": return text.map((each) => { - return ( - - {each} - - ); + return {each}; }); case "SLATE_PUBLIC_TEXT_TAG": - return !text ? ( - Private - ) : ( - Public - ); + return !text ? Private : Public; case "TEXT_TAG": return {text}; case "FILE_DATE": @@ -306,9 +266,7 @@ export const TableContent = ({ return text; } - return ( - onNavigateTo({ id: data.id }, data)}>{text} - ); + return onNavigateTo({ id: data.id }, data)}>{text}; case "FILE_LINK": if (!data) { return text; diff --git a/pages/api/data/get.js b/pages/api/data/get.js index 197798fe..74b5997a 100644 --- a/pages/api/data/get.js +++ b/pages/api/data/get.js @@ -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); diff --git a/pages/api/data/remove.js b/pages/api/data/remove.js new file mode 100644 index 00000000..5e3ea590 --- /dev/null +++ b/pages/api/data/remove.js @@ -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, + }); +}; diff --git a/scenes/SceneFilesFolder.js b/scenes/SceneFilesFolder.js index c3a307a2..92f60345 100644 --- a/scenes/SceneFilesFolder.js +++ b/scenes/SceneFilesFolder.js @@ -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 ( {this.props.current.name} -
- -
+ ]} + viewer={this.props.viewer} + items={this.props.viewer.library[0].children} + onAction={this.props.onAction} + onRehydrate={this.props.onRehydrate} + />
); } diff --git a/scenes/SceneHome.js b/scenes/SceneHome.js index 9afd5768..bf1d1aee 100644 --- a/scenes/SceneHome.js +++ b/scenes/SceneHome.js @@ -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." /> Home + {this.props.viewer.addresses[0] ? ( -
+
) : null} -
+
{this.props.viewer.library[0] ? ( -
- -
+ onRehydrate={this.props.onRehydrate} + /> ) : null} );