Merge pull request #681 from SkynetLabs/unpin-from-dashboard
unpin skylink from dashboard
This commit is contained in:
commit
f42a3a9c20
|
@ -0,0 +1 @@
|
|||
- added unpinning skylinks from account dashboard
|
|
@ -97,7 +97,7 @@
|
|||
preserve_host: true
|
||||
url: "http://accounts:3000"
|
||||
match:
|
||||
url: "http://oathkeeper<{,:4455}>/<{login,logout,user,user/uploads,user/downloads,user/stats}>"
|
||||
url: "http://oathkeeper<{,:4455}>/<{login,logout,user,user/uploads,user/uploads/*,user/downloads,user/stats}>"
|
||||
methods:
|
||||
- GET
|
||||
- POST
|
||||
|
|
|
@ -10,6 +10,6 @@ RUN yarn --frozen-lockfile
|
|||
COPY public ./public
|
||||
COPY src ./src
|
||||
COPY styles ./styles
|
||||
COPY next.config.js postcss.config.js tailwind.config.js ./
|
||||
COPY postcss.config.js tailwind.config.js ./
|
||||
|
||||
CMD ["sh", "-c", "env | grep -E 'NEXT_PUBLIC|KRATOS|STRIPE' > .env.local && yarn build && yarn start"]
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = {
|
||||
webpack5: true,
|
||||
};
|
|
@ -23,11 +23,13 @@
|
|||
"http-status-codes": "2.1.4",
|
||||
"ky": "0.25.1",
|
||||
"next": "11.1.2",
|
||||
"normalize.css": "8.0.1",
|
||||
"postcss": "8.3.8",
|
||||
"prettier": "2.4.1",
|
||||
"pretty-bytes": "5.6.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-toastify": "8.0.2",
|
||||
"skynet-js": "3.0.2",
|
||||
"stripe": "8.176.0",
|
||||
"superagent": "6.1.0",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect } from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
function Button({ children, disabled, className, ...props }) {
|
||||
function Button({ children, disabled, ...props }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
|
@ -10,8 +10,7 @@ function Button({ children, disabled, className, ...props }) {
|
|||
{
|
||||
"hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500": !disabled,
|
||||
"cursor-auto opacity-50": disabled,
|
||||
},
|
||||
className
|
||||
}
|
||||
)}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
|
@ -21,7 +20,26 @@ function Button({ children, disabled, className, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default function Table({ items, count, headers, actions, offset, setOffset, pageSize = 10 }) {
|
||||
function ButtonAction({ children, disabled, ...props }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={classnames(
|
||||
"inline-flex items-center px-2.5 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white",
|
||||
{
|
||||
"hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500": !disabled,
|
||||
"cursor-auto opacity-50": disabled,
|
||||
}
|
||||
)}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Table({ items, count, headers, mutate, actions, offset, setOffset, pageSize = 10 }) {
|
||||
useEffect(() => {
|
||||
if (offset < 0) setOffset(0);
|
||||
else if (offset >= count && count > 0) setOffset(Math.floor(count / pageSize - 1) * pageSize);
|
||||
|
@ -45,8 +63,8 @@ export default function Table({ items, count, headers, actions, offset, setOffse
|
|||
{name}
|
||||
</th>
|
||||
))}
|
||||
{actions.map(({ key, name }) => (
|
||||
<th key={key} scope="col" className="relative px-6 py-3">
|
||||
{actions.map(({ name }, index) => (
|
||||
<th key={index} scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">{name}</span>
|
||||
</th>
|
||||
))}
|
||||
|
@ -56,7 +74,7 @@ export default function Table({ items, count, headers, actions, offset, setOffse
|
|||
{items && items.length ? (
|
||||
items.map((row, index) => (
|
||||
<tr className={index % 2 ? "bg-gray-100" : "bg-white"} key={index}>
|
||||
{headers.map(({ key, formatter, href, nowrap = true }) => (
|
||||
{headers.map(({ key, formatter, href, nowrap }) => (
|
||||
<td
|
||||
key={key}
|
||||
className={`${nowrap ? "whitespace-nowrap" : ""} px-6 py-4 text-sm font-medium text-gray-900`}
|
||||
|
@ -77,11 +95,9 @@ export default function Table({ items, count, headers, actions, offset, setOffse
|
|||
)) || <>—</>}
|
||||
</td>
|
||||
))}
|
||||
{actions.map(({ key, name, action }) => (
|
||||
<td key={key} className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<a href="#" className="text-green-600 hover:text-green-900" onClick={action}>
|
||||
{name}
|
||||
</a>
|
||||
{actions.map(({ name, action }, index) => (
|
||||
<td key={index} className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<ButtonAction onClick={() => action(row, mutate)}>{name}</ButtonAction>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
@ -107,15 +123,11 @@ export default function Table({ items, count, headers, actions, offset, setOffse
|
|||
<span className="font-medium">{count}</span> results
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 flex justify-between sm:justify-end">
|
||||
<div className="flex-1 flex justify-between sm:justify-end space-x-3">
|
||||
<Button disabled={offset - pageSize < 0} onClick={() => setOffset(offset - pageSize)}>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-3"
|
||||
disabled={offset + pageSize >= count}
|
||||
onClick={() => setOffset(offset + pageSize)}
|
||||
>
|
||||
<Button disabled={offset + pageSize >= count} onClick={() => setOffset(offset + pageSize)}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { Elements } from "@stripe/react-stripe-js";
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import Head from "next/head";
|
||||
import "normalize.css";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import "tailwindcss/tailwind.css";
|
||||
import "@fontsource/metropolis/all.css";
|
||||
|
||||
|
@ -14,6 +17,7 @@ function MyApp({ Component, pageProps }) {
|
|||
<title key="title">Skynet</title>
|
||||
</Head>
|
||||
<Component {...pageProps} />
|
||||
<ToastContainer bodyClassName={() => "Toastify__toast-body text-sm font-medium text-palette-500"} />
|
||||
</Elements>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ export default async (req, res) => {
|
|||
res.setHeader("Set-Cookie", header["set-cookie"]);
|
||||
res.redirect(req.query.return_to ?? "/");
|
||||
} catch (error) {
|
||||
console.log(`Cookie is present but authentication failed: ${error.message}`);
|
||||
|
||||
// credentials were correct but accounts service failed
|
||||
res.redirect("/.ory/kratos/public/self-service/browser/flows/logout");
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@ export async function getServerSideProps(context) {
|
|||
|
||||
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
|
||||
} catch (error) {
|
||||
console.log(`Unexpected error retrieving login flow: ${error.message}`);
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
|
|
|
@ -35,6 +35,8 @@ export async function getServerSideProps(context) {
|
|||
|
||||
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
|
||||
} catch (error) {
|
||||
console.log(`Unexpected error retrieving registration flow: ${error.message}`);
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
|
|
|
@ -12,8 +12,25 @@ const apiPrefix = process.env.NODE_ENV === "development" ? "/api/stubs" : "";
|
|||
const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink);
|
||||
const getRelativeDate = ({ downloadedOn }) => dayjs(downloadedOn).format("YYYY-MM-DD HH:mm:ss");
|
||||
const headers = [
|
||||
{ key: "name", name: "Name", nowrap: false, href: getSkylinkLink },
|
||||
{ key: "skylink", name: "Skylink" },
|
||||
{
|
||||
key: "name",
|
||||
name: "File",
|
||||
formatter: ({ name, skylink }) => (
|
||||
<>
|
||||
<p>
|
||||
<a
|
||||
href={getSkylinkLink({ skylink })}
|
||||
className="text-green-600 hover:text-green-900 break-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-gray-500 text-xs">{skylink}</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{ key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) },
|
||||
{ key: "downloadedOn", name: "Accessed on", formatter: getRelativeDate },
|
||||
];
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import dayjs from "dayjs";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { useState } from "react";
|
||||
import ky from "ky/umd";
|
||||
import { toast } from "react-toastify";
|
||||
import Layout from "../components/Layout";
|
||||
import Table from "../components/Table";
|
||||
import authServerSideProps from "../services/authServerSideProps";
|
||||
|
@ -12,12 +14,42 @@ const apiPrefix = process.env.NODE_ENV === "development" ? "/api/stubs" : "";
|
|||
const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink);
|
||||
const getRelativeDate = ({ uploadedOn }) => dayjs(uploadedOn).format("YYYY-MM-DD HH:mm:ss");
|
||||
const headers = [
|
||||
{ key: "name", name: "Name", nowrap: false, href: getSkylinkLink },
|
||||
{ key: "skylink", name: "Skylink" },
|
||||
{
|
||||
key: "name",
|
||||
name: "File",
|
||||
formatter: ({ name, skylink }) => (
|
||||
<>
|
||||
<p>
|
||||
<a
|
||||
href={getSkylinkLink({ skylink })}
|
||||
className="text-green-600 hover:text-green-900 break-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-gray-500 text-xs">{skylink}</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{ key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) },
|
||||
{ key: "uploadedOn", name: "Uploaded on", formatter: getRelativeDate },
|
||||
];
|
||||
const actions = [];
|
||||
const actions = [
|
||||
{
|
||||
name: "Unpin Skylink",
|
||||
action: async ({ skylink }, mutate) => {
|
||||
await toast.promise(ky.delete(`/user/uploads/${skylink}`), {
|
||||
pending: "Unpinning Skylink",
|
||||
success: "Skylink unpinned",
|
||||
error: (error) => error.message,
|
||||
});
|
||||
|
||||
mutate();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const getServerSideProps = authServerSideProps(async (context, api) => {
|
||||
const initialData = await api.get("user/uploads?pageSize=10&offset=0").json();
|
||||
|
@ -27,7 +59,7 @@ export const getServerSideProps = authServerSideProps(async (context, api) => {
|
|||
|
||||
export default function Uploads({ initialData }) {
|
||||
const [offset, setOffset] = useState(0);
|
||||
const { data } = useAccountsApi(`${apiPrefix}/user/uploads?pageSize=10&offset=${offset}`, {
|
||||
const { data, mutate } = useAccountsApi(`${apiPrefix}/user/uploads?pageSize=10&offset=${offset}`, {
|
||||
initialData: offset === 0 ? initialData : undefined,
|
||||
revalidateOnMount: true,
|
||||
});
|
||||
|
@ -38,7 +70,7 @@ export default function Uploads({ initialData }) {
|
|||
|
||||
return (
|
||||
<Layout title="Your uploads">
|
||||
<Table {...data} headers={headers} actions={actions} setOffset={setOffset} />
|
||||
<Table {...data} mutate={mutate} headers={headers} actions={actions} setOffset={setOffset} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,17 @@ const isProduction = process.env.NODE_ENV === "production";
|
|||
|
||||
export default function authServerSideProps(getServerSideProps) {
|
||||
return function authenticate(context) {
|
||||
if (isProduction && (!("ory_kratos_session" in context.req.cookies) || !("skynet-jwt" in context.req.cookies))) {
|
||||
const authCookies = ["ory_kratos_session", "skynet-jwt"];
|
||||
|
||||
if (isProduction && !authCookies.every((cookie) => context.req.cookies[cookie])) {
|
||||
// it is higly unusual that some of the cookies would be set but other would not
|
||||
if (authCookies.some((cookie) => context.req.cookies[cookie])) {
|
||||
console.log(
|
||||
"Unexpected auth cookies state!",
|
||||
authCookies.map((cookie) => `[${cookie}] is ${context.req.cookies[cookie] ? "set" : "not set"}`)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
|
|
|
@ -640,6 +640,11 @@ clipboardy@2.3.0:
|
|||
execa "^1.0.0"
|
||||
is-wsl "^2.1.1"
|
||||
|
||||
clsx@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
|
@ -2090,6 +2095,11 @@ normalize-range@^0.1.2:
|
|||
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
|
||||
integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
|
||||
|
||||
normalize.css@8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
|
||||
integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
|
||||
|
||||
npm-run-path@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
||||
|
@ -2523,6 +2533,13 @@ react-refresh@0.8.3:
|
|||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
|
||||
|
||||
react-toastify@8.0.2:
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-8.0.2.tgz#11f73b3a847fcffeb47b1823e8974e9895f98fae"
|
||||
integrity sha512-0Nud2d0VD4LIevgkB4L8NYoQ5plTpfqgj2CRVxs58SGA/TTO+2Ojz4C1bLUdGUWsw0zuWqd4GJqxNuMIv0cXMw==
|
||||
dependencies:
|
||||
clsx "^1.1.1"
|
||||
|
||||
react@17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||
|
|
Reference in New Issue