mirror of
https://github.com/moghtech/komodo.git
synced 2026-03-11 17:44:19 -05:00
fix the fe errors with most boilerplate
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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]. */
|
||||
|
||||
@@ -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()} />;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
0
frontend/src/components/resources/sync/actions.tsx
Normal file
0
frontend/src/components/resources/sync/actions.tsx
Normal file
81
frontend/src/components/resources/sync/config.tsx
Normal file
81
frontend/src/components/resources/sync/config.tsx
Normal 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,
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
83
frontend/src/components/resources/sync/index.tsx
Normal file
83
frontend/src/components/resources/sync/index.tsx
Normal 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} />,
|
||||
};
|
||||
45
frontend/src/components/resources/sync/table.tsx
Normal file
45
frontend/src/components/resources/sync/table.tsx
Normal 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} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user