Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

interface for api tokens #266

Merged
merged 3 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
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
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
Loading