resource query tags match on name or id, All or Any mode

This commit is contained in:
mbecker20
2024-03-30 01:55:30 -07:00
parent 7554055767
commit 74a5f429e9
21 changed files with 228 additions and 333 deletions

View File

@@ -7,12 +7,11 @@ use monitor_client::{
entities::{
alerter::{Alerter, AlerterListItem},
permission::PermissionLevel,
resource::AddFilters,
update::ResourceTargetVariant,
user::User,
},
};
use mungos::mongodb::bson::{doc, oid::ObjectId, Document};
use mungos::mongodb::bson::{doc, oid::ObjectId};
use resolver_api::Resolve;
use crate::{
@@ -46,9 +45,7 @@ impl Resolve<ListAlerters, User> for State {
ListAlerters { query }: ListAlerters,
user: User,
) -> anyhow::Result<Vec<AlerterListItem>> {
let mut filters = Document::new();
query.add_filters(&mut filters);
Alerter::list_resources_for_user(filters, &user).await
Alerter::list_resources_for_user(query, &user).await
}
}

View File

@@ -9,7 +9,6 @@ use monitor_client::{
entities::{
build::{Build, BuildActionState, BuildListItem},
permission::PermissionLevel,
resource::AddFilters,
update::{ResourceTargetVariant, UpdateStatus},
user::User,
Operation,
@@ -18,7 +17,7 @@ use monitor_client::{
use mungos::{
find::find_collect,
mongodb::{
bson::{doc, oid::ObjectId, Document},
bson::{doc, oid::ObjectId},
options::FindOptions,
},
};
@@ -56,9 +55,7 @@ impl Resolve<ListBuilds, User> for State {
ListBuilds { query }: ListBuilds,
user: User,
) -> anyhow::Result<Vec<BuildListItem>> {
let mut filters = Document::new();
query.add_filters(&mut filters);
Build::list_resources_for_user(filters, &user).await
Build::list_resources_for_user(query, &user).await
}
}

View File

@@ -7,12 +7,11 @@ use monitor_client::{
entities::{
builder::{Builder, BuilderConfig, BuilderListItem},
permission::PermissionLevel,
resource::AddFilters,
update::ResourceTargetVariant,
user::User,
},
};
use mungos::mongodb::bson::{doc, oid::ObjectId, Document};
use mungos::mongodb::bson::{doc, oid::ObjectId};
use resolver_api::Resolve;
use crate::{
@@ -46,9 +45,7 @@ impl Resolve<ListBuilders, User> for State {
ListBuilders { query }: ListBuilders,
user: User,
) -> anyhow::Result<Vec<BuilderListItem>> {
let mut filters = Document::new();
query.add_filters(&mut filters);
Builder::list_resources_for_user(filters, &user).await
Builder::list_resources_for_user(query, &user).await
}
}

View File

@@ -11,7 +11,6 @@ use monitor_client::{
DockerContainerStats,
},
permission::PermissionLevel,
resource::AddFilters,
server::Server,
update::{Log, ResourceTargetVariant, UpdateStatus},
user::User,
@@ -21,7 +20,7 @@ use monitor_client::{
use mungos::{
find::find_collect,
mongodb::{
bson::{doc, oid::ObjectId, Document},
bson::{doc, oid::ObjectId},
options::FindOneOptions,
},
};
@@ -61,9 +60,7 @@ impl Resolve<ListDeployments, User> for State {
ListDeployments { query }: ListDeployments,
user: User,
) -> anyhow::Result<Vec<DeploymentListItem>> {
let mut filters = Document::new();
query.add_filters(&mut filters);
Deployment::list_resources_for_user(filters, &user).await
Deployment::list_resources_for_user(query, &user).await
}
}

View File

@@ -48,7 +48,6 @@ enum ReadRequest {
GetProcedure(GetProcedure),
GetProcedureActionState(GetProcedureActionState),
ListProcedures(ListProcedures),
ListProceduresByIds(ListProceduresByIds),
// ==== SERVER ====
GetServersSummary(GetServersSummary),

View File

@@ -7,15 +7,14 @@ use monitor_client::{
GetProcedure, GetProcedureActionState,
GetProcedureActionStateResponse, GetProcedureResponse,
GetProceduresSummary, GetProceduresSummaryResponse,
ListProcedures, ListProceduresByIds, ListProceduresByIdsResponse,
ListProceduresResponse,
ListProcedures, ListProceduresResponse,
},
entities::{
permission::PermissionLevel, procedure::Procedure,
resource::AddFilters, update::ResourceTargetVariant, user::User,
update::ResourceTargetVariant, user::User,
},
};
use mungos::mongodb::bson::{doc, oid::ObjectId, Document};
use mungos::mongodb::bson::{doc, oid::ObjectId};
use resolver_api::Resolve;
use crate::{
@@ -49,24 +48,7 @@ impl Resolve<ListProcedures, User> for State {
ListProcedures { query }: ListProcedures,
user: User,
) -> anyhow::Result<ListProceduresResponse> {
let mut filters = Document::new();
query.add_filters(&mut filters);
Procedure::list_resources_for_user(filters, &user).await
}
}
#[async_trait]
impl Resolve<ListProceduresByIds, User> for State {
async fn resolve(
&self,
ListProceduresByIds { ids }: ListProceduresByIds,
user: User,
) -> anyhow::Result<ListProceduresByIdsResponse> {
Procedure::list_resources_for_user(
doc! { "_id": { "$in": ids } },
&user,
)
.await
Procedure::list_resources_for_user(query, &user).await
}
}

View File

@@ -7,12 +7,11 @@ use monitor_client::{
entities::{
permission::PermissionLevel,
repo::{Repo, RepoActionState, RepoListItem},
resource::AddFilters,
update::ResourceTargetVariant,
user::User,
},
};
use mungos::mongodb::bson::{doc, oid::ObjectId, Document};
use mungos::mongodb::bson::{doc, oid::ObjectId};
use resolver_api::Resolve;
use crate::{
@@ -46,9 +45,7 @@ impl Resolve<ListRepos, User> for State {
ListRepos { query }: ListRepos,
user: User,
) -> anyhow::Result<Vec<RepoListItem>> {
let mut filters = Document::new();
query.add_filters(&mut filters);
Repo::list_resources_for_user(filters, &user).await
Repo::list_resources_for_user(query, &user).await
}
}

View File

@@ -2,7 +2,7 @@ use async_trait::async_trait;
use monitor_client::{
api::read::{FindResources, FindResourcesResponse},
entities::{
build, deployment, repo, server,
build, deployment, procedure, repo, server,
update::ResourceTargetVariant::{self, *},
user::User,
},
@@ -33,7 +33,7 @@ impl Resolve<FindResources, User> for State {
for resource_type in resource_types {
match resource_type {
Server => {
res.servers = server::Server::list_resources_for_user(
res.servers = server::Server::query_resources_for_user(
query.clone(),
&user,
)
@@ -41,216 +41,37 @@ impl Resolve<FindResources, User> for State {
}
Deployment => {
res.deployments =
deployment::Deployment::list_resources_for_user(
deployment::Deployment::query_resources_for_user(
query.clone(),
&user,
)
.await?;
}
Build => {
res.builds = build::Build::list_resources_for_user(
res.builds = build::Build::query_resources_for_user(
query.clone(),
&user,
)
.await?;
}
Repo => {
res.repos =
repo::Repo::list_resources_for_user(query.clone(), &user)
.await?;
res.repos = repo::Repo::query_resources_for_user(
query.clone(),
&user,
)
.await?;
}
_ => unreachable!(),
Procedure => {
res.procedures =
procedure::Procedure::query_resources_for_user(
query.clone(),
&user,
)
.await?;
}
_ => {}
}
}
todo!()
Ok(res)
}
}
// #[async_trait]
// impl Resolve<FindResources, User> for State {
// async fn resolve(
// &self,
// FindResources { search, tags }: FindResources,
// user: User,
// ) -> anyhow::Result<FindResourcesResponse> {
// let SeperateTags {
// resource_types,
// server_ids,
// custom_tag_ids,
// } = seperate_tags(tags);
// let mut query = doc! {
// "name": { "$regex": search }
// };
// if !user.admin {
// query.insert(
// format!("permissions.{}", user.id),
// doc! { "$in": ["read", "execute", "update"] },
// );
// }
// if !custom_tag_ids.is_empty() {
// query.insert("tags", doc! { "$all": custom_tag_ids });
// }
// let mut response = FindResourcesResponse::default();
// for resource_type in resource_types {
// match resource_type {
// Server => {
// let servers = if server_ids.is_empty() {
// db_client().await.servers.get_some(query.clone(), None).await?
// } else {
// let server_ids = server_ids
// .iter()
// .map(|id| {
// ObjectId::from_str(id)
// .context("failed to parse server id as ObjectId")
// })
// .collect::<anyhow::Result<Vec<_>>>()?;
// let mut query = query.clone();
// query.insert("_id", doc! { "$in": server_ids });
// db_client().await.servers.get_some(query, None).await?
// };
// for server in servers {
// let status = self
// .server_status_cache
// .get(&server.id)
// .await
// .map(|s| s.status)
// .unwrap_or_default();
// let item = ServerListItem {
// status,
// id: server.id,
// name: server.name,
// tags: server.tags,
// };
// response.servers.push(item);
// }
// }
// Deployment => {
// let mut query = query.clone();
// if !server_ids.is_empty() {
// query.insert("config.server_id", doc! { "$in": &server_ids });
// }
// let deployments = self
// .db
// .deployments
// .get_some(query, None)
// .await?
// .into_iter()
// .filter(|d| d.get_user_permissions(&user.id) > PermissionLevel::Read);
// for deployment in deployments {
// let status = self.deployment_status_cache.get(&deployment.id).await;
// let item = DeploymentListItem {
// id: deployment.id,
// name: deployment.name,
// tags: deployment.tags,
// state: status.as_ref().map(|s| s.state).unwrap_or_default(),
// status: status.as_ref().and_then(|s| {
// s.container.as_ref().and_then(|c| c.status.to_owned())
// }),
// image: String::new(),
// server_id: String::new(),
// build_id: None,
// };
// response.deployments.push(item);
// }
// }
// Build => {
// let mut query = query.clone();
// if !server_ids.is_empty() {
// query.insert(
// "config.builder.params.server_id",
// doc! { "$in": &server_ids },
// );
// }
// let builds = self
// .db
// .builds
// .get_some(query, None)
// .await?
// .into_iter()
// .filter(|d| d.get_user_permissions(&user.id) > PermissionLevel::Read);
// for build in builds {
// let item = BuildListItem {
// id: build.id,
// name: build.name,
// tags: build.tags,
// last_built_at: build.last_built_at,
// version: build.config.version,
// };
// response.builds.push(item);
// }
// }
// Repo => {
// let mut query = query.clone();
// if !server_ids.is_empty() {
// query.insert("config.server_id", doc! { "$in": &server_ids });
// }
// let repos = self
// .db
// .repos
// .get_some(query, None)
// .await?
// .into_iter()
// .filter(|d| d.get_user_permissions(&user.id) > PermissionLevel::Read);
// for repo in repos {
// let item = RepoListItem {
// id: repo.id,
// name: repo.name,
// tags: repo.tags,
// last_pulled_at: repo.last_pulled_at,
// };
// response.repos.push(item);
// }
// }
// _ => return Err(anyhow!("{resource_type} is not compatible with this route")),
// }
// }
// Ok(response)
// }
// }
// #[derive(Default)]
// struct SeperateTags {
// resource_types: Vec<ResourceTargetVariant>,
// server_ids: Vec<String>,
// custom_tag_ids: Vec<String>,
// }
// fn seperate_tags(tags: Vec<Tag>) -> SeperateTags {
// let mut seperated = SeperateTags::default();
// for tag in tags {
// match tag {
// Tag::Custom { tag_id } => seperated.custom_tag_ids.push(tag_id),
// Tag::Server { server_id } => seperated.server_ids.push(server_id),
// Tag::ResourceType { resource } => {
// if !matches!(resource, Builder | Alerter | System,)
// && !seperated.resource_types.contains(&resource)
// {
// seperated.resource_types.push(resource);
// }
// }
// }
// }
// if seperated.resource_types.is_empty() {
// seperated.resource_types = FIND_RESOURCE_TYPES.to_vec();
// }
// seperated
// }

View File

@@ -6,7 +6,6 @@ use monitor_client::{
entities::{
deployment::ContainerSummary,
permission::PermissionLevel,
resource::AddFilters,
server::{
docker_image::ImageSummary, docker_network::DockerNetwork,
stats::SystemInformation, Server, ServerActionState,
@@ -17,10 +16,7 @@ use monitor_client::{
};
use mungos::{
find::find_collect,
mongodb::{
bson::{doc, Document},
options::FindOptions,
},
mongodb::{bson::doc, options::FindOptions},
};
use periphery_client::api::{self, GetAccountsResponse};
use resolver_api::{Resolve, ResolveToString};
@@ -42,7 +38,8 @@ impl Resolve<GetServersSummary, User> for State {
user: User,
) -> anyhow::Result<GetServersSummaryResponse> {
let servers =
Server::list_resources_for_user(Document::new(), &user).await?;
Server::list_resources_for_user(Default::default(), &user)
.await?;
let mut res = GetServersSummaryResponse::default();
for server in servers {
res.total += 1;
@@ -107,9 +104,7 @@ impl Resolve<ListServers, User> for State {
ListServers { query }: ListServers,
user: User,
) -> anyhow::Result<Vec<ServerListItem>> {
let mut filters = Document::new();
query.add_filters(&mut filters);
Server::list_resources_for_user(filters, &user).await
Server::list_resources_for_user(query, &user).await
}
}

View File

@@ -7,27 +7,33 @@ use monitor_client::{
entities::{
alerter::{
Alerter, AlerterConfig, AlerterInfo, AlerterListItem,
AlerterListItemInfo,
AlerterListItemInfo, AlerterQuerySpecifics,
},
build::{
Build, BuildConfig, BuildInfo, BuildListItem, BuildListItemInfo,
Build, BuildConfig, BuildInfo, BuildListItem,
BuildListItemInfo, BuildQuerySpecifics,
},
builder::{
Builder, BuilderConfig, BuilderListItem, BuilderListItemInfo,
BuilderQuerySpecifics,
},
deployment::{
Deployment, DeploymentConfig, DeploymentImage,
DeploymentListItem, DeploymentListItemInfo,
DeploymentQuerySpecifics,
},
permission::PermissionLevel,
procedure::{
Procedure, ProcedureConfig, ProcedureListItem,
ProcedureListItemInfo,
ProcedureListItemInfo, ProcedureQuerySpecifics,
},
repo::{Repo, RepoConfig, RepoInfo, RepoListItem},
resource::Resource,
repo::{
Repo, RepoConfig, RepoInfo, RepoListItem, RepoQuerySpecifics,
},
resource::{AddFilters, Resource, ResourceQuery},
server::{
Server, ServerConfig, ServerListItem, ServerListItemInfo,
ServerQuerySpecifics,
},
update::{ResourceTarget, ResourceTargetVariant},
user::User,
@@ -65,6 +71,7 @@ pub trait StateResource {
+ Serialize
+ DeserializeOwned
+ 'static;
type QuerySpecifics: AddFilters + Default;
fn name() -> &'static str;
@@ -139,6 +146,16 @@ pub trait StateResource {
}
async fn list_resources_for_user(
mut query: ResourceQuery<Self::QuerySpecifics>,
user: &User,
) -> anyhow::Result<Vec<Self::ListItem>> {
validate_resource_query_tags(&mut query).await;
let mut filters = Document::new();
query.add_filters(&mut filters);
Self::query_resources_for_user(filters, user).await
}
async fn query_resources_for_user(
mut filters: Document,
user: &User,
) -> anyhow::Result<Vec<Self::ListItem>> {
@@ -258,6 +275,7 @@ impl StateResource for Server {
type ListItem = ServerListItem;
type Config = ServerConfig;
type Info = ();
type QuerySpecifics = ServerQuerySpecifics;
fn name() -> &'static str {
"server"
@@ -302,6 +320,7 @@ impl StateResource for Deployment {
type ListItem = DeploymentListItem;
type Config = DeploymentConfig;
type Info = ();
type QuerySpecifics = DeploymentQuerySpecifics;
fn name() -> &'static str {
"deployment"
@@ -359,6 +378,7 @@ impl StateResource for Build {
type ListItem = BuildListItem;
type Config = BuildConfig;
type Info = BuildInfo;
type QuerySpecifics = BuildQuerySpecifics;
fn name() -> &'static str {
"build"
@@ -395,6 +415,7 @@ impl StateResource for Repo {
type ListItem = RepoListItem;
type Config = RepoConfig;
type Info = RepoInfo;
type QuerySpecifics = RepoQuerySpecifics;
fn name() -> &'static str {
"repo"
@@ -428,6 +449,7 @@ impl StateResource for Builder {
type ListItem = BuilderListItem;
type Config = BuilderConfig;
type Info = ();
type QuerySpecifics = BuilderQuerySpecifics;
fn name() -> &'static str {
"builder"
@@ -473,6 +495,7 @@ impl StateResource for Alerter {
type ListItem = AlerterListItem;
type Config = AlerterConfig;
type Info = AlerterInfo;
type QuerySpecifics = AlerterQuerySpecifics;
fn name() -> &'static str {
"alerter"
@@ -513,6 +536,7 @@ impl StateResource for Procedure {
type ListItem = ProcedureListItem;
type Config = ProcedureConfig;
type Info = ();
type QuerySpecifics = ProcedureQuerySpecifics;
fn name() -> &'static str {
"procedure"
@@ -605,3 +629,11 @@ pub async fn get_resource_ids_for_non_admin(
.collect();
Ok(permissions)
}
pub async fn validate_resource_query_tags<T: Default>(
query: &mut ResourceQuery<T>,
) {
let futures = query.tags.iter().map(|tag| get_tag(tag));
let res = join_all(futures).await;
query.tags = res.into_iter().flatten().map(|tag| tag.id).collect();
}

View File

@@ -5,10 +5,10 @@ use std::collections::HashMap;
use anyhow::Context;
use monitor_client::entities::{
resource::ResourceQuery,
server::{Server, ServerListItem},
user::User,
};
use mungos::mongodb::bson::Document;
use crate::helpers::resource::StateResource;
@@ -34,7 +34,7 @@ async fn get_all_servers_map() -> anyhow::Result<(
HashMap<String, String>,
)> {
let servers = Server::list_resources_for_user(
Document::new(),
ResourceQuery::default(),
&User {
admin: true,
..Default::default()

View File

@@ -44,21 +44,6 @@ pub type ListProceduresResponse = Vec<ProcedureListItem>;
//
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(MonitorReadRequest)]
#[response(ListProceduresByIdsResponse)]
pub struct ListProceduresByIds {
pub ids: Vec<String>,
}
#[typeshare]
pub type ListProceduresByIdsResponse = Vec<ProcedureListItem>;
//
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,

View File

@@ -1,7 +1,10 @@
use std::str::FromStr;
use derive_builder::Builder;
use derive_default_builder::DefaultBuilder;
use mungos::mongodb::bson::{
doc, serde_helpers::hex_string_as_object_id, Document,
doc, oid::ObjectId, serde_helpers::hex_string_as_object_id,
Document,
};
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
@@ -63,15 +66,29 @@ pub struct ResourceListItem<Info> {
Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder,
)]
pub struct ResourceQuery<T: Default> {
#[serde(default)]
pub ids: Vec<String>,
#[serde(default)]
pub names: Vec<String>,
/// Pass Vec of tag ids
/// Pass Vec of tag ids or tag names
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub tag_behavior: TagBehavior,
#[serde(default)]
pub specific: T,
}
#[typeshare]
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub enum TagBehavior {
/// Returns resources which have strictly all the tags
#[default]
All,
/// Returns resources which have one or more of the tags
Any,
}
pub trait AddFilters {
fn add_filters(&self, _filters: &mut Document) {}
}
@@ -80,11 +97,31 @@ impl AddFilters for () {}
impl<T: AddFilters + Default> AddFilters for ResourceQuery<T> {
fn add_filters(&self, filters: &mut Document) {
if !self.ids.is_empty() {
let ids = self
.ids
.iter()
.flat_map(|id| ObjectId::from_str(id))
.collect::<Vec<_>>();
filters.insert("_id", doc! { "$in": &ids });
}
if !self.names.is_empty() {
filters.insert("name", doc! { "$in": &self.names });
}
if !self.tags.is_empty() {
filters.insert("tags", doc! { "$all": &self.tags });
match self.tag_behavior {
TagBehavior::All => {
filters.insert("tags", doc! { "$all": &self.tags });
}
TagBehavior::Any => {
let ors = self
.tags
.iter()
.map(|tag| doc! { "tags": tag })
.collect::<Vec<_>>();
filters.insert("$or", ors);
}
}
}
self.specific.add_filters(filters);
}

View File

@@ -23,7 +23,6 @@ export type ReadResponses = {
GetProcedure: Types.GetProcedureResponse;
GetProcedureActionState: Types.GetProcedureActionStateResponse;
ListProcedures: Types.ListProceduresResponse;
ListProceduresByIds: Types.ListProceduresByIdsResponse;
// ==== SERVER ====
GetServersSummary: Types.GetServersSummaryResponse;

View File

@@ -332,8 +332,6 @@ export type ProcedureListItem = ResourceListItem<ProcedureListItemInfo>;
export type ListProceduresResponse = ProcedureListItem[];
export type ListProceduresByIdsResponse = ProcedureListItem[];
export interface ProcedureActionState {
running: boolean;
}
@@ -739,11 +737,20 @@ export type _PartialCustomAlerterConfig = Partial<CustomAlerterConfig>;
export type _PartialSlackAlerterConfig = Partial<SlackAlerterConfig>;
export enum TagBehavior {
/** Returns resources which have strictly all the tags */
All = "All",
/** Returns resources which have one or more of the tags */
Any = "Any",
}
/** Passing empty Vec is the same as not filtering by that field */
export interface ResourceQuery<T> {
ids?: string[];
names?: string[];
/** Pass Vec of tag ids */
/** Pass Vec of tag ids or tag names */
tags?: string[];
tag_behavior?: TagBehavior;
specific?: T;
}
@@ -1188,10 +1195,6 @@ export interface ListProcedures {
query?: ProcedureQuery;
}
export interface ListProceduresByIds {
ids: string[];
}
export interface GetProceduresSummary {
}
@@ -1705,7 +1708,7 @@ export interface SlackAlerterConfig {
}
export interface ServerBuilderConfig {
id: string;
server_id: string;
}
export interface AwsBuilderConfig {
@@ -1803,7 +1806,6 @@ export type ReadRequest =
| { type: "GetProcedure", params: GetProcedure }
| { type: "GetProcedureActionState", params: GetProcedureActionState }
| { type: "ListProcedures", params: ListProcedures }
| { type: "ListProceduresByIds", params: ListProceduresByIds }
| { type: "GetServersSummary", params: GetServersSummary }
| { type: "GetServer", params: GetServer }
| { type: "ListServers", params: ListServers }

View File

@@ -2,7 +2,7 @@ import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { RequiredResourceComponents } from "@types";
import { AlertTriangle, HardDrive, Rocket, Server } from "lucide-react";
import { cn } from "@lib/utils";
import { cn, snake_case_to_upper_space_case } from "@lib/utils";
import { useState } from "react";
import { NewResource, Section } from "@components/layouts";
@@ -22,6 +22,7 @@ import { DataTable } from "@ui/data-table";
import { ResourceComponents } from "..";
import { TagsWithBadge, useTagsFilter } from "@components/tags";
import { DeploymentsChart } from "@components/dashboard/deployments-chart";
import { Button } from "@ui/button";
export const useDeployment = (id?: string) =>
useRead("ListDeployments", {}, { refetchInterval: 5000 }).data?.find(
@@ -94,7 +95,6 @@ export const DeploymentTable = ({
}
columns={[
{
accessorKey: "id",
header: "Name",
cell: ({ row }) => {
const id = row.original.id;
@@ -103,49 +103,71 @@ export const DeploymentTable = ({
to={`/deployments/${id}`}
className="flex items-center gap-2"
>
<ResourceComponents.Deployment.Icon id={id} />
<ResourceComponents.Deployment.Name id={id} />
<Button variant="link" className="flex gap-2 items-center p-0">
<ResourceComponents.Deployment.Icon id={id} />
<ResourceComponents.Deployment.Name id={id} />
</Button>
</Link>
);
},
},
{
header: "Image",
cell: ({
row: {
original: {
info: { build_id, image },
},
},
}) => {
const builds = useRead("ListBuilds", {}).data;
if (build_id) {
const build = builds?.find((build) => build.id === build_id);
if (build) {
return (
<Link to={`/builds/${build_id}`}>
<Button
variant="link"
className="flex gap-2 items-center p-0"
>
<ResourceComponents.Build.Icon id={build_id} />
<ResourceComponents.Build.Name id={build_id} />
</Button>
</Link>
);
} else {
return undefined;
}
} else {
const [img, _] = image.split(":");
return img;
}
},
},
{
header: "Server",
cell: ({ row }) => {
const id = row.original.info.server_id;
return (
<Link to={`/servers/${id}`} className="flex items-center gap-2">
<ResourceComponents.Server.Icon id={id} />
<ResourceComponents.Server.Name id={id} />
<Link to={`/servers/${id}`}>
<Button variant="link" className="flex items-center gap-2 p-0">
<ResourceComponents.Server.Icon id={id} />
<ResourceComponents.Server.Name id={id} />
</Button>
</Link>
);
},
},
// {
// header: "Build",
// cell: ({ row }) => {
// const id = row.original.info.build_id;
// if (!id) return null;
// return (
// <Link to={`/builds/${id}`} className="flex items-center gap-2">
// <ResourceComponents.Build.Icon id={id} />
// <ResourceComponents.Build.Name id={id} />
// </Link>
// );
// },
// },
{
accessorKey: "info.image",
header: "Image",
},
{
header: "Status",
header: "State",
cell: ({ row }) => {
const status = row.original.info.status;
if (!status) return null;
const state = row.original.info.state;
const color = deployment_state_text_color(state);
return <div className={color}>{status}</div>;
return (
<div className={color}>
{snake_case_to_upper_space_case(state)}
</div>
);
},
},
{

View File

@@ -12,13 +12,14 @@ import {
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "@ui/dropdown-menu";
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
import { useToast } from "@ui/use-toast";
import { atom, useAtom } from "jotai";
import { PlusCircle, Tag } from "lucide-react";
import { useAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { MinusCircle, PlusCircle, Tag } from "lucide-react";
import { useEffect, useState } from "react";
type TargetExcludingSystem = Exclude<Types.ResourceTarget, { type: "System" }>;
const tagsAtom = atom<string[]>([]);
const tagsAtom = atomWithStorage<string[]>("tags-v0", []);
export const useTagsFilter = () => {
const [tags, _] = useAtom(tagsAtom);
@@ -29,9 +30,8 @@ export const TagsFilter = () => {
const [tags, setTags] = useAtom(tagsAtom);
const all_tags = useRead("ListTags", {}).data;
return (
<div className="flex gap-4">
<TagsWithBadge
className="cursor-pointer"
<div className="flex gap-4 items-center">
<TagsFilterTags
tag_ids={tags}
onBadgeClick={(tag_id) => setTags(tags.filter((id) => id !== tag_id))}
/>
@@ -61,6 +61,33 @@ export const TagsFilter = () => {
);
};
export const TagsFilterTags = ({
tag_ids,
onBadgeClick,
}: {
tag_ids?: string[];
onBadgeClick?: (tag_id: string) => void;
}) => {
const all_tags = useRead("ListTags", {}).data;
const get_name = (tag_id: string) =>
all_tags?.find((t) => t._id?.$oid === tag_id)?.name ?? "unknown";
return (
<>
{tag_ids?.map((tag_id) => (
<Badge
key={tag_id}
variant="destructive"
className="flex gap-1 px-2 py-1.5 cursor-pointer"
onClick={() => onBadgeClick && onBadgeClick(tag_id)}
>
{get_name(tag_id)}
<MinusCircle className="w-3 h-3" />
</Badge>
))}
</>
);
};
export const ResourceTags = ({
target,
click_to_delete,
@@ -101,6 +128,8 @@ export const TagsWithBadge = ({
className?: string;
}) => {
const all_tags = useRead("ListTags", {}).data;
const get_name = (tag_id: string) =>
all_tags?.find((t) => t._id?.$oid === tag_id)?.name ?? "unknown";
return (
<>
{tag_ids?.map((tag_id) => (
@@ -110,7 +139,7 @@ export const TagsWithBadge = ({
className={className ?? "px-1.5 py-0.5 cursor-pointer"}
onClick={() => onBadgeClick && onBadgeClick(tag_id)}
>
{all_tags?.find((t) => t._id?.$oid === tag_id)?.name ?? "unknown"}
{get_name(tag_id)}
</Badge>
))}
</>

View File

@@ -43,14 +43,14 @@
--popover: 220.1 0.1% 98.2%;
--popover-foreground: 220.1 40.1% 11.2%;
--primary: 220.1 0.1% 11.2%;
--primary: 220.1 20.1% 11.2%;
--primary-foreground: 220.1 40.1% 98.2%;
--secondary: 220.1 40.1% 98.2%;
--secondary-foreground: 220.1 40.1% 11.2%;
--secondary: 220.1 40.1% 90.2%;
--secondary-foreground: 220.1 40.1% 5.2%;
--muted: 220.1 40.1% 98.2%;
--muted-foreground: 215.4 20.1% 40.2%;
--muted-foreground: 220.1 20.1% 40.2%;
--accent: 220.1 40.1% 98.2%;
--accent-foreground: 220.1 40.1% 11.2%;

View File

@@ -130,4 +130,4 @@ export const usePushRecentlyViewed = ({ type, id }: Types.ResourceTarget) => {
}, [type, id, push]);
return push;
};
};

View File

@@ -6,7 +6,6 @@ import { Link } from "react-router-dom";
import { ServerComponents } from "@components/resources/server";
import { AlertLevel } from "@components/util";
import { fmt_date_with_minutes } from "@lib/utils";
import { useState } from "react";
import { Button } from "@ui/button";
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { DeploymentComponents } from "@components/resources/deployment";
@@ -17,6 +16,8 @@ import { AlerterComponents } from "@components/resources/alerter";
import { ProcedureComponents } from "@components/resources/procedure/index";
import { TagsSummary } from "@components/dashboard/tags";
import { ApiKeysSummary } from "@components/dashboard/api-keys";
import { atomWithStorage } from "jotai/utils";
import { useAtom } from "jotai";
export const Dashboard = () => {
return (
@@ -28,8 +29,10 @@ export const Dashboard = () => {
);
};
const openAtom = atomWithStorage("show-alerts-v0", true);
const OpenAlerts = () => {
const [open, setOpen] = useState(true);
const [open, setOpen] = useAtom(openAtom);
const alerts = useRead("ListAlerts", { query: { resolved: false } }).data
?.alerts;
if (!alerts || alerts.length === 0) return null;

View File

@@ -5,6 +5,10 @@ import { useRead, useResourceParamType } from "@lib/hooks";
import { Button } from "@ui/button";
import { Input } from "@ui/input";
import { useState } from "react";
import { atomWithStorage } from "jotai/utils";
import { useAtom } from "jotai";
const viewAtom = atomWithStorage<"cards" | "table">("list-show-as-v0", "table");
export const Resources = () => {
const type = useResourceParamType();
@@ -15,7 +19,7 @@ export const Resources = () => {
const list = useRead(`List${type}s`, { query: { tags } }).data;
const [search, set] = useState("");
const [view, setView] = useState<"cards" | "table">("table");
const [view, setView] = useAtom(viewAtom);
return (
<Page