add rybbit

This commit is contained in:
2025-12-20 22:23:35 -03:00
parent f0307d4603
commit 43e6973f88
17 changed files with 269 additions and 115 deletions

View File

@@ -1,7 +1,7 @@
from aws_lambda_powertools import Logger from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import NotFoundError from aws_lambda_powertools.event_handler.exceptions import NotFoundError
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, PartitionKey
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from config import ENROLLMENT_TABLE from config import ENROLLMENT_TABLE
@@ -11,6 +11,14 @@ router = Router()
dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
@router.get('/<org_id>/enrollments/submissions')
def submissions(org_id: str):
return dyn.collection.query(
PartitionKey(f'SUBMISSION#ORG#{org_id}'),
projection_expr='id, sk, created_by',
)
@router.get('/<org_id>/enrollments/<submission_id>/submitted') @router.get('/<org_id>/enrollments/<submission_id>/submitted')
def submitted(org_id: str, submission_id: str): def submitted(org_id: str, submission_id: str):
return dyn.collection.get_item( return dyn.collection.get_item(

View File

@@ -26,7 +26,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment: Environment:
Variables: Variables:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo

View File

@@ -667,7 +667,7 @@ wheels = [
[[package]] [[package]]
name = "layercake" name = "layercake"
version = "0.11.3" version = "0.11.4"
source = { directory = "../layercake" } source = { directory = "../layercake" }
dependencies = [ dependencies = [
{ name = "arnparse" }, { name = "arnparse" },

View File

@@ -0,0 +1,53 @@
interface Rybbit {
/**
* Tracks a page view
*/
pageview: () => void
/**
* Tracks a custom event
* @param name Name of the event
* @param properties Optional properties for the event
*/
event: (name: string, properties?: Record<string, any>) => void
/**
* Sets a custom user ID for tracking logged-in users
* @param userId The user ID to set (will be stored in localStorage)
* @param traits Optional user metadata (email, name, custom fields)
*/
identify: (userId: string, traits?: Record<string, unknown>) => void
/**
* Updates traits for the currently identified user
* @param traits User metadata to merge with existing traits
*/
setTraits: (traits: Record<string, unknown>) => void
/**
* Clears the stored user ID
*/
clearUserId: () => void
/**
* Gets the currently set user ID
* @returns The current user ID or null if not set
*/
getUserId: () => string | null
/**
* Manually tracks outbound link clicks
* @param url The URL of the outbound link
* @param text Optional text content of the link
* @param target Optional target attribute of the link
*/
trackOutbound: (url: string, text?: string, target?: string) => void
}
declare global {
interface Window {
rybbit: Rybbit
}
}
export {}

View File

@@ -208,7 +208,6 @@ function List({ items, search }) {
} }
return ( return (
<div className="border rounded-lg overflow-hidden">
<Table className="table-auto w-full"> <Table className="table-auto w-full">
{charges.length ? ( {charges.length ? (
<> <>
@@ -303,7 +302,6 @@ function List({ items, search }) {
</TableRow> </TableRow>
</TableFooter> </TableFooter>
</Table> </Table>
</div>
) )
} }

View File

@@ -1,6 +1,7 @@
import type { Route } from './+types/route' import type { Route } from './+types/route'
import { useToggle } from 'ahooks' import Fuse from 'fuse.js'
import { useRequest, useToggle } from 'ahooks'
import { ErrorMessage } from '@hookform/error-message' import { ErrorMessage } from '@hookform/error-message'
import { import {
CalendarIcon, CalendarIcon,
@@ -15,7 +16,8 @@ import {
ArrowDownAZIcon, ArrowDownAZIcon,
ArrowUpAZIcon, ArrowUpAZIcon,
AlertTriangleIcon, AlertTriangleIcon,
UserIcon UserIcon,
EllipsisIcon
} from 'lucide-react' } from 'lucide-react'
import { redirect, Link, useParams, useFetcher } from 'react-router' import { redirect, Link, useParams, useFetcher } from 'react-router'
import { Controller, useFieldArray, useForm } from 'react-hook-form' import { Controller, useFieldArray, useForm } from 'react-hook-form'
@@ -23,9 +25,10 @@ import { Fragment, use, useEffect, useMemo, useState } from 'react'
import { format } from 'date-fns' import { format } from 'date-fns'
import { ptBR } from 'react-day-picker/locale' import { ptBR } from 'react-day-picker/locale'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import Fuse from 'fuse.js'
import { formatCPF } from '@brazilian-utils/brazilian-utils' import { formatCPF } from '@brazilian-utils/brazilian-utils'
import { pick } from 'ramda'
import { DateTime } from '@repo/ui/components/datetime'
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar' import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
import { Abbr } from '@repo/ui/components/abbr' import { Abbr } from '@repo/ui/components/abbr'
import { import {
@@ -51,6 +54,7 @@ import {
} from '@repo/ui/components/ui/input-group' } from '@repo/ui/components/ui/input-group'
import { import {
Card, Card,
CardAction,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
@@ -75,7 +79,6 @@ import { cloudflareContext } from '@repo/auth/context'
import { SearchFilter } from '@repo/ui/components/search-filter' import { SearchFilter } from '@repo/ui/components/search-filter'
import { formSchema, type Schema, MAX_ITEMS } from './data' import { formSchema, type Schema, MAX_ITEMS } from './data'
import { pick } from 'ramda'
export function meta({}: Route.MetaArgs) { export function meta({}: Route.MetaArgs) {
return [{ title: 'Adicionar matrícula' }] return [{ title: 'Adicionar matrícula' }]
@@ -215,6 +218,9 @@ export default function Route({
<CardDescription> <CardDescription>
Siga os passos abaixo para adicionar novas matrículas. Siga os passos abaixo para adicionar novas matrículas.
</CardDescription> </CardDescription>
<CardAction>
<ActionMenu />
</CardAction>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
@@ -731,3 +737,72 @@ function DuplicateRowMultipleTimes({
</Popover> </Popover>
) )
} }
function ActionMenu() {
const { orgid } = useParams()
const [open, { set }] = useToggle()
const { data, runAsync, loading } = useRequest(
async () => {
const r = await fetch(`/~/api/orgs/${orgid}/enrollments/submissions`, {
method: 'GET'
})
return await r.json()
},
{ manual: true }
)
return (
<Popover
open={open}
onOpenChange={async (open) => {
set(open)
await runAsync()
}}
>
<PopoverTrigger asChild>
<Button variant="ghost" className="cursor-pointer">
<EllipsisIcon />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="p-0 overflow-hidden w-56">
<div className="border-b p-2 text-xs text-muted-foreground font-medium">
Envios recentes
</div>
<Command className="rounded-none">
<CommandList>
<CommandGroup>
{loading && (
<CommandItem disabled>
<Spinner />
</CommandItem>
)}
{data?.items?.map(({ sk }, index) => (
<CommandItem asChild key={index}>
<Link
to={`../enrollments/${sk}/submitted`}
className="cursor-pointer"
>
<DateTime
options={{
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}}
>
{sk}
</DateTime>
</Link>
</CommandItem>
))}
{data?.items?.length === 0 && (
<CommandEmpty>Nenhum envio ainda</CommandEmpty>
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}

View File

@@ -406,14 +406,16 @@ function Failed({ items = [] }) {
} }
function ActionMenu({ sk }: { sk: string }) { function ActionMenu({ sk }: { sk: string }) {
const [open, { toggle, set }] = useToggle()
const { revalidate } = useRevalidator() const { revalidate } = useRevalidator()
const onSuccess = () => { const onSuccess = () => {
revalidate() revalidate()
set(false)
} }
return ( return (
<DropdownMenu> <DropdownMenu open={open} onOpenChange={toggle}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon-sm" className="cursor-pointer"> <Button variant="ghost" size="icon-sm" className="cursor-pointer">
<EllipsisIcon /> <EllipsisIcon />

View File

@@ -2,12 +2,15 @@ import type { Route } from './+types/route'
import * as cookie from 'cookie' import * as cookie from 'cookie'
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router' import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
import { useEffect } from 'react'
import { request as req } from '@repo/util/request'
import { import {
WorkspaceProvider, WorkspaceProvider,
type Workspace type Workspace
} from '@/components/workspace-switcher' } from '@/components/workspace-switcher'
import { userContext } from '@repo/auth/context' import { userContext } from '@repo/auth/context'
import { Toaster } from '@repo/ui/components/ui/sonner'
import { authMiddleware } from '@repo/auth/middleware/auth' import { authMiddleware } from '@repo/auth/middleware/auth'
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode' import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
import { NavUser } from '@repo/ui/components/nav-user' import { NavUser } from '@repo/ui/components/nav-user'
@@ -16,10 +19,9 @@ import {
SidebarProvider, SidebarProvider,
SidebarTrigger SidebarTrigger
} from '@repo/ui/components/ui/sidebar' } from '@repo/ui/components/ui/sidebar'
import { Toaster } from '@repo/ui/components/ui/sonner'
import { request as req } from '@repo/util/request'
import { AppSidebar } from '@/components/app-sidebar' import { AppSidebar } from '@/components/app-sidebar'
// import { Notification } from '@/components/notification' // import { Notification } from '@/components/notification'
export const middleware: Route.MiddlewareFunction[] = [authMiddleware] export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
@@ -64,6 +66,16 @@ export function shouldRevalidate({
export default function Route({ loaderData }: Route.ComponentProps) { export default function Route({ loaderData }: Route.ComponentProps) {
const { user, orgs, sidebar_state } = loaderData const { user, orgs, sidebar_state } = loaderData
useEffect(() => {
if (typeof window !== 'undefined' && window.rybbit) {
window.rybbit.identify(user.sub, {
username: user.email,
name: user.name,
email: user.email
})
}
}, [])
return ( return (
<WorkspaceProvider workspaces={orgs as Workspace[]}> <WorkspaceProvider workspaces={orgs as Workspace[]}>
<SidebarProvider defaultOpen={sidebar_state === 'true'} className="flex"> <SidebarProvider defaultOpen={sidebar_state === 'true'} className="flex">

View File

@@ -14,7 +14,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment: Environment:
Variables: Variables:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo

View File

@@ -25,7 +25,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment: Environment:
Variables: Variables:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo

View File

@@ -14,7 +14,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment: Environment:
Variables: Variables:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo

View File

@@ -20,7 +20,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment: Environment:
Variables: Variables:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo

View File

@@ -623,6 +623,7 @@ class DynamoDBPersistenceLayer:
expr_attr_values: dict = {}, expr_attr_values: dict = {},
start_key: dict = {}, start_key: dict = {},
filter_expr: str | None = None, filter_expr: str | None = None,
projection_expr: str | None = None,
limit: int | None = None, limit: int | None = None,
index_forward: bool = True, index_forward: bool = True,
table_name: str | None = None, table_name: str | None = None,
@@ -661,6 +662,9 @@ class DynamoDBPersistenceLayer:
if filter_expr: if filter_expr:
attrs['FilterExpression'] = filter_expr attrs['FilterExpression'] = filter_expr
if projection_expr:
attrs['ProjectionExpression'] = projection_expr
if limit: if limit:
attrs['Limit'] = limit attrs['Limit'] = limit
@@ -1148,6 +1152,7 @@ class DynamoDBCollection:
expr_attr_values: dict = {}, expr_attr_values: dict = {},
start_key: str | None = None, start_key: str | None = None,
filter_expr: str | None = None, filter_expr: str | None = None,
projection_expr: str | None = None,
index_forward: bool = False, index_forward: bool = False,
limit: int = LIMIT, limit: int = LIMIT,
table_name: str | None = None, table_name: str | None = None,
@@ -1209,6 +1214,7 @@ class DynamoDBCollection:
expr_attr_name=key.expr_attr_name() | expr_attr_name, expr_attr_name=key.expr_attr_name() | expr_attr_name,
expr_attr_values=key.expr_attr_values() | expr_attr_values, expr_attr_values=key.expr_attr_values() | expr_attr_values,
filter_expr=filter_expr, filter_expr=filter_expr,
projection_expr=projection_expr,
index_forward=index_forward, index_forward=index_forward,
limit=limit, limit=limit,
start_key=_startkey_b64decode(start_key) if start_key else {}, start_key=_startkey_b64decode(start_key) if start_key else {},

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "layercake" name = "layercake"
version = "0.11.3" version = "0.11.4"
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions." description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
readme = "README.md" readme = "README.md"
authors = [ authors = [

View File

@@ -26,7 +26,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment: Environment:
Variables: Variables:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo

View File

@@ -8,7 +8,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment: Environment:
Variables: Variables:
LOG_LEVEL: DEBUG LOG_LEVEL: DEBUG
@@ -16,12 +16,12 @@ Globals:
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
POWERTOOLS_LOGGER_LOG_EVENT: true POWERTOOLS_LOGGER_LOG_EVENT: true
MEILISEARCH_HOST: https://meili.saladeaula.digital MEILISEARCH_HOST: https://meili.saladeaula.digital
MEILISEARCH_API_KEY: "{{resolve:ssm:/saladeaula/meili_api_key}}" MEILISEARCH_API_KEY: '{{resolve:ssm:/saladeaula/meili_api_key}}'
POSTGRES_DB: saladeaula.digital POSTGRES_DB: saladeaula.digital
POSTGRES_HOST: sp-node01.saladeaula.digital POSTGRES_HOST: sp-node01.saladeaula.digital
POSTGRES_PORT: 5432 POSTGRES_PORT: 5432
POSTGRES_USER: "{{resolve:ssm:/saladeaula/postgres_user}}" POSTGRES_USER: '{{resolve:ssm:/saladeaula/postgres_user}}'
POSTGRES_PASSWORD: "{{resolve:ssm:/saladeaula/postgres_password}}" POSTGRES_PASSWORD: '{{resolve:ssm:/saladeaula/postgres_password}}'
Resources: Resources:
MeilisearchLog: MeilisearchLog:

View File

@@ -17,7 +17,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment: Environment:
Variables: Variables:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo