fix the fe errors with most boilerplate

This commit is contained in:
mbecker20
2024-06-07 20:00:01 -07:00
parent 45eafd10b9
commit 5eacb7191b
15 changed files with 386 additions and 42 deletions

View File

@@ -110,7 +110,7 @@ impl Resolve<GetBuilderAvailableAccounts, User> for State {
let res = self
.resolve(
read::GetAvailableAccounts {
server: config.server_id,
server: Some(config.server_id),
},
user,
)

View File

@@ -389,18 +389,24 @@ impl Resolve<GetAvailableAccounts, User> for State {
GetAvailableAccounts { server }: GetAvailableAccounts,
user: User,
) -> anyhow::Result<GetAvailableAccountsResponse> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Read,
)
.await?;
let (github, docker) = match server {
Some(server) => {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Read,
)
.await?;
let GetAccountsResponse { github, docker } =
periphery_client(&server)?
.request(api::GetAccounts {})
.await
.context("failed to get accounts from periphery")?;
let GetAccountsResponse { github, docker } =
periphery_client(&server)?
.request(api::GetAccounts {})
.await
.context("failed to get accounts from periphery")?;
(github, docker)
}
None => Default::default(),
};
let mut github_set = HashSet::<String>::new();

View File

@@ -33,7 +33,7 @@ mod variable;
#[resolver_target(State)]
#[resolver_args(User)]
#[serde(tag = "type", content = "params")]
enum WriteRequest {
pub enum WriteRequest {
// ==== SERVICE USER ====
CreateServiceUser(CreateServiceUser),
UpdateServiceUserDescription(UpdateServiceUserDescription),

View File

@@ -5,9 +5,10 @@ use axum::{extract::Path, http::HeaderMap, routing::post, Router};
use hex::ToHex;
use hmac::{Hmac, Mac};
use monitor_client::{
api::execute,
api::{execute, write::RefreshResourceSyncPending},
entities::{
build::Build, procedure::Procedure, repo::Repo, user::github_user,
build::Build, procedure::Procedure, repo::Repo,
sync::ResourceSync, user::github_user,
},
};
use resolver_api::Resolve;
@@ -117,6 +118,50 @@ pub fn router() -> Router {
},
)
)
.route(
"/sync/:id/refresh",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
tokio::spawn(async move {
let span = info_span!("sync_refresh_webhook", id);
async {
let res = handle_sync_refresh_webhook(
id.clone(),
headers,
body
).await;
if let Err(e) = res {
warn!("failed to run sync webook for sync {id} | {e:#}");
}
}
.instrument(span)
.await
});
},
)
)
.route(
"/sync/:id/sync",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
tokio::spawn(async move {
let span = info_span!("sync_execute_webhook", id);
async {
let res = handle_sync_execute_webhook(
id.clone(),
headers,
body
).await;
if let Err(e) = res {
warn!("failed to run sync webook for sync {id} | {e:#}");
}
}
.instrument(span)
.await
});
},
)
)
}
async fn handle_build_webhook(
@@ -253,6 +298,66 @@ async fn handle_procedure_webhook(
Ok(())
}
async fn handle_sync_refresh_webhook(
sync_id: String,
headers: HeaderMap,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = sync_locks().get_or_insert_default(&sync_id).await;
let _lock = lock.lock().await;
verify_gh_signature(headers, &body).await?;
let request_branch = extract_branch(&body)?;
let sync = resource::get::<ResourceSync>(&sync_id).await?;
if !sync.config.webhook_enabled {
return Err(anyhow!("sync does not have webhook enabled"));
}
if request_branch != sync.config.branch {
return Err(anyhow!("request branch does not match expected"));
}
let user = github_user().to_owned();
State
.resolve(RefreshResourceSyncPending { sync: sync_id }, user)
.await?;
Ok(())
}
async fn handle_sync_execute_webhook(
sync_id: String,
headers: HeaderMap,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = sync_locks().get_or_insert_default(&sync_id).await;
let _lock = lock.lock().await;
verify_gh_signature(headers, &body).await?;
let request_branch = extract_branch(&body)?;
let sync = resource::get::<ResourceSync>(&sync_id).await?;
if !sync.config.webhook_enabled {
return Err(anyhow!("sync does not have webhook enabled"));
}
if request_branch != sync.config.branch {
return Err(anyhow!("request branch does not match expected"));
}
let user = github_user().to_owned();
let req =
crate::api::execute::ExecuteRequest::RunSync(execute::RunSync {
sync: sync_id,
});
let update = init_execution_update(&req, &user).await?;
let crate::api::execute::ExecuteRequest::RunSync(req) = req else {
unreachable!()
};
State.resolve(req, (user, update)).await?;
Ok(())
}
#[instrument(skip_all)]
async fn verify_gh_signature(
headers: HeaderMap,
@@ -313,3 +418,8 @@ fn procedure_locks() -> &'static ListenerLockCache {
static BUILD_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
BUILD_LOCKS.get_or_init(Default::default)
}
fn sync_locks() -> &'static ListenerLockCache {
static SYNC_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
SYNC_LOCKS.get_or_init(Default::default)
}

View File

@@ -320,7 +320,9 @@ pub struct GetServersSummaryResponse {
//
/// Get the usernames for the available github / docker accounts
/// on the target server.
/// on the target server, or only available globally if no server
/// is provided.
///
/// Response: [GetAvailableAccountsResponse].
#[typeshare]
#[derive(
@@ -331,7 +333,7 @@ pub struct GetServersSummaryResponse {
pub struct GetAvailableAccounts {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub server: String,
pub server: Option<String>,
}
/// Response for [GetAvailableAccounts].

View File

@@ -1197,6 +1197,10 @@ export interface PendingUpdates {
server_template_updates?: string;
/** Readable log of any pending resource sync updates */
resource_sync_updates?: string;
/** Readable log of any pending variable updates */
variable_updates?: string;
/** Readable log of any pending user group updates */
user_group_updates?: string;
}
export interface ResourceSyncInfo {
@@ -2566,12 +2570,14 @@ export interface GetServersSummaryResponse {
/**
* Get the usernames for the available github / docker accounts
* on the target server.
* on the target server, or only available globally if no server
* is provided.
*
* Response: [GetAvailableAccountsResponse].
*/
export interface GetAvailableAccounts {
/** Id or name */
server: string;
server?: string;
}
/** Response for [GetAvailableAccounts]. */

View File

@@ -82,7 +82,7 @@ export const ConfigLayout = <
);
};
type PrimitiveConfigArgs = { placeholder: string };
type PrimitiveConfigArgs = { placeholder?: string; label?: string };
type ConfigComponent<T> = {
label: string;
@@ -258,7 +258,7 @@ export const ConfigAgain = <
case "string":
return (
<ConfigInput
key={key.toString()}
key={args?.label ?? key.toString()}
label={key.toString()}
value={value}
onChange={(value) => set({ [key]: value } as Partial<T>)}
@@ -270,7 +270,7 @@ export const ConfigAgain = <
return (
<ConfigInput
key={key.toString()}
label={key.toString()}
label={args?.label ?? key.toString()}
value={Number(value)}
onChange={(value) =>
set({ [key]: Number(value) } as Partial<T>)
@@ -283,14 +283,14 @@ export const ConfigAgain = <
return (
<ConfigSwitch
key={key.toString()}
label={key.toString()}
label={args?.label ?? key.toString()}
value={value}
onChange={(value) => set({ [key]: value } as Partial<T>)}
disabled={disabled}
/>
);
default:
return <div>{key.toString()}</div>;
return <div>{args?.label ?? key.toString()}</div>;
}
} else if (component === false) {
return <Fragment key={key.toString()} />;

View File

@@ -194,18 +194,18 @@ export const AccountSelector = ({
placeholder,
}: {
disabled: boolean;
id: string | undefined;
type: "Server" | "Builder";
id?: string;
type: "Server" | "None" | "Builder";
account_type: keyof Types.GetBuilderAvailableAccountsResponse;
selected: string | undefined;
onSelect: (id: string) => void;
placeholder: string;
}) => {
const [request, params] =
type === "Server"
? ["GetAvailableAccounts", { server: id! }]
type === "Server" || type === "None"
? ["GetAvailableAccounts", { server: id }]
: ["GetBuilderAvailableAccounts", { builder: id }];
const accounts = useRead(request as any, params, { enabled: !!id }).data;
const accounts = useRead(request as any, params).data;
return (
<ConfigItem label={`${account_type} Account`}>
<Select

View File

@@ -7,6 +7,7 @@ import { RepoComponents } from "./repo";
import { ServerComponents } from "./server";
import { ProcedureComponents } from "./procedure/index";
import { ServerTemplateComponents } from "./server-template";
import { ResourceSyncComponents } from "./sync";
export const ResourceComponents: {
[key in UsableResource]: RequiredResourceComponents;
@@ -19,4 +20,5 @@ export const ResourceComponents: {
Builder: BuilderComponents,
ServerTemplate: ServerTemplateComponents,
Alerter: AlerterComponents,
ResourceSync: ResourceSyncComponents,
};

View File

@@ -713,4 +713,15 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
/>
),
},
RunSync: {
params: { sync: "" },
Component: ({ params, setParams, disabled }) => (
<ResourceSelector
type="ResourceSync"
selected={params.sync}
onSelect={(id) => setParams({ sync: id })}
disabled={disabled}
/>
),
},
};

View File

@@ -54,19 +54,17 @@ export const RepoConfig = ({ id }: { id: string }) => {
commit: { placeholder: "Enter specific commit hash. Optional." },
github_account: (value, set) => {
const server_id = update.server_id || config.server_id;
if (server_id) {
return (
<AccountSelector
id={server_id}
account_type="github"
type="Server"
selected={value}
onSelect={(github_account) => set({ github_account })}
disabled={disabled}
placeholder="None"
/>
);
}
return (
<AccountSelector
id={server_id}
account_type="github"
type={server_id ? "Server" : "None"}
selected={value}
onSelect={(github_account) => set({ github_account })}
disabled={disabled}
placeholder="None"
/>
);
},
},
},

View File

@@ -0,0 +1,81 @@
import { Config } from "@components/config";
import { AccountSelector, ConfigItem } from "@components/config/util";
import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { ReactNode, useState } from "react";
import { CopyGithubWebhook } from "../common";
export const ResourceSyncConfig = ({
id,
titleOther,
}: {
id: string;
titleOther: ReactNode;
}) => {
const perms = useRead("GetPermissionLevel", {
target: { type: "ResourceSync", id },
}).data;
const config = useRead("GetResourceSync", { sync: id }).data?.config;
const global_disabled =
useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false;
const [update, set] = useState<Partial<Types.ResourceSyncConfig>>({});
const { mutateAsync } = useWrite("UpdateResourceSync");
if (!config) return null;
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
return (
<Config
titleOther={titleOther}
disabled={disabled}
config={config}
update={update}
set={set}
onSave={async () => {
await mutateAsync({ id, config: update });
}}
components={{
general: [
{
label: "General",
components: {
repo: { placeholder: "Enter repo" },
branch: { placeholder: "Enter branch" },
commit: { placeholder: "Enter specific commit hash. Optional." },
github_account: (value, set) => {
return (
<AccountSelector
account_type="github"
type="None"
selected={value}
onSelect={(github_account) => set({ github_account })}
disabled={disabled}
placeholder="None"
/>
);
},
resource_path: { placeholder: "./resources" },
delete: { label: "Delete Unmatched Resources" },
},
},
{
label: "Github Webhook",
components: {
["refresh" as any]: () => (
<ConfigItem label="Refresh Pending">
<CopyGithubWebhook path={`/sync/${id}/refresh`} />
</ConfigItem>
),
["execute" as any]: () => (
<ConfigItem label="Execute Sync">
<CopyGithubWebhook path={`/sync/${id}/execute`} />
</ConfigItem>
),
webhook_enabled: true,
},
},
],
}}
/>
);
};

View File

@@ -0,0 +1,83 @@
import { useRead } from "@lib/hooks";
import { RequiredResourceComponents } from "@types";
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { FolderSync } from "lucide-react";
import { Link } from "react-router-dom";
import { DeleteResource, NewResource } from "../common";
import { ResourceSyncTable } from "./table";
import { Types } from "@monitor/client";
import { ResourceSyncConfig } from "./config";
import { useState } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
const useResourceSync = (id?: string) =>
useRead("ListResourceSyncs", {}).data?.find((d) => d.id === id);
const PendingOrConfig = ({ id }: { id: string }) => {
const [view, setView] = useState("Pending");
const pendingDisabled = true;
const currentView = view === "Pending" && pendingDisabled ? "Config" : view;
const tabsList = (
<TabsList className="justify-start w-fit">
<TabsTrigger value="Pending" className="w-[110px]" disabled={true}>
Pending
</TabsTrigger>
<TabsTrigger value="Config" className="w-[110px]">
Config
</TabsTrigger>
</TabsList>
);
return (
<Tabs value={currentView} onValueChange={setView} className="grid gap-4">
<TabsContent value="Config">
<ResourceSyncConfig id={id} titleOther={tabsList} />
</TabsContent>
</Tabs>
);
};
export const ResourceSyncComponents: RequiredResourceComponents = {
list_item: (id) => useResourceSync(id),
Dashboard: () => {
const syncs_count = useRead("ListResourceSyncs", {}).data?.length;
return (
<Link to="/syncs/" className="w-full">
<Card className="hover:bg-accent/50 transition-colors cursor-pointer">
<CardHeader>
<div className="flex justify-between">
<div>
<CardTitle>Syncs</CardTitle>
<CardDescription>{syncs_count} Total</CardDescription>
</div>
<FolderSync className="w-4 h-4" />
</div>
</CardHeader>
</Card>
</Link>
);
},
New: () => <NewResource type="ResourceSync" />,
Table: ({ resources }) => (
<ResourceSyncTable syncs={resources as Types.ResourceSyncListItem[]} />
),
Icon: () => <FolderSync className="w-4 h-4" />,
BigIcon: () => <FolderSync className="w-8 h-8" />,
Status: {},
Info: {},
Actions: {},
Page: {},
Config: PendingOrConfig,
DangerZone: ({ id }) => <DeleteResource type="ResourceSync" id={id} />,
};

View File

@@ -0,0 +1,45 @@
import { DataTable, SortableHeader } from "@ui/data-table";
import { ResourceLink } from "../common";
import { TableTags } from "@components/tags";
import { Types } from "@monitor/client";
export const ResourceSyncTable = ({
syncs,
}: {
syncs: Types.ResourceSyncListItem[];
}) => {
return (
<DataTable
tableKey="syncs"
data={syncs}
columns={[
{
accessorKey: "name",
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
cell: ({ row }) => (
<ResourceLink type="ResourceSync" id={row.original.id} />
),
},
// {
// accessorKey: "info.builder_type",
// header: ({ column }) => (
// <SortableHeader column={column} title="Provider" />
// ),
// },
// {
// accessorKey: "info.instance_type",
// header: ({ column }) => (
// <SortableHeader column={column} title="Instance Type" />
// ),
// cell: ({ row }) => <BuilderInstanceType id={row.original.id} />,
// },
{
header: "Tags",
cell: ({ row }) => <TableTags tag_ids={row.original.tags} />,
},
]}
/>
);
};