Skip to content

Commit 5402ece

Browse files
committed
chore: a mess of merging everything together
1 parent 8aebfec commit 5402ece

File tree

56 files changed

+757
-287
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+757
-287
lines changed

frontend/apps/hub/ARCHITECTURE.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Hub Architecture
2+
3+
## Overview
4+
5+
The Hub is a React/TypeScript dashboard application built with:
6+
- **TanStack Router** - File-based routing
7+
- **TanStack Query** - Server state management
8+
- **Jotai** - Complex client state
9+
- **React Context** - Global app state
10+
- **Tailwind CSS** - Styling
11+
12+
## Directory Structure
13+
14+
```
15+
src/
16+
├── domains/ # Feature domains
17+
│ └── {domain}/
18+
│ ├── components/ # Domain-specific components
19+
│ ├── queries/ # Query options & mutations
20+
│ ├── data/ # Context providers
21+
│ └── forms/ # Form schemas
22+
├── routes/ # Page components (file-based)
23+
├── components/ # Shared UI components
24+
├── queries/ # Global query setup
25+
├── hooks/ # Custom React hooks
26+
├── lib/ # Utilities
27+
└── app.tsx # Root setup
28+
```
29+
30+
## State Management
31+
32+
### Server State (TanStack Query)
33+
```typescript
34+
// Query pattern: src/domains/{domain}/queries/query-options.ts
35+
export const actorLogsQueryOptions = ({ projectNameId, environmentNameId, actorId }) =>
36+
queryOptions({
37+
queryKey: ["project", projectNameId, "environment", environmentNameId, "actor", actorId, "logs"],
38+
queryFn: async ({ signal }) => rivetClient.actors.logs.get(...),
39+
meta: { watch: true }, // Enable real-time updates
40+
});
41+
```
42+
43+
### Client State (Jotai)
44+
Used for complex, modular state (e.g., actor management):
45+
```typescript
46+
// Atoms for fine-grained reactivity
47+
export const currentActorIdAtom = atom<string | undefined>(undefined);
48+
export const actorsAtom = atom<Actor[]>([]);
49+
```
50+
51+
### Global State (React Context)
52+
```typescript
53+
// Context for app-wide concerns
54+
AuthContext // User authentication
55+
ProjectContext // Current project
56+
EnvironmentContext // Current environment
57+
```
58+
59+
## Query Patterns
60+
61+
### Standard Query
62+
```typescript
63+
const { data } = useQuery(projectQueryOptions(projectId));
64+
```
65+
66+
### Infinite Query
67+
```typescript
68+
const { data, fetchNextPage } = useInfiniteQuery(
69+
projectActorsQueryOptions({ projectNameId, environmentNameId })
70+
);
71+
```
72+
73+
### Mutations
74+
```typescript
75+
const mutation = useMutation({
76+
mutationFn: (data) => rivetClient.actors.destroy(data),
77+
onSuccess: () => queryClient.invalidateQueries(...)
78+
});
79+
```
80+
81+
### Real-time Updates
82+
Queries with `watchIndex` parameter automatically refetch when server data changes:
83+
```typescript
84+
queryFn: ({ meta }) => api.call({
85+
watchIndex: getMetaWatchIndex(meta)
86+
})
87+
```
88+
89+
## Key Conventions
90+
91+
### Query Keys
92+
Hierarchical structure matching API resources:
93+
```typescript
94+
["project", projectId, "environment", envId, "actor", actorId, "logs"]
95+
```
96+
97+
### File Naming
98+
- Query options: `query-options.ts`
99+
- Mutations: `mutations.ts`
100+
- Context: `{resource}-context.tsx`
101+
- Components: `{feature}-{component}.tsx`
102+
103+
### Import Aliases
104+
```typescript
105+
@/domains // Domain logic
106+
@/components // Shared components
107+
@/queries // Query utilities
108+
@/hooks // Custom hooks
109+
@/lib // Utilities
110+
```
111+
112+
## API Integration
113+
114+
- **rivetClient**: Main API client (`@rivet-gg/api-full`)
115+
- **rivetEeClient**: Enterprise API (`@rivet-gg/api-ee`)
116+
- Auto token refresh on expiration
117+
- Request deduplication & caching
118+
119+
## Example: Adding a New Feature
120+
121+
1. **Create query options**:
122+
```typescript
123+
// src/domains/project/queries/feature/query-options.ts
124+
export const featureQueryOptions = (id: string) => queryOptions({
125+
queryKey: ["feature", id],
126+
queryFn: () => rivetClient.feature.get(id),
127+
});
128+
```
129+
130+
2. **Create mutations**:
131+
```typescript
132+
// src/domains/project/queries/feature/mutations.ts
133+
export const useCreateFeatureMutation = () => useMutation({
134+
mutationFn: (data) => rivetClient.feature.create(data),
135+
});
136+
```
137+
138+
3. **Create route**:
139+
```typescript
140+
// src/routes/.../feature.tsx
141+
export const Route = createFileRoute('...')({
142+
component: FeaturePage,
143+
});
144+
```
145+
146+
4. **Use in component**:
147+
```typescript
148+
function FeaturePage() {
149+
const { data } = useQuery(featureQueryOptions(id));
150+
const mutation = useCreateFeatureMutation();
151+
// ...
152+
}
153+
```

frontend/apps/hub/src/domains/project/queries/actors/query-options.ts

Lines changed: 72 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,13 @@ export const actorLogsQueryOptions = (
225225
{
226226
project,
227227
environment,
228-
actorIdsJson: JSON.stringify([actorId]),
228+
queryJson: JSON.stringify({
229+
string_equal: {
230+
property: "actor_id",
231+
value: actorId,
232+
},
233+
}),
229234
watchIndex: getMetaWatchIndex(meta),
230-
stream: "all",
231235
},
232236
{ abortSignal },
233237
);
@@ -313,21 +317,21 @@ export const actorMetricsQueryOptions = (
313317
response.metricNames.forEach((metricName, index) => {
314318
const metricValues = response.metricValues[index];
315319
const attributes = response.metricAttributes[index] || {};
316-
320+
317321
// Create the metric key based on the metric name and attributes
318322
let metricKey = metricName;
319-
323+
320324
// Handle specific attribute mappings to match UI expectations
321325
if (attributes.failure_type && attributes.scope) {
322326
metricKey = `memory_failures_${attributes.failure_type}_${attributes.scope}`;
323327
} else if (attributes.tcp_state) {
324-
if (metricName.includes('tcp6')) {
328+
if (metricName.includes("tcp6")) {
325329
metricKey = `network_tcp6_usage_${attributes.tcp_state}`;
326330
} else {
327331
metricKey = `network_tcp_usage_${attributes.tcp_state}`;
328332
}
329333
} else if (attributes.udp_state) {
330-
if (metricName.includes('udp6')) {
334+
if (metricName.includes("udp6")) {
331335
metricKey = `network_udp6_usage_${attributes.udp_state}`;
332336
} else {
333337
metricKey = `network_udp_usage_${attributes.udp_state}`;
@@ -336,20 +340,26 @@ export const actorMetricsQueryOptions = (
336340
metricKey = `tasks_state_${attributes.state}`;
337341
} else if (attributes.interface) {
338342
// Handle network interface attributes
339-
const baseMetric = metricName.replace(/^container_/, '');
343+
const baseMetric = metricName.replace(
344+
/^container_/,
345+
"",
346+
);
340347
metricKey = `${baseMetric}_${attributes.interface}`;
341348
} else if (attributes.device) {
342349
// Handle filesystem device attributes
343-
const baseMetric = metricName.replace(/^container_/, '');
350+
const baseMetric = metricName.replace(
351+
/^container_/,
352+
"",
353+
);
344354
metricKey = `${baseMetric}_${attributes.device}`;
345355
} else {
346356
// Remove "container_" prefix to match UI expectations
347-
metricKey = metricName.replace(/^container_/, '');
357+
metricKey = metricName.replace(/^container_/, "");
348358
}
349-
359+
350360
// Store raw time series data for rate calculations
351361
rawData[metricKey] = metricValues || [];
352-
362+
353363
if (metricValues && metricValues.length > 0) {
354364
// Get the latest non-zero value (last value is often 0)
355365
let value = null;
@@ -684,36 +694,67 @@ export const logsAggregatedQueryOptions = ({
684694
client,
685695
queryKey: [_, project, __, environment, ___, search],
686696
}) => {
687-
const actors = await client.fetchInfiniteQuery({
688-
...projectActorsQueryOptions({
689-
projectNameId: project,
690-
environmentNameId: environment,
691-
includeDestroyed: true,
692-
tags: {},
693-
}),
694-
pages: 10,
695-
});
696-
697-
const allActors = actors.pages.flatMap((page) => page.actors || []);
698-
699-
const actorsMap = new Map<string, Rivet.actors.Actor>();
700-
for (const actor of allActors) {
701-
actorsMap.set(actor.id, actor);
697+
let query = {};
698+
if (search?.text) {
699+
if (search.enableRegex) {
700+
query = {
701+
string_match_regex: {
702+
property: "message",
703+
pattern: search.enableRegex,
704+
case_insensitive: !search.enableRegex,
705+
},
706+
};
707+
} else {
708+
query = {
709+
string_contains: {
710+
property: "message",
711+
pattern: search.enableRegex,
712+
case_insensitive: !search.enableRegex,
713+
},
714+
};
715+
}
702716
}
703717

704718
const logs = await rivetClient.actors.logs.get(
705719
{
706-
stream: "all",
707720
project,
708721
environment,
709-
searchText: search?.text,
710-
searchCaseSensitive: search?.caseSensitive,
711-
searchEnableRegex: search?.enableRegex,
712-
actorIdsJson: JSON.stringify(allActors.map((a) => a.id)),
722+
queryJson: query ? JSON.stringify(query) : undefined,
713723
},
714724
{ abortSignal },
715725
);
716726

727+
// Fetch all actors that appear in the logs
728+
const actorsMap = new Map<string, Rivet.actors.Actor>();
729+
730+
// Get unique actor IDs from logs
731+
const uniqueActorIds = [...new Set(logs.actorIds)];
732+
733+
// Fetch actor details in parallel
734+
const actorPromises = uniqueActorIds.map(async (actorId) => {
735+
try {
736+
const response = await rivetClient.actors.get(
737+
actorId,
738+
{ project, environment },
739+
{ abortSignal }
740+
);
741+
return response.actor;
742+
} catch (error) {
743+
// If actor not found or error, return null
744+
console.warn(`Failed to fetch actor ${actorId}:`, error);
745+
return null;
746+
}
747+
});
748+
749+
const actors = await Promise.all(actorPromises);
750+
751+
// Populate the actors map
752+
for (const actor of actors) {
753+
if (actor) {
754+
actorsMap.set(actor.id, actor);
755+
}
756+
}
757+
717758
const parsed = logs.lines.map((line, idx) => {
718759
const actorIdx = logs.actorIndices[idx];
719760
const actorId = logs.actorIds[actorIdx];

packages/common/clickhouse-user-query/examples/case_sensitivity_demo.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ fn main() {
1717
property: "username".to_string(),
1818
map_key: None,
1919
value: "JohnDoe".to_string(),
20-
case_sensitive: true,
20+
case_insensitive: false,
2121
};
22-
let builder1 = UserDefinedQueryBuilder::new(&schema, &query1).unwrap();
22+
let builder1 = UserDefinedQueryBuilder::new(&schema, Some(&query1)).unwrap();
2323
println!(" Query: {}", builder1.where_expr());
2424
println!(" -> Will match: 'JohnDoe'");
2525
println!(" -> Won't match: 'johndoe', 'JOHNDOE'\n");
@@ -30,9 +30,9 @@ fn main() {
3030
property: "username".to_string(),
3131
map_key: None,
3232
value: "JohnDoe".to_string(),
33-
case_sensitive: false,
33+
case_insensitive: true,
3434
};
35-
let builder2 = UserDefinedQueryBuilder::new(&schema, &query2).unwrap();
35+
let builder2 = UserDefinedQueryBuilder::new(&schema, Some(&query2)).unwrap();
3636
println!(" Query: {}", builder2.where_expr());
3737
println!(" -> Will match: 'JohnDoe', 'johndoe', 'JOHNDOE', 'jOhNdOe'\n");
3838

@@ -42,9 +42,9 @@ fn main() {
4242
property: "email".to_string(),
4343
map_key: None,
4444
pattern: "^[A-Z].*@example\\.com$".to_string(),
45-
case_sensitive: true,
45+
case_insensitive: false,
4646
};
47-
let builder3 = UserDefinedQueryBuilder::new(&schema, &query3).unwrap();
47+
let builder3 = UserDefinedQueryBuilder::new(&schema, Some(&query3)).unwrap();
4848
println!(" Query: {}", builder3.where_expr());
4949
println!(" Pattern: ^[A-Z].*@example\\.com$");
5050
println!(" -> Will match: '[email protected]'");
@@ -56,9 +56,9 @@ fn main() {
5656
property: "email".to_string(),
5757
map_key: None,
5858
pattern: "admin|support".to_string(),
59-
case_sensitive: false,
59+
case_insensitive: true,
6060
};
61-
let builder4 = UserDefinedQueryBuilder::new(&schema, &query4).unwrap();
61+
let builder4 = UserDefinedQueryBuilder::new(&schema, Some(&query4)).unwrap();
6262
println!(" Query: {}", builder4.where_expr());
6363
println!(" Pattern: admin|support (with (?i) prefix)");
6464
println!(" -> Will match: '[email protected]', '[email protected]', '[email protected]'\n");
@@ -69,9 +69,9 @@ fn main() {
6969
property: "username".to_string(),
7070
map_key: None,
7171
values: vec!["Admin".to_string(), "Support".to_string()],
72-
case_sensitive: false,
72+
case_insensitive: true,
7373
};
74-
let builder5 = UserDefinedQueryBuilder::new(&schema, &query5).unwrap();
74+
let builder5 = UserDefinedQueryBuilder::new(&schema, Some(&query5)).unwrap();
7575
println!(" Query: {}", builder5.where_expr());
7676
println!(" -> Will match: 'admin', 'ADMIN', 'support', 'SUPPORT'\n");
7777

@@ -83,17 +83,17 @@ fn main() {
8383
property: "username".to_string(),
8484
map_key: None,
8585
value: "Admin".to_string(),
86-
case_sensitive: false, // Case-insensitive username
86+
case_insensitive: true, // Case-insensitive username
8787
},
8888
QueryExpr::StringMatchRegex {
8989
property: "tags".to_string(),
9090
map_key: Some("role".to_string()),
9191
pattern: "^(Admin|Manager)$".to_string(),
92-
case_sensitive: true, // Case-sensitive role
92+
case_insensitive: false, // Case-sensitive role
9393
},
9494
],
9595
};
96-
let builder6 = UserDefinedQueryBuilder::new(&schema, &query6).unwrap();
96+
let builder6 = UserDefinedQueryBuilder::new(&schema, Some(&query6)).unwrap();
9797
println!(" Query: {}", builder6.where_expr());
9898
println!(" -> Username matches 'admin' (any case)");
9999
println!(" -> Role must be exactly 'Admin' or 'Manager' (case-sensitive)");

0 commit comments

Comments
 (0)