Skip to content

Commit

Permalink
interface for api tokens (#266)
Browse files Browse the repository at this point in the history
and another cleanup pass through the app, which was becoming difficult to work in
  • Loading branch information
jbr authored Jul 7, 2023
1 parent b9e71aa commit 7669c9c
Show file tree
Hide file tree
Showing 37 changed files with 1,234 additions and 631 deletions.
388 changes: 174 additions & 214 deletions Cargo.lock

Large diffs are not rendered by default.

63 changes: 47 additions & 16 deletions app/src/ApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,20 @@ export interface NewAggregator {
bearer_token: string;
}

export interface ApiToken {
id: string;
account_id: string;
token_hash: string;
created_at: string;
deleted_at?: string;
name?: string;
last_used_at?: string;
}

const mime = "application/vnd.divviup+json;version=0.1";

export class ApiClient {
private client?: Promise<AxiosInstance> | AxiosInstance;
currentUser?: User;

static async fetchBaseUrl(): Promise<URL> {
let url = new URL(window.location.href);
Expand All @@ -135,7 +144,7 @@ export class ApiClient {
Accept: mime,
},
validateStatus(status) {
return status >= 200 && status < 500;
return (status >= 200 && status < 300) || status == 400;
},
});
}
Expand All @@ -151,30 +160,27 @@ export class ApiClient {
return (await this.populateClient()).getUri({ url: "/login" });
}

async logoutUrl(): Promise<string> {
return (await this.populateClient()).getUri({ url: "/logout" });
async redirectToLogin(): Promise<null> {
let loginUrl = await this.loginUrl();
window.location.href = loginUrl;
return null;
}

isLoggedIn(): boolean {
return !!this.currentUser;
async logoutUrl(): Promise<string> {
return (await this.populateClient()).getUri({ url: "/logout" });
}

async getCurrentUser(): Promise<User> {
if (this.currentUser) {
return this.currentUser;
}
let client = await this.populateClient();
let res = await client.get("/api/users/me");
this.currentUser = res.data as User;
return this.currentUser;
let res = await this.get("/api/users/me");
return res.data as User;
}

private async get(path: string): Promise<AxiosResponse> {
let client = await this.populateClient();
return client.get(path);
}

private async post(path: string, body: unknown): Promise<AxiosResponse> {
private async post(path: string, body?: unknown): Promise<AxiosResponse> {
let client = await this.populateClient();
return client.post(path, body);
}
Expand Down Expand Up @@ -302,6 +308,31 @@ export class ApiClient {
}
}

async accountApiTokens(accountId: string): Promise<ApiToken[]> {
const res = await this.get(`/api/accounts/${accountId}/api_tokens`);
return res.data as ApiToken[];
}

async createApiToken(
accountId: string
): Promise<ApiToken & { token: string }> {
const res = await this.post(`/api/accounts/${accountId}/api_tokens`);
return res.data as ApiToken & { token: string };
}

async deleteApiToken(tokenId: string): Promise<null> {
await this.delete(`/api/api_tokens/${tokenId}`);
return null;
}

async updateApiToken(
tokenId: string,
token: { name: string }
): Promise<null> {
await this.patch(`/api/api_tokens/${tokenId}`, token);
return null;
}

async queue(searchParams: URLSearchParams): Promise<QueueJob[]> {
const res = await this.get(`/api/admin/queue?${searchParams}`);
return res.data as QueueJob[];
Expand Down Expand Up @@ -377,8 +408,8 @@ export interface FormikLikeErrors {

export type ValidationErrorsFor<T extends object> = {
[K in keyof T]?: T[K] extends object
? ValidationErrorsFor<T[K]>
: ValidationError[];
? ValidationErrorsFor<T[K]>
: ValidationError[];
};

export interface ValidationError {
Expand Down
File renamed without changes.
8 changes: 5 additions & 3 deletions app/src/AccountList.tsx → app/src/accounts/AccountList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import Col from "react-bootstrap/Col";
import Breadcrumb from "react-bootstrap/Breadcrumb";
import React from "react";
import { useLoaderData, useAsyncValue, Await } from "react-router-dom";
import { Account } from "./ApiClient";
import { Account } from "../ApiClient";
import ListGroup from "react-bootstrap/ListGroup";
import { LinkContainer } from "react-router-bootstrap";
import { Button } from "react-bootstrap";
import { Button, Placeholder } from "react-bootstrap";
import { BuildingAdd } from "react-bootstrap-icons";

function Breadcrumbs() {
Expand Down Expand Up @@ -49,7 +49,9 @@ export default function AccountList() {
<React.Suspense
fallback={
<ListGroup>
<ListGroup.Item>Loading</ListGroup.Item>
<ListGroup.Item>
<Placeholder animation="glow" xs={12} />
</ListGroup.Item>
</ListGroup>
}
>
Expand Down
24 changes: 12 additions & 12 deletions app/src/AccountSummary.tsx → app/src/accounts/AccountSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,20 @@ import Breadcrumb from "react-bootstrap/Breadcrumb";
import Col from "react-bootstrap/Col";
import Row from "react-bootstrap/Row";
import ListGroup from "react-bootstrap/ListGroup";
import {
Await,
Form,
useActionData,
useAsyncValue,
useRouteLoaderData,
} from "react-router-dom";
import { Form, useActionData } from "react-router-dom";
import { Suspense, useCallback, useEffect, useState } from "react";
import { Account } from "./ApiClient";
import Spinner from "react-bootstrap/Spinner";
import { LinkContainer } from "react-router-bootstrap";
import {
Building,
CloudUpload,
FileEarmarkCode,
PencilFill,
People,
ShieldLock,
} from "react-bootstrap-icons";
import { Button, FormControl, InputGroup } from "react-bootstrap";
import { WithAccount } from "./util";
import { WithAccount } from "../util";
import Placeholder from "react-bootstrap/Placeholder";

function AccountName() {
let [isEditingName, setIsEditingName] = useState(false);
Expand Down Expand Up @@ -72,7 +66,7 @@ function AccountName() {
<Col xs="11">
<h1>
<Building />
<Suspense fallback="...">
<Suspense fallback={<Placeholder animation="glow" xs={6} />}>
<WithAccount>{(account) => account.name}</WithAccount>
</Suspense>
</h1>
Expand Down Expand Up @@ -100,7 +94,7 @@ export default function AccountSummary() {
<Breadcrumb.Item>Accounts</Breadcrumb.Item>
</LinkContainer>
<Breadcrumb.Item active>
<Suspense fallback="...">
<Suspense fallback={<Placeholder animation="glow" xs={6} />}>
<WithAccount>{(account) => account.name}</WithAccount>
</Suspense>
</Breadcrumb.Item>
Expand Down Expand Up @@ -128,6 +122,12 @@ export default function AccountSummary() {
<CloudUpload /> Aggregators
</ListGroup.Item>
</LinkContainer>

<LinkContainer to="api_tokens">
<ListGroup.Item action>
<ShieldLock /> API Tokens
</ListGroup.Item>
</LinkContainer>
</ListGroup>
</Col>
</Row>
Expand Down
81 changes: 81 additions & 0 deletions app/src/accounts/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { RouteObject, defer, redirect } from "react-router-dom";
import ApiClient, { PartialAccount } from "../ApiClient";
import AccountSummary from "./AccountSummary";
import AccountForm from "./AccountForm";
import AccountList from "./AccountList";

export default function accounts(
apiClient: ApiClient,
children: RouteObject[]
): RouteObject {
return {
path: "accounts",

children: [
{
path: "",
element: <AccountList />,
loader() {
return defer({ accounts: apiClient.accounts() });
},
index: true,
},
{
path: ":account_id",
id: "account",
loader({ params }) {
return defer({
account: apiClient.account(params.account_id as string),
});
},

async action({ params, request }) {
let data = Object.fromEntries(await request.formData());
switch (request.method) {
case "PATCH":
return {
account: await apiClient.updateAccount(
params.account_id as string,
data as unknown as PartialAccount
),
};
default:
throw new Error(`unexpected method ${request.method}`);
}
},

shouldRevalidate(args) {
return (
typeof args.actionResult === "object" &&
args.actionResult !== null &&
"account" in args.actionResult
);
},
children: [
{
path: "",
element: <AccountSummary />,
},
{
path: "new",
element: <AccountForm />,
async action({ request }) {
let data = Object.fromEntries(await request.formData());
switch (request.method) {
case "POST":
const account = await apiClient.createAccount(
data as unknown as PartialAccount
);
return redirect(`/accounts/${account.id}`);
default:
throw new Error(`unexpected method ${request.method}`);
}
},
},

...children,
],
},
],
};
}
33 changes: 33 additions & 0 deletions app/src/admin/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { RouteObject } from "react-router-dom";
import ApiClient from "../ApiClient";

export default function admin(apiClient: ApiClient): RouteObject {
return {
path: "admin",
children: [
{
path: "queue",
async lazy() {
return import("../admin/Queue");
},
async loader({ request }) {
const params = new URL(request.url).searchParams;
return apiClient.queue(params);
},
children: [
{
path: ":job_id",
async lazy() {
return import("../admin/QueueJob");
},

async loader({ params }) {
if ("job_id" in params && typeof params.job_id === "string")
return apiClient.queueJob(params.job_id);
},
},
],
},
],
};
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Await, useLoaderData, useParams } from "react-router-dom";
import { Aggregator } from "./ApiClient";
import { AccountBreadcrumbs } from "./util";
import { Aggregator } from "../ApiClient";
import { AccountBreadcrumbs } from "../util";
import { LinkContainer } from "react-router-bootstrap";
import Breadcrumb from "react-bootstrap/Breadcrumb";
import React, { Suspense } from "react";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import { CloudUpload } from "react-bootstrap-icons";
import Table from "react-bootstrap/Table";
import D from "./logo/color/svg/small.svg";
import D from "../logo/color/svg/small.svg";
import Placeholder from "react-bootstrap/Placeholder";

function Breadcrumbs() {
let { aggregator } = useLoaderData() as {
Expand All @@ -22,7 +23,7 @@ function Breadcrumbs() {
<Breadcrumb.Item>Aggregators</Breadcrumb.Item>
</LinkContainer>
<Breadcrumb.Item active>
<React.Suspense fallback="...">
<React.Suspense fallback={<Placeholder animation="glow" xs={6} />}>
<Await resolve={aggregator}>{(aggregator) => aggregator.name}</Await>
</React.Suspense>
</Breadcrumb.Item>
Expand All @@ -44,7 +45,7 @@ export default function AggregatorDetail() {
<Suspense
fallback={
<>
<CloudUpload /> {" ..."}
<CloudUpload /> <Placeholder animation="glow" xs={6} />
</>
}
>
Expand Down Expand Up @@ -121,7 +122,7 @@ export function WithAggregator({
};

return (
<Suspense fallback="...">
<Suspense fallback={<Placeholder animation="glow" xs={6} />}>
<Await resolve={aggregator} children={children} />
</Suspense>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Breadcrumb from "react-bootstrap/Breadcrumb";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { AccountBreadcrumbs, WithAccount } from "./util";
import { AccountBreadcrumbs, WithAccount } from "../util";
import { CloudUpload } from "react-bootstrap-icons";
import React from "react";
import { LinkContainer } from "react-router-bootstrap";
Expand All @@ -12,16 +12,17 @@ import FormControl from "react-bootstrap/FormControl";
import FormGroup from "react-bootstrap/FormGroup";
import FormLabel from "react-bootstrap/FormLabel";
import FormSelect from "react-bootstrap/FormSelect";
import ApiClient, { NewAggregator, formikErrors } from "./ApiClient";
import ApiClient, { NewAggregator, formikErrors } from "../ApiClient";
import {
NavigateFunction,
useActionData,
useNavigate,
useNavigation,
useParams,
} from "react-router-dom";
import { ApiClientContext } from "./ApiClientContext";
import { ApiClientContext } from "../ApiClientContext";
const { Suspense } = React;
import Placeholder from "react-bootstrap/Placeholder";

async function submit(
apiClient: ApiClient,
Expand Down Expand Up @@ -67,7 +68,7 @@ export default function AggregatorForm() {
<Col>
<h1>
<CloudUpload />{" "}
<Suspense fallback="...">
<Suspense fallback={<Placeholder animation="glow" xs={6} />}>
<WithAccount>{(account) => account.name}</WithAccount>
</Suspense>{" "}
Aggregators
Expand Down
Loading

0 comments on commit 7669c9c

Please sign in to comment.