diff --git a/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js b/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js
index c5cdee36..f8a5cf9e 100644
--- a/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js
+++ b/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js
@@ -2,7 +2,8 @@ import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { useUser } from "../../contexts/user";
-import useSubscriptionPlans from "../../hooks/useSubscriptionPlans";
+import useActivePlan from "../../hooks/useActivePlan";
+import { ContainerLoadingIndicator } from "../LoadingIndicator";
import LatestPayment from "./LatestPayment";
import SuggestedPlan from "./SuggestedPlan";
@@ -11,13 +12,10 @@ dayjs.extend(relativeTime);
const CurrentPlan = () => {
const { user, error: userError } = useUser();
- const { activePlan, plans, error: plansError } = useSubscriptionPlans(user);
+ const { plans, activePlan, error: plansError } = useActivePlan(user);
if (!user || !activePlan) {
- return (
- // TODO: a nicer loading indicator
-
Loading...
- );
+ return ;
}
if (userError || plansError) {
diff --git a/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js b/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js
index b467e1ea..44be79ed 100644
--- a/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js
+++ b/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js
@@ -1,24 +1,50 @@
-import * as React from "react";
+import { useEffect, useMemo, useState } from "react";
import fileSize from "pretty-bytes";
import { Link } from "gatsby";
+import useSWR from "swr";
+
+import { useUser } from "../../contexts/user";
+import useActivePlan from "../../hooks/useActivePlan";
+import { ContainerLoadingIndicator } from "../LoadingIndicator";
import { GraphBar } from "./GraphBar";
import { UsageGraph } from "./UsageGraph";
-// TODO: get real data
-const useUsageData = () => ({
- files: {
- used: 19_521,
- limit: 20_000,
- },
- storage: {
- used: 23_000_000_000,
- limit: 1_000_000_000_000,
- },
-});
+const useUsageData = () => {
+ const { user } = useUser();
+ const { activePlan, error } = useActivePlan(user);
+ const { data: stats, error: statsError } = useSWR("user/stats");
+
+ const [loading, setLoading] = useState(true);
+ const [usage, setUsage] = useState({});
+
+ const hasError = error || statsError;
+ const hasData = activePlan && stats;
+
+ useEffect(() => {
+ if (hasData || hasError) {
+ setLoading(false);
+ }
+
+ if (hasData && !hasError) {
+ setUsage({
+ filesUsed: stats?.numUploads,
+ filesLimit: activePlan?.limits?.maxNumberUploads,
+ storageUsed: stats?.totalUploadsSize,
+ storageLimit: activePlan?.limits?.storageLimit,
+ });
+ }
+ }, [hasData, hasError, stats, activePlan]);
+
+ return {
+ error: error || statsError,
+ loading,
+ usage,
+ };
+};
const size = (bytes) => {
- const text = fileSize(bytes, { maximumFractionDigits: 1 });
+ const text = fileSize(bytes ?? 0, { maximumFractionDigits: 0 });
const [value, unit] = text.split(" ");
return {
@@ -28,12 +54,26 @@ const size = (bytes) => {
};
};
-export default function CurrentUsage() {
- const { files, storage } = useUsageData();
+const ErrorMessage = () => (
+
+
We were not able to fetch the current usage data.
+
We'll try again automatically.
+
+);
- const storageUsage = size(storage.used);
- const storageLimit = size(storage.limit);
- const filesUsedLabel = React.useMemo(() => ({ value: files.used, unit: "files" }), [files.used]);
+export default function CurrentUsage() {
+ const { usage, error, loading } = useUsageData();
+ const storageUsage = size(usage.storageUsed);
+ const storageLimit = size(usage.storageLimit);
+ const filesUsedLabel = useMemo(() => ({ value: usage.filesUsed, unit: "files" }), [usage.filesUsed]);
+
+ if (loading) {
+ return ;
+ }
+
+ if (error) {
+ return ;
+ }
return (
<>
@@ -41,7 +81,7 @@ export default function CurrentUsage() {
{storageUsage.text} of {storageLimit.text}
- {files.used} of {files.limit} files
+ {usage.filesUsed} of {usage.filesLimit} files
@@ -49,8 +89,8 @@ export default function CurrentUsage() {
{storageLimit.text}
-
-
+
+
Files
@@ -62,7 +102,7 @@ export default function CurrentUsage() {
UPGRADE
{" "}
{/* TODO: proper URL */}
- {files.limit}
+ {usage.filesLimit}
diff --git a/packages/dashboard-v2/src/components/LatestActivity/ActivityTable.js b/packages/dashboard-v2/src/components/LatestActivity/ActivityTable.js
index 647f9bf8..345a2daa 100644
--- a/packages/dashboard-v2/src/components/LatestActivity/ActivityTable.js
+++ b/packages/dashboard-v2/src/components/LatestActivity/ActivityTable.js
@@ -2,6 +2,7 @@ import * as React from "react";
import useSWR from "swr";
import { Table, TableBody, TableCell, TableRow } from "../Table";
+import { ContainerLoadingIndicator } from "../LoadingIndicator";
import useFormattedActivityData from "./useFormattedActivityData";
@@ -12,10 +13,10 @@ export default function ActivityTable({ type }) {
if (!items.length) {
return (
- {/* TODO: proper loading indicator / error message */}
- {!data && !error &&
Loading...
}
+ {/* TODO: proper error message */}
+ {!data && !error &&
}
{!data && error &&
An error occurred while loading this data.
}
- {data &&
No files found.
}
+ {data && !error &&
No files found.
}
);
}
diff --git a/packages/dashboard-v2/src/components/LatestActivity/LatestActivity.js b/packages/dashboard-v2/src/components/LatestActivity/LatestActivity.js
index 9c53554a..87825661 100644
--- a/packages/dashboard-v2/src/components/LatestActivity/LatestActivity.js
+++ b/packages/dashboard-v2/src/components/LatestActivity/LatestActivity.js
@@ -23,7 +23,7 @@ export default function LatestActivity() {
-
+
diff --git a/packages/dashboard-v2/src/components/LoadingIndicator/ContainerLoadingIndicator.js b/packages/dashboard-v2/src/components/LoadingIndicator/ContainerLoadingIndicator.js
new file mode 100644
index 00000000..de86a849
--- /dev/null
+++ b/packages/dashboard-v2/src/components/LoadingIndicator/ContainerLoadingIndicator.js
@@ -0,0 +1,18 @@
+import styled from "styled-components";
+import { CircledProgressIcon } from "../Icons";
+
+/**
+ * This loading indicator is designed to be replace entire blocks (i.e. components)
+ * while they are fetching required data.
+ *
+ * It will take 50% of the parent's height, but won't get bigger than 150x150 px.
+ */
+const Wrapper = styled.div.attrs({
+ className: "flex w-full h-full justify-center items-center p-8 text-palette-100",
+})``;
+
+export const ContainerLoadingIndicator = (props) => (
+
+
+
+);
diff --git a/packages/dashboard-v2/src/components/LoadingIndicator/index.js b/packages/dashboard-v2/src/components/LoadingIndicator/index.js
new file mode 100644
index 00000000..df7c2a88
--- /dev/null
+++ b/packages/dashboard-v2/src/components/LoadingIndicator/index.js
@@ -0,0 +1 @@
+export * from "./ContainerLoadingIndicator";
diff --git a/packages/dashboard-v2/src/contexts/plans/PlansContext.js b/packages/dashboard-v2/src/contexts/plans/PlansContext.js
new file mode 100644
index 00000000..ff35b45e
--- /dev/null
+++ b/packages/dashboard-v2/src/contexts/plans/PlansContext.js
@@ -0,0 +1,7 @@
+import { createContext } from "react";
+
+export const PlansContext = createContext({
+ plans: [],
+ limits: [],
+ error: null,
+});
diff --git a/packages/dashboard-v2/src/contexts/plans/PlansProvider.js b/packages/dashboard-v2/src/contexts/plans/PlansProvider.js
new file mode 100644
index 00000000..c481e296
--- /dev/null
+++ b/packages/dashboard-v2/src/contexts/plans/PlansProvider.js
@@ -0,0 +1,47 @@
+import { useEffect, useState } from "react";
+import useSWR from "swr";
+
+import freePlan from "../../lib/tiers";
+
+import { PlansContext } from "./PlansContext";
+
+/**
+ * NOTE: this function heavily relies on the fact that each Plan's `tier`
+ * property corresponds to the plan's index in UserLimits array in
+ * skynet-accounts code.
+ *
+ * @see https://github.com/SkynetLabs/skynet-accounts/blob/7337e740b71b77e6d08016db801e293b8ad81abc/database/user.go#L53-L101
+ */
+const aggregatePlansAndLimits = (plans, limits) => {
+ const sortedPlans = [freePlan, ...plans].sort((planA, planB) => planA.tier - planB.tier);
+
+ // Decorate each plan with its corresponding limits data, if available.
+ if (limits?.length) {
+ return sortedPlans.map((plan) => ({ ...plan, limits: limits[plan.tier] || null }));
+ }
+
+ // If we don't have the limits data yet, set just return the plans.
+
+ return sortedPlans;
+};
+
+export const PlansProvider = ({ children }) => {
+ const { data: rawPlans, error: plansError } = useSWR("stripe/prices");
+ const { data: limits, error: limitsError } = useSWR("limits");
+
+ const [plans, setPlans] = useState([freePlan]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (plansError || limitsError) {
+ setLoading(false);
+ setError(plansError || limitsError);
+ } else if (rawPlans) {
+ setLoading(false);
+ setPlans(aggregatePlansAndLimits(rawPlans, limits?.userLimits));
+ }
+ }, [rawPlans, limits, plansError, limitsError]);
+
+ return {children};
+};
diff --git a/packages/dashboard-v2/src/contexts/plans/index.js b/packages/dashboard-v2/src/contexts/plans/index.js
new file mode 100644
index 00000000..84dd790f
--- /dev/null
+++ b/packages/dashboard-v2/src/contexts/plans/index.js
@@ -0,0 +1,2 @@
+export * from "./PlansProvider";
+export * from "./usePlans";
diff --git a/packages/dashboard-v2/src/contexts/plans/usePlans.js b/packages/dashboard-v2/src/contexts/plans/usePlans.js
new file mode 100644
index 00000000..f36e8595
--- /dev/null
+++ b/packages/dashboard-v2/src/contexts/plans/usePlans.js
@@ -0,0 +1,5 @@
+import { useContext } from "react";
+
+import { PlansContext } from "./PlansContext";
+
+export const usePlans = () => useContext(PlansContext);
diff --git a/packages/dashboard-v2/src/hooks/useActivePlan.js b/packages/dashboard-v2/src/hooks/useActivePlan.js
new file mode 100644
index 00000000..53e28b63
--- /dev/null
+++ b/packages/dashboard-v2/src/hooks/useActivePlan.js
@@ -0,0 +1,22 @@
+import { useEffect, useState } from "react";
+
+import freeTier from "../lib/tiers";
+import { usePlans } from "../contexts/plans";
+
+export default function useActivePlan(user) {
+ const { plans, error } = usePlans();
+
+ const [activePlan, setActivePlan] = useState(freeTier);
+
+ useEffect(() => {
+ if (user) {
+ setActivePlan(plans.find((plan) => plan.tier === user.tier));
+ }
+ }, [plans, user]);
+
+ return {
+ error,
+ plans,
+ activePlan,
+ };
+}
diff --git a/packages/dashboard-v2/src/hooks/useSubscriptionPlans.js b/packages/dashboard-v2/src/hooks/useSubscriptionPlans.js
deleted file mode 100644
index 26658df8..00000000
--- a/packages/dashboard-v2/src/hooks/useSubscriptionPlans.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { useEffect, useState } from "react";
-import useSWR from "swr";
-import freeTier from "../lib/tiers";
-
-export default function useSubscriptionPlans(user) {
- const { data: paidPlans, error, mutate } = useSWR("stripe/prices");
- const [plans, setPlans] = useState([freeTier]);
- const [activePlan, setActivePlan] = useState(freeTier);
-
- useEffect(() => {
- if (paidPlans) {
- setPlans((plans) => [...plans, ...paidPlans].sort((planA, planB) => planA.tier - planB.tier));
- }
- }, [paidPlans]);
-
- useEffect(() => {
- if (user) {
- setActivePlan(plans.find((plan) => plan.tier === user.tier));
- }
- }, [plans, user]);
-
- return {
- error,
- mutate,
- plans,
- activePlan,
- };
-}
diff --git a/packages/dashboard-v2/src/layouts/DashboardLayout.js b/packages/dashboard-v2/src/layouts/DashboardLayout.js
index 07f4eabf..b369ece3 100644
--- a/packages/dashboard-v2/src/layouts/DashboardLayout.js
+++ b/packages/dashboard-v2/src/layouts/DashboardLayout.js
@@ -8,6 +8,7 @@ import { PageContainer } from "../components/PageContainer";
import { NavBar } from "../components/Navbar";
import { Footer } from "../components/Footer";
import { UserProvider, useUser } from "../contexts/user";
+import { ContainerLoadingIndicator } from "../components/LoadingIndicator";
const Wrapper = styled.div.attrs({
className: "min-h-screen overflow-hidden",
@@ -25,7 +26,7 @@ const Layout = ({ children }) => {
{!user && (
-
Loading...
{/* TODO: Do something nicer here */}
+
)}
{user && <>{children}>}
diff --git a/packages/dashboard-v2/src/pages/index.js b/packages/dashboard-v2/src/pages/index.js
index 695e6ac3..4db97e04 100644
--- a/packages/dashboard-v2/src/pages/index.js
+++ b/packages/dashboard-v2/src/pages/index.js
@@ -2,6 +2,7 @@ import * as React from "react";
import { useMedia } from "react-use";
import theme from "../lib/theme";
+import { PlansProvider } from "../contexts/plans/PlansProvider";
import { ArrowRightIcon } from "../components/Icons";
import { Panel } from "../components/Panel";
import { Tab, TabPanel, Tabs } from "../components/Tabs";
@@ -16,7 +17,7 @@ const IndexPage = () => {
const showRecentActivity = useMedia(`(min-width: ${theme.screens.md})`);
return (
- <>
+
{
)}
- >
+
);
};