FE Auth vendor changes (#270)

This commit is contained in:
Kerem Yilmaz
2024-05-07 11:31:05 -07:00
committed by GitHub
parent 0862232db4
commit 5ce37dfaaf
16 changed files with 99 additions and 32 deletions

View File

@@ -18,7 +18,9 @@ export function setAuthorizationHeader(token: string) {
} }
export function removeAuthorizationHeader() { export function removeAuthorizationHeader() {
delete client.defaults.headers.common["Authorization"]; if (client.defaults.headers.common["Authorization"]) {
delete client.defaults.headers.common["Authorization"];
}
} }
export function setApiKeyHeader(apiKey: string) { export function setApiKeyHeader(apiKey: string) {
@@ -26,7 +28,24 @@ export function setApiKeyHeader(apiKey: string) {
} }
export function removeApiKeyHeader() { export function removeApiKeyHeader() {
delete client.defaults.headers.common["X-API-Key"]; if (client.defaults.headers.common["X-API-Key"]) {
delete client.defaults.headers.common["X-API-Key"];
}
} }
export { client, artifactApiClient }; async function getClient(credentialGetter: CredentialGetter | null) {
if (credentialGetter) {
removeApiKeyHeader();
const credential = await credentialGetter();
if (!credential) {
console.warn("No credential found");
return client;
}
setAuthorizationHeader(credential);
}
return client;
}
export type CredentialGetter = () => Promise<string | null>;
export { getClient, artifactApiClient };

View File

@@ -0,0 +1,15 @@
import { useTheme } from "./useTheme";
function useThemeAsDarkOrLight(): "light" | "dark" {
const { theme: baseTheme } = useTheme();
if (baseTheme === "dark" || baseTheme === "light") {
return baseTheme;
}
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
export { useThemeAsDarkOrLight };

View File

@@ -0,0 +1,9 @@
import { CredentialGetterContext } from "@/store/CredentialGetterContext";
import { useContext } from "react";
function useCredentialGetter() {
const credentialGetter = useContext(CredentialGetterContext);
return credentialGetter;
}
export { useCredentialGetter };

View File

@@ -3,10 +3,9 @@ import { ExitIcon, PersonIcon } from "@radix-ui/react-icons";
type Props = { type Props = {
name: string; name: string;
onLogout?: () => void;
}; };
function Profile({ name, onLogout }: Props) { function Profile({ name }: Props) {
return ( return (
<div className="flex items-center border-2 p-2 rounded-lg"> <div className="flex items-center border-2 p-2 rounded-lg">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
@@ -14,13 +13,7 @@ function Profile({ name, onLogout }: Props) {
<p className="w-40 overflow-hidden text-ellipsis">{name}</p> <p className="w-40 overflow-hidden text-ellipsis">{name}</p>
</div> </div>
<div> <div>
<Button <Button variant="outline" size="icon">
variant="outline"
size="icon"
onClick={() => {
onLogout?.();
}}
>
<ExitIcon className="h-4 w-4" /> <ExitIcon className="h-4 w-4" />
</Button> </Button>
</div> </div>

View File

@@ -7,11 +7,7 @@ import { Profile } from "./Profile";
import { useContext } from "react"; import { useContext } from "react";
import { UserContext } from "@/store/UserContext"; import { UserContext } from "@/store/UserContext";
type Props = { function RootLayout() {
onLogout?: () => void;
};
function RootLayout({ onLogout }: Props) {
const user = useContext(UserContext); const user = useContext(UserContext);
return ( return (
@@ -30,7 +26,7 @@ function RootLayout({ onLogout }: Props) {
<SideNav /> <SideNav />
{user ? ( {user ? (
<div className="absolute bottom-2 left-0 w-72 px-6 shrink-0"> <div className="absolute bottom-2 left-0 w-72 px-6 shrink-0">
<Profile name={user.name} onLogout={onLogout} /> <Profile name={user.name} />
</div> </div>
) : null} ) : null}
</aside> </aside>

View File

@@ -8,12 +8,12 @@ import { NavLink } from "react-router-dom";
function SideNav() { function SideNav() {
return ( return (
<nav className="flex flex-col gap-4"> <nav>
<NavLink <NavLink
to="create" to="create"
className={({ isActive }) => { className={({ isActive }) => {
return cn( return cn(
"flex items-center px-6 py-2 hover:bg-primary-foreground rounded-2xl", "flex items-center px-6 py-4 hover:bg-primary-foreground rounded-2xl",
{ {
"bg-primary-foreground": isActive, "bg-primary-foreground": isActive,
}, },
@@ -27,7 +27,7 @@ function SideNav() {
to="tasks" to="tasks"
className={({ isActive }) => { className={({ isActive }) => {
return cn( return cn(
"flex items-center px-6 py-2 hover:bg-primary-foreground rounded-2xl", "flex items-center px-6 py-4 hover:bg-primary-foreground rounded-2xl",
{ {
"bg-primary-foreground": isActive, "bg-primary-foreground": isActive,
}, },
@@ -41,7 +41,7 @@ function SideNav() {
to="settings" to="settings"
className={({ isActive }) => { className={({ isActive }) => {
return cn( return cn(
"flex items-center px-6 py-2 hover:bg-primary-foreground rounded-2xl", "flex items-center px-6 py-4 hover:bg-primary-foreground rounded-2xl",
{ {
"bg-primary-foreground": isActive, "bg-primary-foreground": isActive,
}, },

View File

@@ -21,7 +21,7 @@ import {
} from "../data/descriptionHelperContent"; } from "../data/descriptionHelperContent";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { client } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { InfoCircledIcon } from "@radix-ui/react-icons"; import { InfoCircledIcon } from "@radix-ui/react-icons";
import { import {
@@ -34,6 +34,7 @@ import { ToastAction } from "@radix-ui/react-toast";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import fetchToCurl from "fetch-to-curl"; import fetchToCurl from "fetch-to-curl";
import { apiBaseUrl, envCredential } from "@/util/env"; import { apiBaseUrl, envCredential } from "@/util/env";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
const createNewTaskFormSchema = z.object({ const createNewTaskFormSchema = z.object({
url: z.string().url({ url: z.string().url({
@@ -68,6 +69,7 @@ function createTaskRequestObject(formValues: CreateNewTaskFormValues) {
function CreateNewTaskForm({ initialValues }: Props) { function CreateNewTaskForm({ initialValues }: Props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { toast } = useToast(); const { toast } = useToast();
const credentialGetter = useCredentialGetter();
const form = useForm<CreateNewTaskFormValues>({ const form = useForm<CreateNewTaskFormValues>({
resolver: zodResolver(createNewTaskFormSchema), resolver: zodResolver(createNewTaskFormSchema),
@@ -75,8 +77,9 @@ function CreateNewTaskForm({ initialValues }: Props) {
}); });
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (formValues: CreateNewTaskFormValues) => { mutationFn: async (formValues: CreateNewTaskFormValues) => {
const taskRequest = createTaskRequestObject(formValues); const taskRequest = createTaskRequestObject(formValues);
const client = await getClient(credentialGetter);
return client.post< return client.post<
ReturnType<typeof createTaskRequestObject>, ReturnType<typeof createTaskRequestObject>,
{ data: { task_id: string } } { data: { task_id: string } }

View File

@@ -1,4 +1,4 @@
import { client } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { import {
ArtifactApiResponse, ArtifactApiResponse,
ArtifactType, ArtifactType,
@@ -16,6 +16,7 @@ import { TextArtifact } from "./TextArtifact";
import { getImageURL } from "./artifactUtils"; import { getImageURL } from "./artifactUtils";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { basicTimeFormat } from "@/util/timeFormat"; import { basicTimeFormat } from "@/util/timeFormat";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
type Props = { type Props = {
id: string; id: string;
@@ -24,6 +25,7 @@ type Props = {
function StepArtifacts({ id, stepProps }: Props) { function StepArtifacts({ id, stepProps }: Props) {
const { taskId } = useParams(); const { taskId } = useParams();
const credentialGetter = useCredentialGetter();
const { const {
data: artifacts, data: artifacts,
isFetching, isFetching,
@@ -32,6 +34,7 @@ function StepArtifacts({ id, stepProps }: Props) {
} = useQuery<Array<ArtifactApiResponse>>({ } = useQuery<Array<ArtifactApiResponse>>({
queryKey: ["task", taskId, "steps", id, "artifacts"], queryKey: ["task", taskId, "steps", id, "artifacts"],
queryFn: async () => { queryFn: async () => {
const client = await getClient(credentialGetter);
return client return client
.get(`/tasks/${taskId}/steps/${id}/artifacts`) .get(`/tasks/${taskId}/steps/${id}/artifacts`)
.then((response) => response.data); .then((response) => response.data);

View File

@@ -4,10 +4,12 @@ import { StepArtifacts } from "./StepArtifacts";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { StepApiResponse } from "@/api/types"; import { StepApiResponse } from "@/api/types";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { client } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
function StepArtifactsLayout() { function StepArtifactsLayout() {
const [activeIndex, setActiveIndex] = useState(0); const [activeIndex, setActiveIndex] = useState(0);
const credentialGetter = useCredentialGetter();
const { taskId } = useParams(); const { taskId } = useParams();
const { const {
@@ -17,6 +19,7 @@ function StepArtifactsLayout() {
} = useQuery<Array<StepApiResponse>>({ } = useQuery<Array<StepApiResponse>>({
queryKey: ["task", taskId, "steps"], queryKey: ["task", taskId, "steps"],
queryFn: async () => { queryFn: async () => {
const client = await getClient(credentialGetter);
return client return client
.get(`/tasks/${taskId}/steps`) .get(`/tasks/${taskId}/steps`)
.then((response) => response.data); .then((response) => response.data);

View File

@@ -1,10 +1,11 @@
import { client } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { StepApiResponse } from "@/api/types"; import { StepApiResponse } from "@/api/types";
import { cn } from "@/util/utils"; import { cn } from "@/util/utils";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useParams, useSearchParams } from "react-router-dom"; import { useParams, useSearchParams } from "react-router-dom";
import { PAGE_SIZE } from "../constants"; import { PAGE_SIZE } from "../constants";
import { CheckboxIcon, CrossCircledIcon } from "@radix-ui/react-icons"; import { CheckboxIcon, CrossCircledIcon } from "@radix-ui/react-icons";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
type Props = { type Props = {
activeIndex: number; activeIndex: number;
@@ -15,6 +16,7 @@ function StepNavigation({ activeIndex, onActiveIndexChange }: Props) {
const { taskId } = useParams(); const { taskId } = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1; const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
const credentialGetter = useCredentialGetter();
const { const {
data: steps, data: steps,
@@ -23,6 +25,7 @@ function StepNavigation({ activeIndex, onActiveIndexChange }: Props) {
} = useQuery<Array<StepApiResponse>>({ } = useQuery<Array<StepApiResponse>>({
queryKey: ["task", taskId, "steps", page], queryKey: ["task", taskId, "steps", page],
queryFn: async () => { queryFn: async () => {
const client = await getClient(credentialGetter);
return client return client
.get(`/tasks/${taskId}/steps`, { .get(`/tasks/${taskId}/steps`, {
params: { params: {

View File

@@ -1,4 +1,4 @@
import { client } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { Status, TaskApiResponse } from "@/api/types"; import { Status, TaskApiResponse } from "@/api/types";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
@@ -20,9 +20,11 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
function TaskDetails() { function TaskDetails() {
const { taskId } = useParams(); const { taskId } = useParams();
const credentialGetter = useCredentialGetter();
const { const {
data: task, data: task,
@@ -32,6 +34,7 @@ function TaskDetails() {
} = useQuery<TaskApiResponse>({ } = useQuery<TaskApiResponse>({
queryKey: ["task", taskId, "details"], queryKey: ["task", taskId, "details"],
queryFn: async () => { queryFn: async () => {
const client = await getClient(credentialGetter);
return client.get(`/tasks/${taskId}`).then((response) => response.data); return client.get(`/tasks/${taskId}`).then((response) => response.data);
}, },
refetchInterval: (query) => { refetchInterval: (query) => {

View File

@@ -1,4 +1,4 @@
import { client } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { TaskApiResponse } from "@/api/types"; import { TaskApiResponse } from "@/api/types";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { import {
@@ -32,11 +32,13 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
function TaskList() { function TaskList() {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1; const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
const credentialGetter = useCredentialGetter();
const { const {
data: tasks, data: tasks,
@@ -46,6 +48,7 @@ function TaskList() {
} = useQuery<Array<TaskApiResponse>>({ } = useQuery<Array<TaskApiResponse>>({
queryKey: ["tasks", "all", page], queryKey: ["tasks", "all", page],
queryFn: async () => { queryFn: async () => {
const client = await getClient(credentialGetter);
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append("page", String(page)); params.append("page", String(page));
params.append("page_size", String(PAGE_SIZE)); params.append("page_size", String(PAGE_SIZE));

View File

@@ -1,4 +1,4 @@
import { client } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { import {
ArtifactApiResponse, ArtifactApiResponse,
ArtifactType, ArtifactType,
@@ -7,12 +7,15 @@ import {
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getImageURL } from "../detail/artifactUtils"; import { getImageURL } from "../detail/artifactUtils";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
type Props = { type Props = {
id: string; id: string;
}; };
function LatestScreenshot({ id }: Props) { function LatestScreenshot({ id }: Props) {
const credentialGetter = useCredentialGetter();
const { const {
data: artifact, data: artifact,
isFetching, isFetching,
@@ -20,6 +23,7 @@ function LatestScreenshot({ id }: Props) {
} = useQuery<ArtifactApiResponse | undefined>({ } = useQuery<ArtifactApiResponse | undefined>({
queryKey: ["task", id, "latestScreenshot"], queryKey: ["task", id, "latestScreenshot"],
queryFn: async () => { queryFn: async () => {
const client = await getClient(credentialGetter);
const steps: StepApiResponse[] = await client const steps: StepApiResponse[] = await client
.get(`/tasks/${id}/steps`) .get(`/tasks/${id}/steps`)
.then((response) => response.data); .then((response) => response.data);

View File

@@ -1,4 +1,4 @@
import { client } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { Status, TaskApiResponse } from "@/api/types"; import { Status, TaskApiResponse } from "@/api/types";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { basicTimeFormat } from "@/util/timeFormat"; import { basicTimeFormat } from "@/util/timeFormat";
@@ -12,12 +12,16 @@ import {
} from "@/components/ui/table"; } from "@/components/ui/table";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { StatusBadge } from "@/components/StatusBadge"; import { StatusBadge } from "@/components/StatusBadge";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
function QueuedTasks() { function QueuedTasks() {
const navigate = useNavigate(); const navigate = useNavigate();
const credentialGetter = useCredentialGetter();
const { data: tasks } = useQuery<Array<TaskApiResponse>>({ const { data: tasks } = useQuery<Array<TaskApiResponse>>({
queryKey: ["tasks", "queued"], queryKey: ["tasks", "queued"],
queryFn: async () => { queryFn: async () => {
const client = await getClient(credentialGetter);
return client return client
.get("/tasks", { .get("/tasks", {
params: { params: {

View File

@@ -1,4 +1,4 @@
import { client } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { TaskApiResponse } from "@/api/types"; import { TaskApiResponse } from "@/api/types";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -12,13 +12,16 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { basicTimeFormat } from "@/util/timeFormat"; import { basicTimeFormat } from "@/util/timeFormat";
import { LatestScreenshot } from "./LatestScreenshot"; import { LatestScreenshot } from "./LatestScreenshot";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
function RunningTasks() { function RunningTasks() {
const navigate = useNavigate(); const navigate = useNavigate();
const credentialGetter = useCredentialGetter();
const { data: runningTasks } = useQuery<Array<TaskApiResponse>>({ const { data: runningTasks } = useQuery<Array<TaskApiResponse>>({
queryKey: ["tasks", "running"], queryKey: ["tasks", "running"],
queryFn: async () => { queryFn: async () => {
const client = await getClient(credentialGetter);
return client return client
.get("/tasks", { .get("/tasks", {
params: { params: {

View File

@@ -0,0 +1,6 @@
import { CredentialGetter } from "@/api/AxiosClient";
import { createContext } from "react";
const CredentialGetterContext = createContext<CredentialGetter | null>(null);
export { CredentialGetterContext };