forked from github-starred/komodo
no unnecessary user group sync
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::{anyhow, Context};
|
||||
use mongo_indexed::doc;
|
||||
use monitor_client::{
|
||||
api::{execute::RunSync, write::RefreshResourceSyncPending},
|
||||
@@ -48,6 +48,10 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
>(&sync, &user, PermissionLevel::Execute)
|
||||
.await?;
|
||||
|
||||
if sync.config.repo.is_empty() {
|
||||
return Err(anyhow!("resource sync repo not configured"));
|
||||
}
|
||||
|
||||
let (res, logs, hash, message) =
|
||||
crate::helpers::sync::remote::get_remote_resources(&sync)
|
||||
.await
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::{anyhow, Context};
|
||||
use monitor_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
@@ -104,6 +104,10 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
>(&sync, &user, PermissionLevel::Execute)
|
||||
.await?;
|
||||
|
||||
if sync.config.repo.is_empty() {
|
||||
return Err(anyhow!("resource sync repo not configured"));
|
||||
}
|
||||
|
||||
let res = async {
|
||||
let (res, _, hash, message) =
|
||||
crate::helpers::sync::remote::get_remote_resources(&sync)
|
||||
|
||||
@@ -275,7 +275,11 @@ pub async fn get_updates_for_view(
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Some(update))
|
||||
let any_change = update.to_create > 0
|
||||
|| update.to_update > 0
|
||||
|| update.to_delete > 0;
|
||||
|
||||
Ok(any_change.then_some(update))
|
||||
}
|
||||
|
||||
pub async fn get_updates_for_execution(
|
||||
|
||||
@@ -120,7 +120,11 @@ pub async fn get_updates_for_view(
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Some(update))
|
||||
let any_change = update.to_create > 0
|
||||
|| update.to_update > 0
|
||||
|| update.to_delete > 0;
|
||||
|
||||
Ok(any_change.then_some(update))
|
||||
}
|
||||
|
||||
pub async fn get_updates_for_execution(
|
||||
|
||||
@@ -60,7 +60,7 @@ export const RepoComponents: RequiredResourceComponents = {
|
||||
<HoverCardTrigger asChild>
|
||||
<Card className="px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer">
|
||||
<div className="text-muted-foreground text-sm text-nowrap overflow-hidden overflow-ellipsis">
|
||||
{info.latest_hash}
|
||||
latest commit: {info.latest_hash}
|
||||
</div>
|
||||
</Card>
|
||||
</HoverCardTrigger>
|
||||
|
||||
@@ -1,125 +1,24 @@
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { RequiredResourceComponents } from "@types";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@ui/card";
|
||||
import { FolderSync } from "lucide-react";
|
||||
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
|
||||
import { Clock, 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";
|
||||
import { ExecuteSync, RefreshSync } from "./actions";
|
||||
import { sanitizeOnlySpan, sync_no_changes } from "@lib/utils";
|
||||
import { Section } from "@components/layouts";
|
||||
import { PendingOrConfig } from "./pending-or-config";
|
||||
import {
|
||||
bg_color_class_by_intention,
|
||||
resource_sync_state_intention,
|
||||
} from "@lib/color";
|
||||
import { cn } from "@lib/utils";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@ui/hover-card";
|
||||
import { fmt_date } from "@lib/formatting";
|
||||
|
||||
const useResourceSync = (id?: string) =>
|
||||
useRead("ListResourceSyncs", {}).data?.find((d) => d.id === id);
|
||||
|
||||
const PENDING_TYPE_KEYS: Array<[string, string]> = [
|
||||
["Server", "server_updates"],
|
||||
["Deployment", "deployment_updates"],
|
||||
["Build", "build_updates"],
|
||||
["Repo", "repo_updates"],
|
||||
["Procedure", "procedure_updates"],
|
||||
["Alerter", "alerter_updates"],
|
||||
["Builder", "builder_updates"],
|
||||
["Server Template", "server_template_updates"],
|
||||
["Resource Sync", "resource_sync_updates"],
|
||||
["Variable", "variable_updates"],
|
||||
["User Group", "user_group_updates"],
|
||||
];
|
||||
|
||||
const PendingOrConfig = ({ id }: { id: string }) => {
|
||||
const [view, setView] = useState("Pending");
|
||||
|
||||
const sync = useRead("GetResourceSync", { sync: id }).data;
|
||||
|
||||
const pendingDisabled = !sync || sync_no_changes(sync);
|
||||
const currentView = view === "Pending" && pendingDisabled ? "Config" : view;
|
||||
|
||||
const tabsList = (
|
||||
<TabsList className="justify-start w-fit">
|
||||
<TabsTrigger
|
||||
value="Pending"
|
||||
className="w-[110px]"
|
||||
disabled={pendingDisabled}
|
||||
>
|
||||
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>
|
||||
<TabsContent value="Pending">
|
||||
<Section titleOther={tabsList}>
|
||||
{PENDING_TYPE_KEYS.map(([type, key]) => (
|
||||
<PendingView
|
||||
key={type}
|
||||
type={type}
|
||||
pending={sync?.info?.pending?.[key]}
|
||||
/>
|
||||
))}
|
||||
</Section>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
const PendingView = ({
|
||||
type,
|
||||
pending,
|
||||
}: {
|
||||
type: string;
|
||||
pending: Types.SyncUpdate | undefined;
|
||||
}) => {
|
||||
if (!pending) return;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex items-center justify-between gap-4">
|
||||
<CardTitle>{type} Updates</CardTitle>
|
||||
<div className="flex gap-4 items-center">
|
||||
{pending.to_create && (
|
||||
<div className="flex gap-2 items-center">
|
||||
To Create: {pending.to_create}
|
||||
</div>
|
||||
)}
|
||||
{pending.to_update && (
|
||||
<div className="flex gap-2 items-center">
|
||||
To Update: {pending.to_update}
|
||||
</div>
|
||||
)}
|
||||
{pending.to_delete && (
|
||||
<div className="flex gap-2 items-center">
|
||||
To Delete: {pending.to_delete}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeOnlySpan(pending.log),
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const ResourceSyncComponents: RequiredResourceComponents = {
|
||||
list_item: (id) => useResourceSync(id),
|
||||
|
||||
@@ -151,9 +50,55 @@ export const ResourceSyncComponents: RequiredResourceComponents = {
|
||||
Icon: () => <FolderSync className="w-4 h-4" />,
|
||||
BigIcon: () => <FolderSync className="w-8 h-8" />,
|
||||
|
||||
Status: {},
|
||||
Status: {
|
||||
State: ({ id }) => {
|
||||
const state = useResourceSync(id)?.info.state;
|
||||
const color = bg_color_class_by_intention(
|
||||
resource_sync_state_intention(state)
|
||||
);
|
||||
return (
|
||||
<Card className={cn("w-fit", color)}>
|
||||
<CardHeader className="py-0 px-2">{state}</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
Status: ({ id }) => {
|
||||
const info = useResourceSync(id)?.info;
|
||||
if (info?.last_sync_hash && info?.last_sync_message) {
|
||||
return (
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<Card className="px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer">
|
||||
<div className="text-muted-foreground text-sm text-nowrap overflow-hidden overflow-ellipsis">
|
||||
last sync: {info.last_sync_hash}
|
||||
</div>
|
||||
</Card>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent align="start">
|
||||
<div className="grid">
|
||||
<div className="text-muted-foreground">commit message:</div>
|
||||
{info.last_sync_message}
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
} else {
|
||||
return <div className="text-muted-foreground">{"Never synced"}</div>;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Info: {},
|
||||
Info: {
|
||||
LastSync: ({ id }) => {
|
||||
const last_ts = useResourceSync(id)?.info.last_sync_ts;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
{last_ts ? fmt_date(new Date(last_ts)) : "Never"}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
Actions: { RefreshSync, ExecuteSync },
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { sanitizeOnlySpan, sync_no_changes } from "@lib/utils";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
|
||||
import { useState } from "react";
|
||||
import { ResourceSyncConfig } from "./config";
|
||||
import { Section } from "@components/layouts";
|
||||
import { Types } from "@monitor/client";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@ui/card";
|
||||
|
||||
const PENDING_TYPE_KEYS: Array<[string, string]> = [
|
||||
["Server", "server_updates"],
|
||||
["Deployment", "deployment_updates"],
|
||||
["Build", "build_updates"],
|
||||
["Repo", "repo_updates"],
|
||||
["Procedure", "procedure_updates"],
|
||||
["Alerter", "alerter_updates"],
|
||||
["Builder", "builder_updates"],
|
||||
["Server Template", "server_template_updates"],
|
||||
["Resource Sync", "resource_sync_updates"],
|
||||
["Variable", "variable_updates"],
|
||||
["User Group", "user_group_updates"],
|
||||
];
|
||||
|
||||
export const PendingOrConfig = ({ id }: { id: string }) => {
|
||||
const [view, setView] = useState("Pending");
|
||||
|
||||
const sync = useRead("GetResourceSync", { sync: id }).data;
|
||||
|
||||
const pendingDisabled = !sync || sync_no_changes(sync);
|
||||
const currentView = view === "Pending" && pendingDisabled ? "Config" : view;
|
||||
|
||||
const pending = sync?.info?.pending;
|
||||
|
||||
const tabsList = (
|
||||
<TabsList className="justify-start w-fit">
|
||||
<TabsTrigger
|
||||
value="Pending"
|
||||
className="w-[110px]"
|
||||
disabled={pendingDisabled}
|
||||
>
|
||||
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>
|
||||
<TabsContent value="Pending">
|
||||
<Section titleOther={tabsList}>
|
||||
{pending?.hash && pending.message && (
|
||||
<Card>
|
||||
<CardHeader className="flex items-center gap-4 text-muted-foreground">
|
||||
Commit: {pending.hash}: {pending.message}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)}
|
||||
{pending?.data.type === "Ok" &&
|
||||
PENDING_TYPE_KEYS.map(([type, key]) => (
|
||||
<PendingView
|
||||
key={type}
|
||||
type={type}
|
||||
pending={pending.data.data[key]}
|
||||
/>
|
||||
))}
|
||||
{pending?.data.type === "Err" && (
|
||||
<Card>
|
||||
<CardHeader className="flex items-center justify-between gap-4">
|
||||
<CardTitle>Pending Error</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeOnlySpan(pending.data.data.message),
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</Section>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
const PendingView = ({
|
||||
type,
|
||||
pending,
|
||||
}: {
|
||||
type: string;
|
||||
pending: Types.SyncUpdate | undefined;
|
||||
}) => {
|
||||
if (!pending) return;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center gap-4 px-8 py-4">
|
||||
<CardTitle>{type} Updates</CardTitle>
|
||||
<div className="flex gap-4 items-center m-0">
|
||||
{pending.to_create ? (
|
||||
<>
|
||||
|
|
||||
<div className="flex gap-2 items-center">
|
||||
To Create: {pending.to_create}
|
||||
</div>
|
||||
</>
|
||||
) : undefined}
|
||||
{pending.to_update ? (
|
||||
<>
|
||||
|
|
||||
<div className="flex gap-2 items-center">
|
||||
To Update: {pending.to_update}
|
||||
</div>
|
||||
</>
|
||||
) : undefined}
|
||||
{pending.to_delete ? (
|
||||
<>
|
||||
|
|
||||
<div className="flex gap-2 items-center">
|
||||
To Delete: {pending.to_delete}
|
||||
</div>
|
||||
</>
|
||||
) : undefined}
|
||||
</div>
|
||||
</div>
|
||||
<CardContent>
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeOnlySpan(pending.log),
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
import { Types } from "@monitor/client";
|
||||
import { ResourceSyncComponents } from ".";
|
||||
|
||||
export const ResourceSyncTable = ({
|
||||
syncs,
|
||||
@@ -22,19 +23,16 @@ export const ResourceSyncTable = ({
|
||||
<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} />,
|
||||
// },
|
||||
{
|
||||
accessorKey: "info.state",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ResourceSyncComponents.Status.State id={row.original.id} />
|
||||
),
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
header: "Tags",
|
||||
cell: ({ row }) => <TableTags tag_ids={row.original.tags} />,
|
||||
|
||||
@@ -171,6 +171,25 @@ export const procedure_state_intention = (status?: Types.ProcedureState) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const resource_sync_state_intention = (status?: Types.ResourceSyncState) => {
|
||||
switch (status) {
|
||||
case undefined:
|
||||
return "None";
|
||||
case Types.ResourceSyncState.Unknown:
|
||||
return "Unknown";
|
||||
case Types.ResourceSyncState.Ok:
|
||||
return "Good";
|
||||
case Types.ResourceSyncState.Syncing:
|
||||
return "Warning";
|
||||
case Types.ResourceSyncState.Pending:
|
||||
return "Warning";
|
||||
case Types.ResourceSyncState.Failed:
|
||||
return "Critical";
|
||||
default:
|
||||
return "None";
|
||||
}
|
||||
};
|
||||
|
||||
export const alert_level_intention: (
|
||||
level: Types.SeverityLevel
|
||||
) => ColorIntention = (level) => {
|
||||
|
||||
@@ -39,6 +39,8 @@ export const Resource = () => {
|
||||
);
|
||||
const canWrite = !ui_write_disabled && perms === Types.PermissionLevel.Write;
|
||||
|
||||
const infoEntries = Object.entries(Components.Info);
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={name}
|
||||
@@ -52,13 +54,14 @@ export const Resource = () => {
|
||||
}
|
||||
subtitle={
|
||||
<div className="flex gap-4 items-center text-muted-foreground">
|
||||
{Object.entries(Components.Info).map(([key, Info], i) => (
|
||||
{infoEntries.map(([key, Info], i) => (
|
||||
<Fragment key={key}>
|
||||
{i !== 0 && "| "}
|
||||
<Info key={i} id={id} />
|
||||
</Fragment>
|
||||
))}
|
||||
| <ExportButton targets={[{ type, id }]} />
|
||||
{infoEntries.length ? "| " : ""}
|
||||
<ExportButton targets={[{ type, id }]} />
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
|
||||
Reference in New Issue
Block a user