mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-07 02:58:15 +00:00
Rework theme, file organization, org flow, etc (#25)
This commit is contained in:
parent
b2b4e288d2
commit
910900027d
82 changed files with 1740 additions and 7289 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import '../src/styles/globals.css';
|
||||
import '../src/app/globals.css';
|
||||
|
||||
import type { Preview } from '@storybook/react';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles/globals.css",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@
|
|||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-use": "^17.6.0",
|
||||
"sonner": "^2.0.3",
|
||||
"stripe": "^18.1.0",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
|
|
|
|||
198
pnpm-lock.yaml
generated
198
pnpm-lock.yaml
generated
|
|
@ -128,6 +128,9 @@ importers:
|
|||
react-hook-form:
|
||||
specifier: ^7.53.0
|
||||
version: 7.56.3(react@19.1.0)
|
||||
react-use:
|
||||
specifier: ^17.6.0
|
||||
version: 17.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
sonner:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
|
|
@ -3586,6 +3589,9 @@ packages:
|
|||
'@types/istanbul-reports@3.0.4':
|
||||
resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}
|
||||
|
||||
'@types/js-cookie@2.2.7':
|
||||
resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==}
|
||||
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
|
|
@ -3803,6 +3809,9 @@ packages:
|
|||
'@webassemblyjs/wast-printer@1.14.1':
|
||||
resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==}
|
||||
|
||||
'@xobotyi/scrollbar-width@1.9.5':
|
||||
resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==}
|
||||
|
||||
'@xtuc/ieee754@1.2.0':
|
||||
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
|
||||
|
||||
|
|
@ -4431,6 +4440,9 @@ packages:
|
|||
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
copy-to-clipboard@3.3.3:
|
||||
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
|
||||
|
||||
core-js-compat@3.42.0:
|
||||
resolution: {integrity: sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==}
|
||||
|
||||
|
|
@ -4504,6 +4516,9 @@ packages:
|
|||
resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
css-in-js-utils@3.1.0:
|
||||
resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==}
|
||||
|
||||
css-loader@6.11.0:
|
||||
resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==}
|
||||
engines: {node: '>= 12.13.0'}
|
||||
|
|
@ -4519,6 +4534,10 @@ packages:
|
|||
css-select@4.3.0:
|
||||
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
|
||||
|
||||
css-tree@1.1.3:
|
||||
resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
css-what@6.1.0:
|
||||
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
|
||||
engines: {node: '>= 6'}
|
||||
|
|
@ -5173,9 +5192,15 @@ packages:
|
|||
fast-safe-stringify@2.1.1:
|
||||
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
|
||||
|
||||
fast-shallow-equal@1.0.0:
|
||||
resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==}
|
||||
|
||||
fast-uri@3.0.6:
|
||||
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
|
||||
|
||||
fastest-stable-stringify@2.0.2:
|
||||
resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==}
|
||||
|
||||
fastq@1.19.1:
|
||||
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
||||
|
||||
|
|
@ -5590,6 +5615,9 @@ packages:
|
|||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
hyphenate-style-name@1.1.0:
|
||||
resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -5642,6 +5670,9 @@ packages:
|
|||
ini@1.3.8:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
|
||||
inline-style-prefixer@7.0.1:
|
||||
resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==}
|
||||
|
||||
internal-slot@1.1.0:
|
||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -6063,6 +6094,9 @@ packages:
|
|||
js-base64@3.7.7:
|
||||
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
|
||||
|
||||
js-cookie@2.2.1:
|
||||
resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==}
|
||||
|
||||
js-cookie@3.0.5:
|
||||
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -6354,6 +6388,9 @@ packages:
|
|||
md5.js@1.3.5:
|
||||
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
|
||||
|
||||
mdn-data@2.0.14:
|
||||
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
|
||||
|
||||
media-typer@1.1.0:
|
||||
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
|
@ -6477,6 +6514,12 @@ packages:
|
|||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
nano-css@5.6.2:
|
||||
resolution: {integrity: sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
react-dom: '*'
|
||||
|
||||
nano-spawn@1.0.1:
|
||||
resolution: {integrity: sha512-BfcvzBlUTxSDWfT+oH7vd6CbUV+rThLLHCIym/QO6GGLBsyVXleZs00fto2i2jzC/wPiBYk5jyOmpXWg4YopiA==}
|
||||
engines: {node: '>=20.18'}
|
||||
|
|
@ -7222,6 +7265,18 @@ packages:
|
|||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-universal-interface@0.6.2:
|
||||
resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
tslib: '*'
|
||||
|
||||
react-use@17.6.0:
|
||||
resolution: {integrity: sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
react-dom: '*'
|
||||
|
||||
react@19.1.0:
|
||||
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -7315,6 +7370,9 @@ packages:
|
|||
requires-port@1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
|
||||
resize-observer-polyfill@1.5.1:
|
||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||
|
||||
resolve-cwd@3.0.0:
|
||||
resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -7396,6 +7454,9 @@ packages:
|
|||
rrweb-cssom@0.8.0:
|
||||
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
|
||||
|
||||
rtl-css-js@1.16.1:
|
||||
resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==}
|
||||
|
||||
run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
|
|
@ -7463,6 +7524,10 @@ packages:
|
|||
resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
|
||||
screenfull@5.2.0:
|
||||
resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
secure-compare@3.0.1:
|
||||
resolution: {integrity: sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==}
|
||||
|
||||
|
|
@ -7511,6 +7576,10 @@ packages:
|
|||
resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
set-harmonic-interval@1.0.1:
|
||||
resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==}
|
||||
engines: {node: '>=6.9'}
|
||||
|
||||
set-proto@1.0.0:
|
||||
resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -7646,6 +7715,10 @@ packages:
|
|||
source-map-support@0.5.21:
|
||||
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
||||
|
||||
source-map@0.5.6:
|
||||
resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
source-map@0.6.1:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -7686,6 +7759,9 @@ packages:
|
|||
sprintf-js@1.1.3:
|
||||
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
|
||||
|
||||
stack-generator@2.0.10:
|
||||
resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==}
|
||||
|
||||
stack-trace@0.0.10:
|
||||
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
|
||||
|
||||
|
|
@ -7699,6 +7775,12 @@ packages:
|
|||
stackframe@1.3.4:
|
||||
resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==}
|
||||
|
||||
stacktrace-gps@3.1.2:
|
||||
resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==}
|
||||
|
||||
stacktrace-js@2.0.2:
|
||||
resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==}
|
||||
|
||||
stacktrace-parser@0.1.11:
|
||||
resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -7863,6 +7945,9 @@ packages:
|
|||
babel-plugin-macros:
|
||||
optional: true
|
||||
|
||||
stylis@4.3.6:
|
||||
resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
|
||||
|
||||
supports-color@5.5.0:
|
||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -7944,6 +8029,10 @@ packages:
|
|||
thread-stream@3.1.0:
|
||||
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
||||
|
||||
throttle-debounce@3.0.1:
|
||||
resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
through@2.3.8:
|
||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||
|
||||
|
|
@ -7994,6 +8083,9 @@ packages:
|
|||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
||||
toggle-selection@1.0.6:
|
||||
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
|
||||
|
||||
toidentifier@1.0.1:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
|
@ -8027,6 +8119,9 @@ packages:
|
|||
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
|
||||
engines: {node: '>=6.10'}
|
||||
|
||||
ts-easing@0.2.0:
|
||||
resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==}
|
||||
|
||||
ts-node@10.9.2:
|
||||
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
|
||||
hasBin: true
|
||||
|
|
@ -12260,6 +12355,8 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/istanbul-lib-report': 3.0.3
|
||||
|
||||
'@types/js-cookie@2.2.7': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/mdx@2.0.13': {}
|
||||
|
|
@ -12582,6 +12679,8 @@ snapshots:
|
|||
'@webassemblyjs/ast': 1.14.1
|
||||
'@xtuc/long': 4.2.2
|
||||
|
||||
'@xobotyi/scrollbar-width@1.9.5': {}
|
||||
|
||||
'@xtuc/ieee754@1.2.0': {}
|
||||
|
||||
'@xtuc/long@4.2.2': {}
|
||||
|
|
@ -13263,6 +13362,10 @@ snapshots:
|
|||
|
||||
cookie@1.0.2: {}
|
||||
|
||||
copy-to-clipboard@3.3.3:
|
||||
dependencies:
|
||||
toggle-selection: 1.0.6
|
||||
|
||||
core-js-compat@3.42.0:
|
||||
dependencies:
|
||||
browserslist: 4.24.5
|
||||
|
|
@ -13377,6 +13480,10 @@ snapshots:
|
|||
randombytes: 2.1.0
|
||||
randomfill: 1.0.4
|
||||
|
||||
css-in-js-utils@3.1.0:
|
||||
dependencies:
|
||||
hyphenate-style-name: 1.1.0
|
||||
|
||||
css-loader@6.11.0(webpack@5.99.8(@swc/core@1.11.24)(esbuild@0.25.4)):
|
||||
dependencies:
|
||||
icss-utils: 5.1.0(postcss@8.5.3)
|
||||
|
|
@ -13398,6 +13505,11 @@ snapshots:
|
|||
domutils: 2.8.0
|
||||
nth-check: 2.1.1
|
||||
|
||||
css-tree@1.1.3:
|
||||
dependencies:
|
||||
mdn-data: 2.0.14
|
||||
source-map: 0.6.1
|
||||
|
||||
css-what@6.1.0: {}
|
||||
|
||||
css.escape@1.5.1: {}
|
||||
|
|
@ -14123,8 +14235,12 @@ snapshots:
|
|||
|
||||
fast-safe-stringify@2.1.1: {}
|
||||
|
||||
fast-shallow-equal@1.0.0: {}
|
||||
|
||||
fast-uri@3.0.6: {}
|
||||
|
||||
fastest-stable-stringify@2.0.2: {}
|
||||
|
||||
fastq@1.19.1:
|
||||
dependencies:
|
||||
reusify: 1.1.0
|
||||
|
|
@ -14601,6 +14717,8 @@ snapshots:
|
|||
|
||||
husky@9.1.7: {}
|
||||
|
||||
hyphenate-style-name@1.1.0: {}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
|
@ -14647,6 +14765,10 @@ snapshots:
|
|||
|
||||
ini@1.3.8: {}
|
||||
|
||||
inline-style-prefixer@7.0.1:
|
||||
dependencies:
|
||||
css-in-js-utils: 3.1.0
|
||||
|
||||
internal-slot@1.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
|
@ -15309,6 +15431,8 @@ snapshots:
|
|||
js-base64@3.7.7:
|
||||
optional: true
|
||||
|
||||
js-cookie@2.2.1: {}
|
||||
|
||||
js-cookie@3.0.5: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
|
@ -15588,6 +15712,8 @@ snapshots:
|
|||
inherits: 2.0.4
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
mdn-data@2.0.14: {}
|
||||
|
||||
media-typer@1.1.0: {}
|
||||
|
||||
memfs@3.5.3:
|
||||
|
|
@ -15676,6 +15802,19 @@ snapshots:
|
|||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nano-css@5.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
css-tree: 1.1.3
|
||||
csstype: 3.1.3
|
||||
fastest-stable-stringify: 2.0.2
|
||||
inline-style-prefixer: 7.0.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
rtl-css-js: 1.16.1
|
||||
stacktrace-js: 2.0.2
|
||||
stylis: 4.3.6
|
||||
|
||||
nano-spawn@1.0.1: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
|
@ -16477,6 +16616,30 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/react': 19.1.3
|
||||
|
||||
react-universal-interface@0.6.2(react@19.1.0)(tslib@2.8.1):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
tslib: 2.8.1
|
||||
|
||||
react-use@17.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@types/js-cookie': 2.2.7
|
||||
'@xobotyi/scrollbar-width': 1.9.5
|
||||
copy-to-clipboard: 3.3.3
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-shallow-equal: 1.0.0
|
||||
js-cookie: 2.2.1
|
||||
nano-css: 5.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
react-universal-interface: 0.6.2(react@19.1.0)(tslib@2.8.1)
|
||||
resize-observer-polyfill: 1.5.1
|
||||
screenfull: 5.2.0
|
||||
set-harmonic-interval: 1.0.1
|
||||
throttle-debounce: 3.0.1
|
||||
ts-easing: 0.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
react@19.1.0: {}
|
||||
|
||||
read-pkg@3.0.0:
|
||||
|
|
@ -16601,6 +16764,8 @@ snapshots:
|
|||
|
||||
requires-port@1.0.0: {}
|
||||
|
||||
resize-observer-polyfill@1.5.1: {}
|
||||
|
||||
resolve-cwd@3.0.0:
|
||||
dependencies:
|
||||
resolve-from: 5.0.0
|
||||
|
|
@ -16730,6 +16895,10 @@ snapshots:
|
|||
|
||||
rrweb-cssom@0.8.0: {}
|
||||
|
||||
rtl-css-js@1.16.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
|
||||
run-parallel@1.2.0:
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
|
|
@ -16790,6 +16959,8 @@ snapshots:
|
|||
ajv-formats: 2.1.1(ajv@8.17.1)
|
||||
ajv-keywords: 5.1.0(ajv@8.17.1)
|
||||
|
||||
screenfull@5.2.0: {}
|
||||
|
||||
secure-compare@3.0.1: {}
|
||||
|
||||
secure-json-parse@2.7.0: {}
|
||||
|
|
@ -16853,6 +17024,8 @@ snapshots:
|
|||
functions-have-names: 1.2.3
|
||||
has-property-descriptors: 1.0.2
|
||||
|
||||
set-harmonic-interval@1.0.1: {}
|
||||
|
||||
set-proto@1.0.0:
|
||||
dependencies:
|
||||
dunder-proto: 1.0.1
|
||||
|
|
@ -17047,6 +17220,8 @@ snapshots:
|
|||
buffer-from: 1.1.2
|
||||
source-map: 0.6.1
|
||||
|
||||
source-map@0.5.6: {}
|
||||
|
||||
source-map@0.6.1: {}
|
||||
|
||||
source-map@0.7.4: {}
|
||||
|
|
@ -17093,6 +17268,10 @@ snapshots:
|
|||
|
||||
sprintf-js@1.1.3: {}
|
||||
|
||||
stack-generator@2.0.10:
|
||||
dependencies:
|
||||
stackframe: 1.3.4
|
||||
|
||||
stack-trace@0.0.10: {}
|
||||
|
||||
stack-utils@2.0.6:
|
||||
|
|
@ -17103,6 +17282,17 @@ snapshots:
|
|||
|
||||
stackframe@1.3.4: {}
|
||||
|
||||
stacktrace-gps@3.1.2:
|
||||
dependencies:
|
||||
source-map: 0.5.6
|
||||
stackframe: 1.3.4
|
||||
|
||||
stacktrace-js@2.0.2:
|
||||
dependencies:
|
||||
error-stack-parser: 2.1.4
|
||||
stack-generator: 2.0.10
|
||||
stacktrace-gps: 3.1.2
|
||||
|
||||
stacktrace-parser@0.1.11:
|
||||
dependencies:
|
||||
type-fest: 0.7.1
|
||||
|
|
@ -17289,6 +17479,8 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@babel/core': 7.27.1
|
||||
|
||||
stylis@4.3.6: {}
|
||||
|
||||
supports-color@5.5.0:
|
||||
dependencies:
|
||||
has-flag: 3.0.0
|
||||
|
|
@ -17367,6 +17559,8 @@ snapshots:
|
|||
dependencies:
|
||||
real-require: 0.2.0
|
||||
|
||||
throttle-debounce@3.0.1: {}
|
||||
|
||||
through@2.3.8: {}
|
||||
|
||||
timers-browserify@2.0.12:
|
||||
|
|
@ -17404,6 +17598,8 @@ snapshots:
|
|||
dependencies:
|
||||
is-number: 7.0.0
|
||||
|
||||
toggle-selection@1.0.6: {}
|
||||
|
||||
toidentifier@1.0.1: {}
|
||||
|
||||
totalist@3.0.1: {}
|
||||
|
|
@ -17426,6 +17622,8 @@ snapshots:
|
|||
|
||||
ts-dedent@2.2.0: {}
|
||||
|
||||
ts-easing@0.2.0: {}
|
||||
|
||||
ts-node@10.9.2(@swc/core@1.11.24)(@types/node@22.15.17)(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
|
|
|
|||
2965
public/logo-dark.svg
2965
public/logo-dark.svg
File diff suppressed because it is too large
Load diff
|
Before Width: | Height: | Size: 226 KiB |
2971
public/logo.svg
2971
public/logo.svg
File diff suppressed because it is too large
Load diff
|
Before Width: | Height: | Size: 226 KiB |
|
|
@ -15,18 +15,15 @@ export async function validateAuth(): Promise<
|
|||
{ userId: string; orgId: string } | ApiResponse
|
||||
> {
|
||||
const { userId, orgId } = await auth();
|
||||
|
||||
if (!userId) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Unauthorized: User required',
|
||||
};
|
||||
return { success: false, error: 'Unauthorized: User required' };
|
||||
}
|
||||
|
||||
if (!orgId) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Unauthorized: Organization required',
|
||||
};
|
||||
return { success: false, error: 'Unauthorized: Organization required' };
|
||||
}
|
||||
|
||||
return { userId, orgId };
|
||||
}
|
||||
|
||||
|
|
@ -47,6 +44,7 @@ export function handleError(error: unknown, eventPrefix: string): ApiResponse {
|
|||
event: `${eventPrefix}_update_error`,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
|
|
|
|||
96
src/app/(authenticated)/audit-logs/AuditLogs.tsx
Normal file
96
src/app/(authenticated)/audit-logs/AuditLogs.tsx
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useOrganization } from '@clerk/nextjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { AuditLogType } from '@/db/schema';
|
||||
import { timePeriods, type TimePeriod } from '@/schemas';
|
||||
import { getAuditLogs } from '@/actions/auditLogs';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from '@/components/ui';
|
||||
import { AuditLogDetails } from '@/components/audit-logs/AuditLogDetails';
|
||||
import { AuditLogEntry } from '@/components/audit-logs/AuditLogEntry';
|
||||
|
||||
export const AuditLogs = () => {
|
||||
const { organization } = useOrganization();
|
||||
const [timePeriod, setTimePeriod] = useState<TimePeriod>(7);
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLogType | null>(null);
|
||||
|
||||
const { data: logs = [], isLoading } = useQuery({
|
||||
queryKey: ['auditLogs', organization?.id, timePeriod],
|
||||
queryFn: () =>
|
||||
getAuditLogs({
|
||||
orgId: organization?.id,
|
||||
limit: 20,
|
||||
nRecentDays: timePeriod,
|
||||
}),
|
||||
enabled: !!organization?.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>All Activity</CardTitle>
|
||||
<CardDescription>
|
||||
Showing all audit logs for your organization
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-4 flex space-x-2">
|
||||
{timePeriods.map((period) => (
|
||||
<Button
|
||||
key={period}
|
||||
variant={timePeriod === period ? 'default' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setTimePeriod(period)}
|
||||
>
|
||||
Last {period} days
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{isLoading ? (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
) : logs.length > 0 ? (
|
||||
logs.map((log: AuditLogType) => (
|
||||
<AuditLogEntry
|
||||
key={log.id}
|
||||
log={log}
|
||||
onClick={(log: AuditLogType) => setSelectedLog(log)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No activity found
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Drawer open={!!selectedLog} onClose={() => setSelectedLog(null)}>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Activity Details</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
{selectedLog && <AuditLogDetails log={selectedLog} />}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
5
src/app/(authenticated)/audit-logs/page.tsx
Normal file
5
src/app/(authenticated)/audit-logs/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { AuditLogs } from './AuditLogs';
|
||||
|
||||
export default function Page() {
|
||||
return <AuditLogs />;
|
||||
}
|
||||
|
|
@ -1,21 +1,9 @@
|
|||
'use client';
|
||||
import { UsageCard } from '@/components/usage/UsageCard';
|
||||
import { AuditLogCard } from '@/components/audit-logs/AuditLogCard';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import {
|
||||
TitleBar,
|
||||
UsageAnalyticsCard,
|
||||
AuditLogCard,
|
||||
} from '@/components/dashboard';
|
||||
|
||||
export const Dashboard = () => {
|
||||
const t = useTranslations('DashboardIndex');
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleBar title={t('title_bar')} />
|
||||
<UsageAnalyticsCard />
|
||||
<AuditLogCard />
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const Dashboard = () => (
|
||||
<>
|
||||
<UsageCard />
|
||||
<AuditLogCard />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import { X } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui';
|
||||
|
||||
import type { Filter } from './types';
|
||||
|
||||
export const ActiveFilter = ({
|
||||
filter,
|
||||
onClear,
|
||||
}: {
|
||||
filter: Filter;
|
||||
onClear: () => void;
|
||||
}) => {
|
||||
if (!filter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex items-center rounded-md bg-muted p-2">
|
||||
<span className="mr-2 text-sm">
|
||||
Filtered by {filter.type}: <strong>{filter.name}</strong>
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClear}
|
||||
className="size-6 p-0"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui';
|
||||
import { UsageAnalyticsCard } from '@/components/dashboard';
|
||||
|
||||
import type { Developer, Filter, Model, Task, ViewMode } from './types';
|
||||
import { ViewModeToggle } from './ViewModeToggle';
|
||||
import { ActiveFilter } from './ActiveFilter';
|
||||
import { Developers } from './Developers';
|
||||
import { Models } from './Models';
|
||||
import { Tasks } from './Tasks';
|
||||
import { TaskDetails } from './TaskDetails';
|
||||
|
||||
export const Analytics = () => {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('tasks');
|
||||
const [filter, setFilter] = useState<Filter>(null);
|
||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
||||
|
||||
const onDeveloperClick = (developer: Developer) => {
|
||||
setFilter({ type: 'developer', id: developer.id, name: developer.name });
|
||||
setViewMode('tasks');
|
||||
};
|
||||
|
||||
const onModelClick = (model: Model) => {
|
||||
setFilter({ type: 'model', id: model.id, name: model.name });
|
||||
setViewMode('tasks');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<UsageAnalyticsCard />
|
||||
<ViewModeToggle viewMode={viewMode} setViewMode={setViewMode} />
|
||||
{filter && (
|
||||
<ActiveFilter filter={filter} onClear={() => setFilter(null)} />
|
||||
)}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{viewMode === 'developers'
|
||||
? 'Developers'
|
||||
: viewMode === 'models'
|
||||
? 'Models'
|
||||
: 'Tasks'}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{viewMode === 'tasks' && (
|
||||
<Tasks
|
||||
filter={filter}
|
||||
onTaskClick={(task: Task) => setSelectedTask(task)}
|
||||
/>
|
||||
)}
|
||||
{viewMode === 'developers' && (
|
||||
<Developers onDeveloperClick={onDeveloperClick} />
|
||||
)}
|
||||
{viewMode === 'models' && <Models onModelClick={onModelClick} />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TaskDetails task={selectedTask} onClose={() => setSelectedTask(null)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Button } from '@/components/ui';
|
||||
|
||||
import { type ViewMode, viewModes } from './types';
|
||||
|
||||
export const ViewModeToggle = ({
|
||||
viewMode,
|
||||
setViewMode,
|
||||
}: {
|
||||
viewMode: ViewMode;
|
||||
setViewMode: (mode: ViewMode) => void;
|
||||
}) => {
|
||||
const t = useTranslations('Analytics');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-sm font-medium">{t('view_mode_title')}</h3>
|
||||
<div className="flex gap-2">
|
||||
{viewModes.map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
variant={viewMode === mode ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode(mode)}
|
||||
>
|
||||
{t(`view_mode_${mode}`)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { getLocale, getTranslations } from 'next-intl/server';
|
||||
|
||||
import { TitleBar } from '@/components/dashboard/TitleBar';
|
||||
|
||||
import { Analytics } from './Analytics';
|
||||
|
||||
export default async function Page() {
|
||||
const locale = await getLocale();
|
||||
const t = await getTranslations({ locale, namespace: 'Analytics' });
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleBar
|
||||
title={t('title_bar')}
|
||||
description={t('title_bar_description')}
|
||||
/>
|
||||
<Analytics />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useOrganization } from '@clerk/nextjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from '@/components/ui';
|
||||
import { TitleBar } from '@/components/dashboard/TitleBar';
|
||||
import { AuditLogDetails } from '@/components/dashboard/AuditLogDetails';
|
||||
import { AuditLogEntry } from '@/components/dashboard/AuditLogEntry';
|
||||
import type { AuditLogType } from '@/db/schema';
|
||||
import { getAuditLogs } from '@/actions/auditLogs';
|
||||
|
||||
type TimePeriod = '7' | '30' | '90';
|
||||
|
||||
export default function Page() {
|
||||
const [timePeriod, setTimePeriod] = useState<TimePeriod>('7');
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLogType | null>(null);
|
||||
const { organization } = useOrganization();
|
||||
|
||||
const { data: logs = [], isLoading } = useQuery({
|
||||
queryKey: ['auditLogs', organization?.id, Number(timePeriod)],
|
||||
queryFn: async () =>
|
||||
await getAuditLogs({
|
||||
orgId: organization?.id,
|
||||
limit: 20,
|
||||
nRecentDays: Number(timePeriod),
|
||||
}),
|
||||
enabled: !!organization?.id,
|
||||
});
|
||||
|
||||
const handleLogClick = (log: AuditLogType) => setSelectedLog(log);
|
||||
const handleCloseDrawer = () => setSelectedLog(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleBar
|
||||
title="Audit Logs"
|
||||
description="View all organization activity and changes"
|
||||
/>
|
||||
<div className="w-2/3 rounded-md bg-card p-5">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold">All Activity</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Showing all audit logs for your organization
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4 flex space-x-2">
|
||||
<Button
|
||||
variant={timePeriod === '7' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setTimePeriod('7')}
|
||||
>
|
||||
Last 7 days
|
||||
</Button>
|
||||
<Button
|
||||
variant={timePeriod === '30' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setTimePeriod('30')}
|
||||
>
|
||||
Last 30 days
|
||||
</Button>
|
||||
<Button
|
||||
variant={timePeriod === '90' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setTimePeriod('90')}
|
||||
>
|
||||
Last 90 days
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{isLoading ? (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
) : logs.length > 0 ? (
|
||||
logs.map((log: AuditLogType) => (
|
||||
<AuditLogEntry key={log.id} log={log} onClick={handleLogClick} />
|
||||
))
|
||||
) : (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">No activity found</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Drawer open={!!selectedLog} onClose={handleCloseDrawer}>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Activity Details</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
{selectedLog && <AuditLogDetails log={selectedLog} />}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { DashboardHeader } from '@/components/dashboard/DashboardHeader';
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader />
|
||||
<div className="min-h-[calc(100vh-72px)] bg-muted">
|
||||
<div className="max-w-screen-lg mx-auto px-4 pb-16 pt-6">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { dark } from '@clerk/themes';
|
||||
import { OrganizationProfile } from '@clerk/nextjs';
|
||||
|
||||
import DefaultParametersPage from '@/components/dashboard/DefaultParametersPage';
|
||||
import ProviderWhitelistPage from '@/components/dashboard/ProviderWhitelistPage';
|
||||
|
||||
export const OrgProfile = () => {
|
||||
const t = useTranslations('OrganizationProfile');
|
||||
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
|
||||
return (
|
||||
<OrganizationProfile
|
||||
appearance={{ baseTheme }}
|
||||
routing="path"
|
||||
path="/dashboard/org"
|
||||
afterLeaveOrganizationUrl="/onboarding/select-org"
|
||||
>
|
||||
<OrganizationProfile.Page
|
||||
label={t('provider_whitelist')}
|
||||
url="provider-whitelist"
|
||||
labelIcon={<SettingsIcon />}
|
||||
>
|
||||
<ProviderWhitelistPage />
|
||||
</OrganizationProfile.Page>
|
||||
<OrganizationProfile.Page
|
||||
label={t('default_parameters')}
|
||||
url="default-parameters"
|
||||
labelIcon={<ParametersIcon />}
|
||||
>
|
||||
<DefaultParametersPage />
|
||||
</OrganizationProfile.Page>
|
||||
</OrganizationProfile>
|
||||
);
|
||||
};
|
||||
|
||||
const SettingsIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="size-4"
|
||||
>
|
||||
<path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z" />
|
||||
<path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z" />
|
||||
<path d="M12 2v2" />
|
||||
<path d="M12 22v-2" />
|
||||
<path d="m17 20.66-1-1.73" />
|
||||
<path d="M11 10.27 7 3.34" />
|
||||
<path d="m20.66 17-1.73-1" />
|
||||
<path d="m3.34 7 1.73 1" />
|
||||
<path d="M14 12h8" />
|
||||
<path d="M2 12h2" />
|
||||
<path d="m20.66 7-1.73 1" />
|
||||
<path d="m3.34 17 1.73-1" />
|
||||
<path d="m17 3.34-1 1.73" />
|
||||
<path d="m7 20.66 1-1.73" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const ParametersIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="size-4"
|
||||
>
|
||||
<path d="M3 3v18h18" />
|
||||
<path d="M18.4 9a9 9 0 0 0-9.4 9" />
|
||||
<path d="M8 12l4-4 4 4" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { OrgProfile } from './OrgProfile';
|
||||
|
||||
export default function Page() {
|
||||
return <OrgProfile />;
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
import { dark } from '@clerk/themes';
|
||||
import { UserProfile as ClerkUserProfile } from '@clerk/nextjs';
|
||||
|
||||
export const UserProfile = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
|
||||
return (
|
||||
<ClerkUserProfile
|
||||
appearance={{ baseTheme }}
|
||||
routing="path"
|
||||
path="/dashboard/user"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { UserProfile } from './UserProfile';
|
||||
|
||||
export default function Page() {
|
||||
return <UserProfile />;
|
||||
}
|
||||
|
|
@ -1,16 +1,26 @@
|
|||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { NavbarHeader, NavbarMenu, Section } from '@/components/layout';
|
||||
|
||||
export default async function AuthenticatedLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const user = await currentUser();
|
||||
const { orgId } = await auth();
|
||||
|
||||
if (!user) {
|
||||
return redirect('/sign-in');
|
||||
if (!orgId) {
|
||||
redirect('/onboarding/select-org');
|
||||
}
|
||||
|
||||
return children;
|
||||
return (
|
||||
<>
|
||||
<NavbarHeader className="h-[72px]" />
|
||||
<NavbarMenu className="h-[72px]" />
|
||||
<Section divider={false} className="min-h-[calc(100vh-72px-72px-2px)]">
|
||||
<div className="flex flex-col gap-8 py-8">{children}</div>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
Input,
|
||||
Slider,
|
||||
} from '@/components/ui';
|
||||
import { CheckCheck, FlaskConical, SlidersHorizontal } from 'lucide-react';
|
||||
|
||||
type DefaultParamsFormValues = {
|
||||
experimentalPowerSteering: boolean;
|
||||
|
|
@ -46,7 +47,7 @@ type DefaultParamsFormValues = {
|
|||
matchPrecision: number;
|
||||
};
|
||||
|
||||
const DefaultParametersPage = () => {
|
||||
export const DefaultParameters = () => {
|
||||
const t = useTranslations('ProviderWhitelist');
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
|
@ -109,32 +110,17 @@ const DefaultParametersPage = () => {
|
|||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mb-6 text-sm text-muted-foreground">
|
||||
{t('parameters_section_description')}
|
||||
</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
|
||||
<div className="space-y-4 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="size-5"
|
||||
>
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10" />
|
||||
<path d="m9 12 2 2 4-4" />
|
||||
</svg>
|
||||
<CheckCheck />
|
||||
<h2 className="text-lg font-medium">Checkpoints</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
|
@ -160,8 +146,7 @@ const DefaultParametersPage = () => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
|
||||
<div className="space-y-4 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -326,23 +311,9 @@ const DefaultParametersPage = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Settings Section */}
|
||||
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
|
||||
<div className="space-y-4 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="size-5"
|
||||
>
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||
<polyline points="3.29 7 12 12 20.71 7" />
|
||||
<line x1="12" y1="22" x2="12" y2="12" />
|
||||
</svg>
|
||||
<SlidersHorizontal />
|
||||
<h2 className="text-lg font-medium">Model Settings</h2>
|
||||
</div>
|
||||
|
||||
|
|
@ -721,26 +692,11 @@ const DefaultParametersPage = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Experimental Section */}
|
||||
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="size-5"
|
||||
>
|
||||
<path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2" />
|
||||
<path d="M8.5 2h7" />
|
||||
<path d="M7 16h10" />
|
||||
</svg>
|
||||
<FlaskConical />
|
||||
<h2 className="text-lg font-medium">Experimental</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
|
@ -790,5 +746,3 @@ const DefaultParametersPage = () => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DefaultParametersPage;
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
'use client';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { OrganizationProfile } from '@clerk/nextjs';
|
||||
import { ListTodo, SlidersHorizontal } from 'lucide-react';
|
||||
|
||||
import { DefaultParameters } from './DefaultParameters';
|
||||
import { ProviderWhitelist } from './ProviderWhitelist';
|
||||
|
||||
export const Org = () => {
|
||||
const t = useTranslations('OrganizationProfile');
|
||||
|
||||
return (
|
||||
<div className="mx-auto">
|
||||
<OrganizationProfile
|
||||
routing="path"
|
||||
path="/org"
|
||||
afterLeaveOrganizationUrl="/onboarding/select-org"
|
||||
>
|
||||
<OrganizationProfile.Page
|
||||
label={t('provider_whitelist')}
|
||||
url="provider-whitelist"
|
||||
labelIcon={<ListTodo className="size-4" />}
|
||||
>
|
||||
<ProviderWhitelist />
|
||||
</OrganizationProfile.Page>
|
||||
<OrganizationProfile.Page
|
||||
label={t('default_parameters')}
|
||||
url="default-parameters"
|
||||
labelIcon={<SlidersHorizontal className="size-4" />}
|
||||
>
|
||||
<DefaultParameters />
|
||||
</OrganizationProfile.Page>
|
||||
</OrganizationProfile>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -53,7 +53,7 @@ const initialProviders = [
|
|||
},
|
||||
];
|
||||
|
||||
const ProviderWhitelistPage = () => {
|
||||
export const ProviderWhitelist = () => {
|
||||
const t = useTranslations('ProviderWhitelist');
|
||||
|
||||
const [providers, setProviders] = useState(initialProviders);
|
||||
|
|
@ -61,7 +61,7 @@ const ProviderWhitelistPage = () => {
|
|||
const [allowAllProviders, setAllowAllProviders] = useState(true);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
// Common error handling function for API calls
|
||||
// Common error handling function for API calls.
|
||||
const handleApiError = (error: unknown, errorMessage: string) => {
|
||||
console.error(errorMessage, error);
|
||||
toast.error(errorMessage);
|
||||
|
|
@ -75,26 +75,27 @@ const ProviderWhitelistPage = () => {
|
|||
setAllowAllProviders(newAllowAllProviders);
|
||||
|
||||
let updatedProviders = providers;
|
||||
|
||||
if (newAllowAllProviders) {
|
||||
// Enable all providers and their models
|
||||
// Enable all providers and their models/
|
||||
updatedProviders = providers.map((provider) => ({
|
||||
...provider,
|
||||
enabled: true,
|
||||
models: provider.models.map((model) => ({
|
||||
...model,
|
||||
enabled: true,
|
||||
})),
|
||||
models: provider.models.map((model) => ({ ...model, enabled: true })),
|
||||
}));
|
||||
|
||||
setProviders(updatedProviders);
|
||||
}
|
||||
|
||||
// Call API to update backend with the changed allow all providers setting
|
||||
// Call API to update backend with the changed allow all providers setting.
|
||||
setIsUpdating(true);
|
||||
|
||||
try {
|
||||
const result = await updateAllowAllProviders({
|
||||
allowAllProviders: newAllowAllProviders,
|
||||
policyVersion,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to update setting');
|
||||
}
|
||||
|
|
@ -115,7 +116,7 @@ const ProviderWhitelistPage = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Find the provider to get its current state
|
||||
// Find the provider to get its current state.
|
||||
const provider = providers.find((p) => p.id === providerId);
|
||||
if (!provider) return;
|
||||
|
||||
|
|
@ -166,7 +167,7 @@ const ProviderWhitelistPage = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Find the model to get its current state
|
||||
// Find the model to get its current state.
|
||||
const provider = providers.find((p) => p.id === providerId);
|
||||
if (!provider) return;
|
||||
|
||||
|
|
@ -177,24 +178,19 @@ const ProviderWhitelistPage = () => {
|
|||
const newEnabled = !model.enabled;
|
||||
|
||||
const updatedProviders = providers.map((provider) => {
|
||||
if (provider.id === providerId) {
|
||||
return {
|
||||
...provider,
|
||||
models: provider.models.map((model) => {
|
||||
if (model.id === modelId) {
|
||||
return { ...model, enabled: newEnabled };
|
||||
}
|
||||
return model;
|
||||
}),
|
||||
};
|
||||
}
|
||||
return provider;
|
||||
return provider.id === providerId
|
||||
? {
|
||||
...provider,
|
||||
models: provider.models.map((model) =>
|
||||
model.id === modelId ? { ...model, enabled: newEnabled } : model,
|
||||
),
|
||||
}
|
||||
: provider;
|
||||
});
|
||||
|
||||
setProviders(updatedProviders);
|
||||
|
||||
// Call API to update backend with the changed model status
|
||||
setIsUpdating(true);
|
||||
|
||||
try {
|
||||
const result = await updateModelStatus({
|
||||
providerId,
|
||||
|
|
@ -202,6 +198,7 @@ const ProviderWhitelistPage = () => {
|
|||
enabled: newEnabled,
|
||||
policyVersion,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to update model status');
|
||||
}
|
||||
|
|
@ -222,11 +219,9 @@ const ProviderWhitelistPage = () => {
|
|||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mb-6 text-sm text-muted-foreground">
|
||||
{t('providers_section_description')}
|
||||
</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6">
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
|
|
@ -242,7 +237,6 @@ const ProviderWhitelistPage = () => {
|
|||
Allow All Providers
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className={allowAllProviders ? 'opacity-50' : ''}>
|
||||
{providers.map((provider) => (
|
||||
<div key={provider.id} className="mb-4">
|
||||
|
|
@ -260,7 +254,6 @@ const ProviderWhitelistPage = () => {
|
|||
{provider.name}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="ml-6 space-y-2">
|
||||
{provider.models.map((model) => (
|
||||
<div key={model.id} className="flex items-center space-x-2">
|
||||
|
|
@ -284,7 +277,6 @@ const ProviderWhitelistPage = () => {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 pt-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{`Policy v${policyVersion}`}
|
||||
|
|
@ -300,5 +292,3 @@ const ProviderWhitelistPage = () => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProviderWhitelistPage;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { Org } from './Org';
|
||||
|
||||
export default function Page() {
|
||||
return <Org />;
|
||||
}
|
||||
21
src/app/(authenticated)/usage/ActiveFilter.tsx
Normal file
21
src/app/(authenticated)/usage/ActiveFilter.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { X } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui';
|
||||
|
||||
import type { Filter } from './types';
|
||||
|
||||
type ActiveFilterProps = {
|
||||
filter: Filter;
|
||||
onClear: () => void;
|
||||
};
|
||||
|
||||
export const ActiveFilter = ({ filter, onClear }: ActiveFilterProps) => (
|
||||
<div className="mb-4 flex items-center rounded-md bg-muted p-2">
|
||||
<span className="mr-2 text-sm">
|
||||
Filtered by {filter.type}: <strong>{filter.name}</strong>
|
||||
</span>
|
||||
<Button variant="ghost" size="sm" onClick={onClear} className="size-6 p-0">
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/layout/DataTable';
|
||||
|
||||
import type { Developer } from './types';
|
||||
|
||||
|
|
@ -53,9 +53,9 @@ const mockDevelopers: Developer[] = [
|
|||
];
|
||||
|
||||
export const Developers = ({
|
||||
onDeveloperClick,
|
||||
onClick,
|
||||
}: {
|
||||
onDeveloperClick: (developer: Developer) => void;
|
||||
onClick: (developer: Developer) => void;
|
||||
}) => {
|
||||
const developerColumns: ColumnDef<Developer>[] = [
|
||||
{
|
||||
|
|
@ -66,7 +66,7 @@ export const Developers = ({
|
|||
|
||||
return (
|
||||
<button
|
||||
onClick={() => onDeveloperClick(developer)}
|
||||
onClick={() => onClick(developer)}
|
||||
className="text-left font-medium text-primary hover:underline"
|
||||
>
|
||||
{developer.name}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/layout/DataTable';
|
||||
|
||||
import type { Model } from './types';
|
||||
|
||||
|
|
@ -47,11 +47,7 @@ const mockModels: Model[] = [
|
|||
},
|
||||
];
|
||||
|
||||
export const Models = ({
|
||||
onModelClick,
|
||||
}: {
|
||||
onModelClick: (model: Model) => void;
|
||||
}) => {
|
||||
export const Models = ({ onClick }: { onClick: (model: Model) => void }) => {
|
||||
const modelColumns: ColumnDef<Model>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
|
|
@ -61,7 +57,7 @@ export const Models = ({
|
|||
|
||||
return (
|
||||
<button
|
||||
onClick={() => onModelClick(model)}
|
||||
onClick={() => onClick(model)}
|
||||
className="text-left font-medium text-primary hover:underline"
|
||||
>
|
||||
{model.name}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useMemo } from 'react';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/layout/DataTable';
|
||||
|
||||
import type { Filter, Task, ConversationMessage } from './types';
|
||||
|
||||
|
|
@ -202,10 +202,10 @@ const mockTasks: Task[] = [
|
|||
|
||||
export const Tasks = ({
|
||||
filter,
|
||||
onTaskClick,
|
||||
onClick,
|
||||
}: {
|
||||
filter: Filter;
|
||||
onTaskClick: (task: Task) => void;
|
||||
filter: Filter | null;
|
||||
onClick: (task: Task) => void;
|
||||
}) => {
|
||||
const filteredTasks = useMemo(() => {
|
||||
if (!filter) {
|
||||
|
|
@ -228,7 +228,7 @@ export const Tasks = ({
|
|||
|
||||
return (
|
||||
<button
|
||||
onClick={() => onTaskClick(task)}
|
||||
onClick={() => onClick(task)}
|
||||
className="text-left font-medium text-primary hover:underline"
|
||||
>
|
||||
{task.id}
|
||||
52
src/app/(authenticated)/usage/Usage.tsx
Normal file
52
src/app/(authenticated)/usage/Usage.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { UsageCard } from '@/components/usage/UsageCard';
|
||||
|
||||
import type { Developer, Filter, Model, Task, ViewMode } from './types';
|
||||
import { ViewModeToggle } from './ViewModeToggle';
|
||||
import { ActiveFilter } from './ActiveFilter';
|
||||
import { Developers } from './Developers';
|
||||
import { Models } from './Models';
|
||||
import { Tasks } from './Tasks';
|
||||
import { TaskDetails } from './TaskDetails';
|
||||
|
||||
export const Usage = () => {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('tasks');
|
||||
const [filter, setFilter] = useState<Filter | null>(null);
|
||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-4">
|
||||
<UsageCard />
|
||||
<ViewModeToggle viewMode={viewMode} setViewMode={setViewMode} />
|
||||
{filter && (
|
||||
<ActiveFilter filter={filter} onClear={() => setFilter(null)} />
|
||||
)}
|
||||
{viewMode === 'tasks' ? (
|
||||
<Tasks
|
||||
filter={filter}
|
||||
onClick={(task: Task) => setSelectedTask(task)}
|
||||
/>
|
||||
) : viewMode === 'developers' ? (
|
||||
<Developers
|
||||
onClick={({ id, name }: Developer) => {
|
||||
setFilter({ type: 'developer', id, name });
|
||||
setViewMode('tasks');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Models
|
||||
onClick={({ id, name }: Model) => {
|
||||
setFilter({ type: 'model', id, name });
|
||||
setViewMode('tasks');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<TaskDetails task={selectedTask} onClose={() => setSelectedTask(null)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
30
src/app/(authenticated)/usage/ViewModeToggle.tsx
Normal file
30
src/app/(authenticated)/usage/ViewModeToggle.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Button } from '@/components/ui';
|
||||
|
||||
import { type ViewMode, viewModes } from './types';
|
||||
|
||||
export const ViewModeToggle = ({
|
||||
viewMode,
|
||||
setViewMode,
|
||||
}: {
|
||||
viewMode: ViewMode;
|
||||
setViewMode: (mode: ViewMode) => void;
|
||||
}) => {
|
||||
const t = useTranslations('Analytics');
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
{viewModes.map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
variant={viewMode === mode ? 'default' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode(mode)}
|
||||
>
|
||||
{t(`view_mode_${mode}`)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
5
src/app/(authenticated)/usage/page.tsx
Normal file
5
src/app/(authenticated)/usage/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Usage } from './Usage';
|
||||
|
||||
export default async function Page() {
|
||||
return <Usage />;
|
||||
}
|
||||
|
|
@ -53,4 +53,4 @@ export type Filter = {
|
|||
type: 'developer' | 'model';
|
||||
id: string;
|
||||
name: string;
|
||||
} | null;
|
||||
};
|
||||
7
src/app/(profile)/UserButton.tsx
Normal file
7
src/app/(profile)/UserButton.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { UserButton as ClerkUserButton } from '@clerk/nextjs';
|
||||
|
||||
export const UserButton = () => (
|
||||
<div className="absolute top-8 right-8">
|
||||
<ClerkUserButton />
|
||||
</div>
|
||||
);
|
||||
18
src/app/(profile)/layout.tsx
Normal file
18
src/app/(profile)/layout.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Logo } from '@/components/layout';
|
||||
import { UserButton } from './UserButton';
|
||||
|
||||
export default async function OnboardingLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="flex flex-col gap-8">
|
||||
<Logo className="self-center" />
|
||||
{children}
|
||||
</div>
|
||||
<UserButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
src/app/(profile)/onboarding/create-org/page.tsx
Normal file
5
src/app/(profile)/onboarding/create-org/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { CreateOrganization } from '@clerk/nextjs';
|
||||
|
||||
export default async function Page() {
|
||||
return <CreateOrganization afterCreateOrganizationUrl="/dashboard" />;
|
||||
}
|
||||
11
src/app/(profile)/onboarding/select-org/page.tsx
Normal file
11
src/app/(profile)/onboarding/select-org/page.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { OrganizationList } from '@clerk/nextjs';
|
||||
|
||||
export default async function Page() {
|
||||
return (
|
||||
<OrganizationList
|
||||
afterSelectOrganizationUrl="/dashboard"
|
||||
afterCreateOrganizationUrl="/dashboard"
|
||||
hidePersonal
|
||||
/>
|
||||
);
|
||||
}
|
||||
5
src/app/(profile)/user/[[...user-profile]]/page.tsx
Normal file
5
src/app/(profile)/user/[[...user-profile]]/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { UserProfile } from '@clerk/nextjs';
|
||||
|
||||
export default function Page() {
|
||||
return <UserProfile routing="path" path="/user" />;
|
||||
}
|
||||
16
src/app/Home.tsx
Normal file
16
src/app/Home.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@clerk/nextjs';
|
||||
|
||||
export const Home = () => {
|
||||
const { isSignedIn } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.push(isSignedIn ? '/dashboard' : '/sign-in');
|
||||
}, [router, isSignedIn]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -32,10 +32,10 @@ export const DeepLink = ({ vsCodeUrl, cursorUrl }: DeepLinkProps) => {
|
|||
open VSCode with your credentials.
|
||||
</div>
|
||||
<div className="flex flex-row justify-between gap-4">
|
||||
<Button variant="outline" className="flex-1" asChild>
|
||||
<Button variant="secondary" className="flex-1" asChild>
|
||||
<Link href={vsCodeUrl}>Open in VSCode</Link>
|
||||
</Button>
|
||||
<Button variant="outline" className="flex-1" asChild>
|
||||
<Button variant="secondary" className="flex-1" asChild>
|
||||
<Link href={cursorUrl}>Open in Cursor</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
219
src/app/globals.css
Normal file
219
src/app/globals.css
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
@import 'tailwindcss';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
|
||||
--shadow-2xs: var(--shadow-2xs);
|
||||
--shadow-xs: var(--shadow-xs);
|
||||
--shadow-sm: var(--shadow-sm);
|
||||
--shadow: var(--shadow);
|
||||
--shadow-md: var(--shadow-md);
|
||||
--shadow-lg: var(--shadow-lg);
|
||||
--shadow-xl: var(--shadow-xl);
|
||||
--shadow-2xl: var(--shadow-2xl);
|
||||
--radius: 0.3rem;
|
||||
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||
--tracking-normal: var(--tracking-normal);
|
||||
--spacing: var(--spacing);
|
||||
--letter-spacing: var(--letter-spacing);
|
||||
--shadow-offset-y: var(--shadow-offset-y);
|
||||
--shadow-offset-x: var(--shadow-offset-x);
|
||||
--shadow-spread: var(--shadow-spread);
|
||||
--shadow-blur: var(--shadow-blur);
|
||||
--shadow-opacity: var(--shadow-opacity);
|
||||
--color-shadow-color: var(--shadow-color);
|
||||
|
||||
--animate-hop: hop 0.8s ease-in-out infinite;
|
||||
|
||||
@keyframes hop {
|
||||
0%,
|
||||
100% {
|
||||
transform: none;
|
||||
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-8px);
|
||||
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(0.99 0 0);
|
||||
--foreground: oklch(0 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0 0 0);
|
||||
--popover: oklch(0.99 0 0);
|
||||
--popover-foreground: oklch(0 0 0);
|
||||
--primary: oklch(0 0 0);
|
||||
--primary-foreground: oklch(1 0 0);
|
||||
--secondary: oklch(0.94 0 0);
|
||||
--secondary-foreground: oklch(0 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.44 0 0);
|
||||
--accent: oklch(0.94 0 0);
|
||||
--accent-foreground: oklch(0 0 0);
|
||||
--destructive: oklch(0.63 0.19 23.03);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.92 0 0);
|
||||
--input: oklch(0.94 0 0);
|
||||
--ring: oklch(0 0 0);
|
||||
--chart-1: oklch(0.81 0.17 75.35);
|
||||
--chart-2: oklch(0.55 0.22 264.53);
|
||||
--chart-3: oklch(0.72 0 0);
|
||||
--chart-4: oklch(0.92 0 0);
|
||||
--chart-5: oklch(0.56 0 0);
|
||||
--sidebar: oklch(0.99 0 0);
|
||||
--sidebar-foreground: oklch(0 0 0);
|
||||
--sidebar-primary: oklch(0 0 0);
|
||||
--sidebar-primary-foreground: oklch(1 0 0);
|
||||
--sidebar-accent: oklch(0.94 0 0);
|
||||
--sidebar-accent-foreground: oklch(0 0 0);
|
||||
--sidebar-border: oklch(0.94 0 0);
|
||||
--sidebar-ring: oklch(0 0 0);
|
||||
--radius: 0.3rem;
|
||||
--shadow-2xs: 0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.01);
|
||||
--shadow-xs: 0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.01);
|
||||
--shadow-sm:
|
||||
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
|
||||
0px 1px 2px -0.5px hsl(0 0% 0% / 0.03);
|
||||
--shadow:
|
||||
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
|
||||
0px 1px 2px -0.5px hsl(0 0% 0% / 0.03);
|
||||
--shadow-md:
|
||||
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
|
||||
0px 2px 4px -0.5px hsl(0 0% 0% / 0.03);
|
||||
--shadow-lg:
|
||||
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
|
||||
0px 4px 6px -0.5px hsl(0 0% 0% / 0.03);
|
||||
--shadow-xl:
|
||||
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
|
||||
0px 8px 10px -0.5px hsl(0 0% 0% / 0.03);
|
||||
--shadow-2xl: 0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.07);
|
||||
--shadow-color: hsl(0 0% 0%);
|
||||
--shadow-opacity: 0.03;
|
||||
--shadow-blur: 9.5px;
|
||||
--shadow-spread: 0.5px;
|
||||
--shadow-offset-x: 0px;
|
||||
--shadow-offset-y: 1.5px;
|
||||
--letter-spacing: 0em;
|
||||
--spacing: 0.25rem;
|
||||
--tracking-normal: 0em;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0 0 0);
|
||||
--foreground: oklch(1 0 0);
|
||||
--card: oklch(0.14 0 0);
|
||||
--card-foreground: oklch(1 0 0);
|
||||
--popover: oklch(0.18 0 0);
|
||||
--popover-foreground: oklch(1 0 0);
|
||||
--primary: oklch(1 0 0);
|
||||
--primary-foreground: oklch(0 0 0);
|
||||
--secondary: oklch(0.25 0 0);
|
||||
--secondary-foreground: oklch(1 0 0);
|
||||
--muted: oklch(0.23 0 0);
|
||||
--muted-foreground: oklch(0.72 0 0);
|
||||
--accent: oklch(0.32 0 0);
|
||||
--accent-foreground: oklch(1 0 0);
|
||||
--destructive: oklch(0.69 0.2 23.91);
|
||||
--destructive-foreground: oklch(0 0 0);
|
||||
--border: oklch(0.26 0 0);
|
||||
--input: oklch(0.32 0 0);
|
||||
--ring: oklch(0.72 0 0);
|
||||
--chart-1: oklch(0.81 0.17 75.35);
|
||||
--chart-2: oklch(0.58 0.21 260.84);
|
||||
--chart-3: oklch(0.56 0 0);
|
||||
--chart-4: oklch(0.44 0 0);
|
||||
--chart-5: oklch(0.92 0 0);
|
||||
--sidebar: oklch(0.18 0 0);
|
||||
--sidebar-foreground: oklch(1 0 0);
|
||||
--sidebar-primary: oklch(1 0 0);
|
||||
--sidebar-primary-foreground: oklch(0 0 0);
|
||||
--sidebar-accent: oklch(0.32 0 0);
|
||||
--sidebar-accent-foreground: oklch(1 0 0);
|
||||
--sidebar-border: oklch(0.32 0 0);
|
||||
--sidebar-ring: oklch(0.72 0 0);
|
||||
--font-sans: Geist, sans-serif;
|
||||
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-mono: IBM Plex Mono, monospace;
|
||||
--radius: 0.3rem;
|
||||
--shadow-2xs: 0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.01);
|
||||
--shadow-xs: 0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.01);
|
||||
--shadow-sm:
|
||||
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
|
||||
0px 1px 2px -0.5px hsl(0 0% 0% / 0.03);
|
||||
--shadow:
|
||||
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
|
||||
0px 1px 2px -0.5px hsl(0 0% 0% / 0.03);
|
||||
--shadow-md:
|
||||
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
|
||||
0px 2px 4px -0.5px hsl(0 0% 0% / 0.03);
|
||||
--shadow-lg:
|
||||
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
|
||||
0px 4px 6px -0.5px hsl(0 0% 0% / 0.03);
|
||||
--shadow-xl:
|
||||
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
|
||||
0px 8px 10px -0.5px hsl(0 0% 0% / 0.03);
|
||||
--shadow-2xl: 0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.07);
|
||||
--shadow-color: hsl(0 0% 0%);
|
||||
--shadow-opacity: 0.03;
|
||||
--shadow-blur: 9.5px;
|
||||
--shadow-spread: 0.5px;
|
||||
--shadow-offset-x: 0px;
|
||||
--shadow-offset-y: 1.5px;
|
||||
--letter-spacing: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
letter-spacing: var(--tracking-normal);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,18 @@
|
|||
import '@/styles/globals.css';
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import { Geist, Geist_Mono } from 'next/font/google';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getLocale, getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { ClerkProvider } from '@clerk/nextjs';
|
||||
|
||||
import { getClerkLocale } from '@/i18n/locale';
|
||||
import { ThemeProvider, ReactQueryProvider } from '@/components/layout';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Toaster } from '@/components/ui';
|
||||
import {
|
||||
ThemeProvider,
|
||||
AuthProvider,
|
||||
ReactQueryProvider,
|
||||
} from '@/components/layout';
|
||||
|
||||
import './globals.css';
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const locale = await getLocale();
|
||||
|
|
@ -35,6 +40,11 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||
};
|
||||
}
|
||||
|
||||
const geist = Geist({ subsets: ['latin'] });
|
||||
|
||||
const fontSans = Geist({ variable: '--font-sans', subsets: ['latin'] });
|
||||
const fontMono = Geist_Mono({ variable: '--font-mono', subsets: ['latin'] });
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
|
|
@ -44,19 +54,23 @@ export default async function RootLayout({
|
|||
setRequestLocale(locale);
|
||||
|
||||
return (
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<html
|
||||
lang={locale}
|
||||
className={cn(geist.className)}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body
|
||||
className="bg-background text-foreground antialiased"
|
||||
className={`${fontSans.variable} ${fontMono.variable} font-sans antialiased`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<NextIntlClientProvider>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<ClerkProvider localization={getClerkLocale(locale)}>
|
||||
<AuthProvider localization={getClerkLocale(locale)}>
|
||||
<ReactQueryProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
</ReactQueryProvider>
|
||||
</ClerkProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
import { CreateOrganization } from '@clerk/nextjs';
|
||||
import { dark } from '@clerk/themes';
|
||||
|
||||
export const CreateOrg = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
|
||||
return (
|
||||
<CreateOrganization
|
||||
appearance={{ baseTheme }}
|
||||
afterCreateOrganizationUrl="/dashboard"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { CreateOrg } from './CreateOrg';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function Page() {
|
||||
return <CreateOrg />;
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { redirect } from 'next/navigation';
|
||||
import { auth as clerkAuth } from '@clerk/nextjs/server';
|
||||
|
||||
export default async function OnboardingLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const auth = await clerkAuth();
|
||||
|
||||
if (!auth.userId) {
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
import { dark } from '@clerk/themes';
|
||||
import { OrganizationList } from '@clerk/nextjs';
|
||||
|
||||
export const SelectOrg = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
|
||||
return (
|
||||
<OrganizationList
|
||||
appearance={{ baseTheme }}
|
||||
afterSelectOrganizationUrl="/dashboard"
|
||||
afterCreateOrganizationUrl="/dashboard"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { SelectOrg } from './SelectOrg';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function Page() {
|
||||
return <SelectOrg />;
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import { redirect } from 'next/navigation';
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { Home } from './Home';
|
||||
|
||||
export default async function Page() {
|
||||
const user = await currentUser();
|
||||
redirect(user ? '/dashboard' : '/sign-in');
|
||||
return <Home />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,11 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { dark } from '@clerk/themes';
|
||||
import { SignIn as ClerkSignIn } from '@clerk/nextjs';
|
||||
|
||||
import { Logo } from '@/components/layout';
|
||||
|
||||
export const SignIn = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const forceRedirectUrl = useMemo(() => {
|
||||
|
|
@ -24,10 +20,7 @@ export const SignIn = () => {
|
|||
<Link href="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
<ClerkSignIn
|
||||
appearance={{ baseTheme }}
|
||||
forceRedirectUrl={forceRedirectUrl}
|
||||
/>
|
||||
<ClerkSignIn forceRedirectUrl={forceRedirectUrl} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,15 +3,11 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { dark } from '@clerk/themes';
|
||||
import { SignUp as ClerkSignUp } from '@clerk/nextjs';
|
||||
|
||||
import { Logo } from '@/components/layout';
|
||||
|
||||
export const SignUp = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const forceRedirectUrl = useMemo(() => {
|
||||
|
|
@ -24,10 +20,7 @@ export const SignUp = () => {
|
|||
<Link href="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
<ClerkSignUp
|
||||
appearance={{ baseTheme }}
|
||||
forceRedirectUrl={forceRedirectUrl}
|
||||
/>
|
||||
<ClerkSignUp forceRedirectUrl={forceRedirectUrl} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
97
src/components/audit-logs/AuditLogCard.tsx
Normal file
97
src/components/audit-logs/AuditLogCard.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
'use client';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { useOrganization } from '@clerk/nextjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
|
||||
import type { AuditLogType } from '@/db/schema';
|
||||
import { getAuditLogs } from '@/actions/auditLogs';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from '@/components/ui';
|
||||
import { Button as EnhancedButton } from '@/components/ui/ecosystem';
|
||||
import { AuditLogDetails, AuditLogEntry } from '@/components/audit-logs';
|
||||
|
||||
export function AuditLogCard() {
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLogType | null>(null);
|
||||
const { organization } = useOrganization();
|
||||
|
||||
const path = usePathname();
|
||||
|
||||
const { data: logs = [], isLoading } = useQuery({
|
||||
queryKey: ['auditLogs', organization?.id, 5],
|
||||
queryFn: () => getAuditLogs({ orgId: organization?.id, limit: 5 }),
|
||||
enabled: !!organization?.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader className="relative">
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
<CardDescription>Organization audit logs and changes</CardDescription>
|
||||
{path !== '/audit-logs' && (
|
||||
<EnhancedButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
effect="expandIcon"
|
||||
icon={ArrowRightIcon}
|
||||
iconPlacement="right"
|
||||
className="absolute top-0 right-6"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href="/audit-logs"
|
||||
className="text-sm text-primary hover:underline"
|
||||
>
|
||||
See all logs
|
||||
</Link>
|
||||
</EnhancedButton>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-1">
|
||||
{isLoading ? (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
) : logs.length > 0 ? (
|
||||
logs.map((log: AuditLogType) => (
|
||||
<AuditLogEntry
|
||||
key={log.id}
|
||||
log={log}
|
||||
onClick={(log: AuditLogType) => setSelectedLog(log)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No recent activity
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Drawer open={!!selectedLog} onClose={() => setSelectedLog(null)}>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Activity Details</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
{selectedLog && <AuditLogDetails log={selectedLog} />}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
80
src/components/audit-logs/AuditLogDetails.tsx
Normal file
80
src/components/audit-logs/AuditLogDetails.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
'use client';
|
||||
|
||||
import { ArrowRight, Calendar, Clock, User } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import type { AuditLogType } from '@/db/schema';
|
||||
|
||||
type AuditLogDetailsProps = {
|
||||
log: AuditLogType;
|
||||
};
|
||||
|
||||
const formatValue = (value: unknown) => {
|
||||
if (value === null || value === undefined) {
|
||||
return <span className="italic text-muted-foreground">None</span>;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
<ul className="list-disc pl-5">
|
||||
{value.map((item, index) => (
|
||||
<li key={index}>{String(item)}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{Object.entries(value).map(([key, val]) => (
|
||||
<div key={key} className="grid grid-cols-2 gap-2">
|
||||
<span className="text-sm font-medium">{key}:</span>
|
||||
<span className="text-sm">{String(val)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
|
||||
export const AuditLogDetails = ({ log }: AuditLogDetailsProps) => (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
||||
<Calendar className="size-4" />
|
||||
<span>{log.createdAt.toLocaleDateString()}</span>
|
||||
<Clock className="ml-2 size-4" />
|
||||
<span>{log.createdAt.toLocaleTimeString()}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
||||
<User className="size-4" />
|
||||
<span>{log.userId}</span>
|
||||
</div>
|
||||
{log.targetId && (
|
||||
<Link
|
||||
href={log.targetId}
|
||||
className="mt-2 inline-flex items-center text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
View in settings
|
||||
<ArrowRight className="ml-1 size-4" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-md border p-4">
|
||||
<h3 className="mb-4 text-sm font-medium">Changes</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium uppercase text-muted-foreground">
|
||||
New Value
|
||||
</h4>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
{formatValue(log.newValue)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
46
src/components/audit-logs/AuditLogEntry.tsx
Normal file
46
src/components/audit-logs/AuditLogEntry.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { Settings, Sliders, Users } from 'lucide-react';
|
||||
import { formatDistance } from 'date-fns';
|
||||
|
||||
import { type AuditLogType, AuditLogTargetType } from '@/db/schema';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type AuditLogEntryProps = {
|
||||
log: AuditLogType;
|
||||
onClick: (log: AuditLogType) => void;
|
||||
};
|
||||
|
||||
const getIconByType = (type: AuditLogTargetType) => {
|
||||
switch (type) {
|
||||
case AuditLogTargetType.PROVIDER_WHITELIST:
|
||||
return <Settings className="size-4 text-purple-500" />;
|
||||
case AuditLogTargetType.DEFAULT_PARAMETERS:
|
||||
return <Sliders className="size-4 text-green-500" />;
|
||||
case AuditLogTargetType.MEMBER_CHANGE:
|
||||
return <Users className="size-4 text-amber-500" />;
|
||||
default:
|
||||
return <Settings className="size-4 text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
export const AuditLogEntry = ({ log, onClick }: AuditLogEntryProps) => (
|
||||
<button
|
||||
onClick={() => onClick(log)}
|
||||
className={cn(
|
||||
'w-full rounded-md p-3 text-left transition-colors',
|
||||
'hover:bg-muted focus:bg-muted focus:outline-none',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 shrink-0">{getIconByType(log.targetType)}</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<p className="text-sm text-foreground">{log.description}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">{log.userId}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatDistance(log.createdAt, new Date(), { addSuffix: true })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
3
src/components/audit-logs/index.ts
Normal file
3
src/components/audit-logs/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { AuditLogCard } from './AuditLogCard';
|
||||
export { AuditLogDetails } from './AuditLogDetails';
|
||||
export { AuditLogEntry } from './AuditLogEntry';
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import { useOrganization } from '@clerk/nextjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from '@/components/ui/drawer';
|
||||
|
||||
import { AuditLogDetails } from './AuditLogDetails';
|
||||
import { AuditLogEntry } from './AuditLogEntry';
|
||||
import type { AuditLogType } from '@/db/schema';
|
||||
import { getAuditLogs } from '@/actions/auditLogs';
|
||||
|
||||
export function AuditLogCard() {
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLogType | null>(null);
|
||||
const { organization } = useOrganization();
|
||||
|
||||
const { data: logs = [], isLoading } = useQuery({
|
||||
queryKey: ['auditLogs', organization?.id, 5],
|
||||
queryFn: async () =>
|
||||
await getAuditLogs({ orgId: organization?.id, limit: 5 }),
|
||||
enabled: !!organization?.id,
|
||||
});
|
||||
|
||||
const handleLogClick = (log: AuditLogType) => setSelectedLog(log);
|
||||
const handleCloseDrawer = () => setSelectedLog(null);
|
||||
|
||||
return (
|
||||
<div className="mb-6 w-2/3 rounded-md bg-card p-5">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold">Recent Activity</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Organization audit logs and changes
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Log entries */}
|
||||
<div className="space-y-1">
|
||||
{isLoading ? (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
) : logs.length > 0 ? (
|
||||
logs.map((log: AuditLogType) => (
|
||||
<AuditLogEntry key={log.id} log={log} onClick={handleLogClick} />
|
||||
))
|
||||
) : (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">No recent activity</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* "See all logs" link */}
|
||||
<div className="mt-4 text-right">
|
||||
<Link
|
||||
href="/dashboard/audit-logs"
|
||||
className="text-sm text-primary hover:underline"
|
||||
>
|
||||
See all logs
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Drawer open={!!selectedLog} onClose={handleCloseDrawer}>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Activity Details</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
{selectedLog && <AuditLogDetails log={selectedLog} />}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { ArrowRight, Calendar, Clock, User } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
|
||||
import { AuditLogTargetType, type AuditLogType } from '@/db/schema';
|
||||
import { getBaseUrl } from '@/lib/metadata';
|
||||
|
||||
type AuditLogDetailsProps = {
|
||||
log: AuditLogType;
|
||||
};
|
||||
|
||||
const getSettingsUrl = (type: AuditLogTargetType): string => {
|
||||
const baseUrl = `${getBaseUrl()}/dashboard/org`;
|
||||
switch (type) {
|
||||
case AuditLogTargetType.MEMBER_CHANGE:
|
||||
return `${baseUrl}/organization-members`;
|
||||
case AuditLogTargetType.PROVIDER_WHITELIST:
|
||||
return `${baseUrl}/provider-whitelist`;
|
||||
case AuditLogTargetType.DEFAULT_PARAMETERS:
|
||||
return `${baseUrl}/default-parameters`;
|
||||
default:
|
||||
return baseUrl;
|
||||
}
|
||||
};
|
||||
|
||||
const formatValue = (value: unknown): React.ReactNode => {
|
||||
if (value === null || value === undefined) {
|
||||
return <span className="italic text-muted-foreground">None</span>;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
<ul className="list-disc pl-5">
|
||||
{value.map((item, index) => (
|
||||
<li key={index}>{String(item)}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{Object.entries(value).map(([key, val]) => (
|
||||
<div key={key} className="grid grid-cols-2 gap-2">
|
||||
<span className="text-sm font-medium">{key}:</span>
|
||||
<span className="text-sm">{String(val)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
|
||||
export function AuditLogDetails({ log }: AuditLogDetailsProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header information */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
||||
<Calendar className="size-4" />
|
||||
<span>{log.createdAt.toLocaleDateString()}</span>
|
||||
<Clock className="ml-2 size-4" />
|
||||
<span>{log.createdAt.toLocaleTimeString()}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
||||
<User className="size-4" />
|
||||
<span>{log.userId}</span>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={getSettingsUrl(log.targetType)}
|
||||
className="mt-2 inline-flex items-center text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
View in settings
|
||||
<ArrowRight className="ml-1 size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Change details */}
|
||||
<div className="rounded-md border p-4">
|
||||
<h3 className="mb-4 text-sm font-medium">Changes</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium uppercase text-muted-foreground">
|
||||
New Value
|
||||
</h4>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
{formatValue(log.newValue)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { Settings, Sliders, Users } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { formatDistance } from 'date-fns';
|
||||
import { type AuditLogType, AuditLogTargetType } from '@/db/schema';
|
||||
|
||||
type AuditLogEntryProps = {
|
||||
log: AuditLogType;
|
||||
onClick: (log: AuditLogType) => void;
|
||||
};
|
||||
|
||||
const getIconByType = (type: AuditLogTargetType) => {
|
||||
switch (type) {
|
||||
case AuditLogTargetType.PROVIDER_WHITELIST:
|
||||
return <Settings className="size-4 text-purple-500" />;
|
||||
case AuditLogTargetType.DEFAULT_PARAMETERS:
|
||||
return <Sliders className="size-4 text-green-500" />;
|
||||
case AuditLogTargetType.MEMBER_CHANGE:
|
||||
return <Users className="size-4 text-amber-500" />;
|
||||
default:
|
||||
return <Settings className="size-4 text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
export function AuditLogEntry({ log, onClick }: AuditLogEntryProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => onClick(log)}
|
||||
className={cn(
|
||||
'w-full rounded-md p-3 text-left transition-colors',
|
||||
'hover:bg-muted focus:bg-muted focus:outline-none',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 shrink-0">{getIconByType(log.targetType)}</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<p className="text-sm text-foreground">{log.description}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">{log.userId}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatDistance(log.createdAt, new Date(), { addSuffix: true })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { OrganizationSwitcher, UserButton } from '@clerk/nextjs';
|
||||
import { dark } from '@clerk/themes';
|
||||
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui';
|
||||
import { LocaleSwitcher } from '@/components/layout/LocaleSwitcher';
|
||||
import { ThemeSwitcher } from '@/components/layout/ThemeSwitcher';
|
||||
import { Logo } from '@/components/layout/Logo';
|
||||
|
||||
export const DashboardHeader = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="p-4 border-b">
|
||||
<div className="flex justify-between max-w-screen-lg mx-auto">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/dashboard">
|
||||
<Logo />
|
||||
</Link>
|
||||
<OrganizationSwitcher
|
||||
appearance={{ baseTheme }}
|
||||
organizationProfileMode="navigation"
|
||||
organizationProfileUrl="/dashboard/org"
|
||||
afterCreateOrganizationUrl="/dashboard"
|
||||
createOrganizationUrl="/onboarding/create-org"
|
||||
/>
|
||||
<Tabs
|
||||
onValueChange={(value) => router.push(value)}
|
||||
defaultValue="/dashboard"
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="/dashboard">Dashboard</TabsTrigger>
|
||||
<TabsTrigger value="/dashboard/analytics">Analytics</TabsTrigger>
|
||||
<TabsTrigger value="/dashboard/audit-logs">
|
||||
Audit Logs
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="/dashboard/org">Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
<ul className="flex items-center gap-2 [&_li[data-fade]:hover]:opacity-100 [&_li[data-fade]]:opacity-60">
|
||||
<li data-fade>
|
||||
<LocaleSwitcher />
|
||||
</li>
|
||||
<li data-fade>
|
||||
<ThemeSwitcher />
|
||||
</li>
|
||||
<li>
|
||||
<UserButton
|
||||
appearance={{ baseTheme }}
|
||||
userProfileMode="navigation"
|
||||
userProfileUrl="/dashboard/user"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAuth } from '@clerk/nextjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { TelemetryEventName } from '@roo-code/types';
|
||||
|
||||
import { type TimePeriod, timePeriods } from '@/schemas';
|
||||
import { getUsage } from '@/actions/analytics';
|
||||
|
||||
import { Button } from '@/components/ui';
|
||||
|
||||
export const UsageAnalyticsCard = () => {
|
||||
const t = useTranslations('DashboardIndex');
|
||||
const { orgId } = useAuth();
|
||||
const [timePeriod, setTimePeriod] = useState<TimePeriod>(7);
|
||||
|
||||
const path = usePathname();
|
||||
|
||||
const usage = useQuery({
|
||||
queryKey: ['usage', orgId, timePeriod],
|
||||
queryFn: () => getUsage({ orgId, timePeriod }),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-md bg-card p-5">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold">{t('analytics_title')}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('analytics_description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4 flex space-x-2">
|
||||
{timePeriods.map((period) => (
|
||||
<Button
|
||||
key={period}
|
||||
variant={period === timePeriod ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setTimePeriod(period)}
|
||||
>
|
||||
{t(`analytics_period_${period}_days`)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-5">
|
||||
<div className="rounded-lg bg-background p-3">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('analytics_active_developers')}
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-semibold">
|
||||
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.userCount ?? '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-background p-3">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('analytics_tasks_started')}
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-semibold">
|
||||
{usage.data?.[TelemetryEventName.TASK_CREATED]?.eventCount ?? '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-background p-3">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('analytics_tasks_completed')}
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-semibold">
|
||||
{usage.data?.[TelemetryEventName.TASK_COMPLETED]?.eventCount ?? '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-background p-3">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('analytics_tokens_consumed')}
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-semibold">
|
||||
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.inputTokens ??
|
||||
'-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-background p-3">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('analytics_llm_costs')}
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-semibold">
|
||||
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.cost?.toFixed(
|
||||
2,
|
||||
) ?? '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{path !== '/dashboard/analytics' && (
|
||||
<div className="mt-4 text-right">
|
||||
<Link
|
||||
href="/dashboard/analytics"
|
||||
className="text-sm text-primary hover:underline"
|
||||
>
|
||||
{t('analytics_view_details')}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export { TitleBar } from './TitleBar';
|
||||
export { UsageAnalyticsCard } from './UsageAnalyticsCard';
|
||||
export { AuditLogCard } from './AuditLogCard';
|
||||
30
src/components/layout/AuthProvider.tsx
Normal file
30
src/components/layout/AuthProvider.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
'use client';
|
||||
|
||||
import { ClerkProvider } from '@clerk/nextjs';
|
||||
import { dark } from '@clerk/themes';
|
||||
|
||||
import { getClerkLocale } from '@/i18n/locale';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
type Localization = ReturnType<typeof getClerkLocale>;
|
||||
|
||||
export const AuthProvider = ({
|
||||
localization,
|
||||
children,
|
||||
}: {
|
||||
localization: Localization;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
|
||||
return (
|
||||
<ClerkProvider
|
||||
appearance={{ baseTheme }}
|
||||
localization={localization}
|
||||
afterSignOutUrl={'/sign-in'}
|
||||
>
|
||||
{children}
|
||||
</ClerkProvider>
|
||||
);
|
||||
};
|
||||
|
|
@ -32,7 +32,7 @@ export function DataTable<TData, TValue>({
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<div className="bg-card border shadow-md rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Section } from '@/components/layout';
|
||||
|
||||
export const Footer = () => {
|
||||
const t = useTranslations('Footer');
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<ul className="mt-4 flex flex-row gap-x-5 text-muted-foreground [&_svg:hover]:text-primary [&_svg:hover]:opacity-100 [&_svg]:size-5 [&_svg]:fill-current [&_svg]:opacity-60">
|
||||
<li>
|
||||
<Link href="/">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.954 4.569a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.691 8.094 4.066 6.13 1.64 3.161a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.061a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.937 4.937 0 004.604 3.417 9.868 9.868 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63a9.936 9.936 0 002.46-2.548l-.047-.02z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-6 flex w-full items-center justify-between gap-y-2 border-t pt-3 text-sm text-muted-foreground max-md:flex-col">
|
||||
<div>© Copyright {new Date().getFullYear()} Roo Code.</div>
|
||||
<ul className="flex gap-x-4 font-medium [&_a:hover]:opacity-100 [&_a]:opacity-60">
|
||||
<li>
|
||||
<Link href="/sign-up">{t('terms_of_service')}</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/sign-up">{t('privacy_policy')}</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,22 +1,71 @@
|
|||
import Image from 'next/image';
|
||||
'use client';
|
||||
|
||||
export const Logo = () => (
|
||||
<div className="flex items-center text-xl font-semibold">
|
||||
<Image
|
||||
src="/logo.svg"
|
||||
alt="Roo Code Logo"
|
||||
width={120}
|
||||
height={26}
|
||||
className="mr-2 dark:hidden"
|
||||
style={{ height: 'auto' }}
|
||||
/>
|
||||
<Image
|
||||
src="/logo-dark.svg"
|
||||
alt="Roo Code Logo"
|
||||
width={120}
|
||||
height={26}
|
||||
className="mr-2 hidden dark:block"
|
||||
style={{ height: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
import { SVGProps, useEffect, useRef, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useHover } from 'react-use';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type LogoProps = Omit<
|
||||
SVGProps<SVGSVGElement>,
|
||||
'xmlns' | 'viewBox' | 'onClick' | 'fill'
|
||||
>;
|
||||
|
||||
export const Logo = ({
|
||||
width = 50,
|
||||
height = 32,
|
||||
className,
|
||||
...props
|
||||
}: LogoProps) => {
|
||||
const router = useRouter();
|
||||
const { resolvedTheme } = useTheme();
|
||||
const [fill, setFill] = useState('#000');
|
||||
|
||||
useEffect(() => {
|
||||
setFill(resolvedTheme === 'dark' ? '#fff' : '#000');
|
||||
}, [resolvedTheme]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="90 12 100 64"
|
||||
onClick={() => router.push('/')}
|
||||
className={cn('logo cursor-pointer', className)}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M171.633,15.8336l-1.7284,6.2499c-.0915.3309-.4369.5221-.7659.4239l-28.9937-8.6507c-.1928-.0575-.4016-.0167-.5586.1092l-28.7143,23.0269c-.0838.0672-.1839.1112-.2901.1276l-17.0849,2.6329c-.3163.0488-.5419.3327-.5178.6519l.0742.9817c.0237.3136.2809.5583.5953.5664l19.8448.513.2263.0063,14.6634-7.8328c.2053-.1097.455-.0936.6445.0415l10.3884,7.4053c.1629.1161.2589.3045.2571.5045l-.0876,9.826c-.0011.1272.0373.2515.11.3559l14.6133,20.9682c.1146.1644.3024.2624.5028.2624h4.626c.4615,0,.7574-.4908.542-.8989l-10.4155-19.7312c-.1019-.193-.0934-.4255.0221-.6106l5.4305-8.6994c.0591-.0947.143-.1715.2425-.222l19.415-9.8522c.1973-.1001.4332-.0861.6172.0366l5.5481,3.6981c.1007.0671.2189.1029.3399.1029h5.0407c.4881,0,.7804-.5429.5116-.9503l-13.9967-21.2171c-.2898-.4393-.962-.3331-1.1022.1741Z"
|
||||
fill={fill}
|
||||
strokeWidth="0"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const HoppingLogo = (props: LogoProps) => {
|
||||
const ref = useRef<SVGSVGElement>(null);
|
||||
const logo = <Logo ref={ref} {...props} />;
|
||||
const [hoverable, hovered] = useHover(logo);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
const isHopping =
|
||||
element !== null && element.classList.contains('animate-hop');
|
||||
|
||||
if (hovered && element && !isHopping) {
|
||||
element.classList.add('animate-hop');
|
||||
} else if (element && isHopping) {
|
||||
const onAnimationEnd = () => {
|
||||
element.classList.remove('animate-hop');
|
||||
element.removeEventListener('animationiteration', onAnimationEnd);
|
||||
};
|
||||
|
||||
element.addEventListener('animationiteration', onAnimationEnd);
|
||||
}
|
||||
}, [hovered]);
|
||||
|
||||
return hoverable;
|
||||
};
|
||||
|
|
|
|||
41
src/components/layout/NavbarHeader.tsx
Normal file
41
src/components/layout/NavbarHeader.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import Link from 'next/link';
|
||||
import { OrganizationSwitcher, UserButton } from '@clerk/nextjs';
|
||||
|
||||
import {
|
||||
LocaleSwitcher,
|
||||
ThemeSwitcher,
|
||||
HoppingLogo,
|
||||
} from '@/components/layout';
|
||||
|
||||
import { Section } from './Section';
|
||||
|
||||
type NavbarHeaderProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>;
|
||||
|
||||
export const NavbarHeader = (props: NavbarHeaderProps) => (
|
||||
<Section {...props}>
|
||||
<div className="flex justify-between items-center h-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/dashboard">
|
||||
<HoppingLogo />
|
||||
</Link>
|
||||
<OrganizationSwitcher
|
||||
organizationProfileMode="navigation"
|
||||
organizationProfileUrl="/org"
|
||||
afterCreateOrganizationUrl="/dashboard"
|
||||
hidePersonal
|
||||
/>
|
||||
</div>
|
||||
<ul className="flex items-center gap-2 [&_li[data-fade]:hover]:opacity-100 [&_li[data-fade]]:opacity-60">
|
||||
<li data-fade>
|
||||
<LocaleSwitcher />
|
||||
</li>
|
||||
<li data-fade>
|
||||
<ThemeSwitcher />
|
||||
</li>
|
||||
<li>
|
||||
<UserButton />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
62
src/components/layout/NavbarMenu.tsx
Normal file
62
src/components/layout/NavbarMenu.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/ecosystem';
|
||||
|
||||
import { Section } from './Section';
|
||||
|
||||
type NavbarMenuProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>;
|
||||
|
||||
const tabValues = [
|
||||
'/dashboard',
|
||||
'/usage',
|
||||
'/audit-logs',
|
||||
'/org',
|
||||
'/hidden',
|
||||
] as const;
|
||||
|
||||
type TabValue = (typeof tabValues)[number];
|
||||
|
||||
const isTabValue = (value: string): value is TabValue =>
|
||||
tabValues.includes(value as TabValue);
|
||||
|
||||
export const NavbarMenu = (props: NavbarMenuProps) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [tabValue, setTabValue] = useState<TabValue | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
setTabValue(isTabValue(pathname) ? pathname : '/hidden');
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Section {...props}>
|
||||
<div className="flex justify-between items-center h-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tabs
|
||||
onValueChange={(value) => {
|
||||
if (isTabValue(value)) {
|
||||
setTabValue(value);
|
||||
|
||||
if (value !== '/hidden') {
|
||||
router.push(value);
|
||||
}
|
||||
}
|
||||
}}
|
||||
value={tabValue}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="/dashboard">Dashboard</TabsTrigger>
|
||||
<TabsTrigger value="/usage">Usage</TabsTrigger>
|
||||
<TabsTrigger value="/audit-logs">Audit Logs</TabsTrigger>
|
||||
<TabsTrigger value="/org">Organization</TabsTrigger>
|
||||
<TabsTrigger value="/hidden" className="hidden" />
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,9 +1,21 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
type SectionProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
type SectionProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
divider?: boolean;
|
||||
};
|
||||
|
||||
export const Section = ({ className, children, ...rest }: SectionProps) => (
|
||||
<div className={cn('px-4 py-16', className)} {...rest}>
|
||||
<div className="mx-auto max-w-screen-lg">{children}</div>
|
||||
export const Section = ({
|
||||
className,
|
||||
divider = true,
|
||||
...rest
|
||||
}: SectionProps) => (
|
||||
<div className={cn({ 'border-b border-dashed': divider })}>
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-screen-lg border-l border-r border-dashed mx-auto px-8',
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ type TitleBarProps = {
|
|||
};
|
||||
|
||||
export const TitleBar = ({ title, description }: TitleBarProps) => (
|
||||
<div className="mb-8">
|
||||
<div>
|
||||
<div className="text-2xl font-semibold">{title}</div>
|
||||
{description && (
|
||||
<div className="text-sm font-medium text-muted-foreground">
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
export { Footer } from './Footer';
|
||||
export { Section } from './Section';
|
||||
export { LocaleSwitcher } from './LocaleSwitcher';
|
||||
export { ThemeProvider } from './ThemeProvider';
|
||||
export { ThemeSwitcher } from './ThemeSwitcher';
|
||||
export { Logo, HoppingLogo } from './Logo';
|
||||
export { NavbarHeader } from './NavbarHeader';
|
||||
export { NavbarMenu } from './NavbarMenu';
|
||||
|
||||
export { ThemeProvider } from './ThemeProvider';
|
||||
export { AuthProvider } from './AuthProvider';
|
||||
export { ReactQueryProvider } from './ReactQueryProvider';
|
||||
export { Logo } from './Logo';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
|||
data-slot="card"
|
||||
className={cn(
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||
'dark:border-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
121
src/components/ui/ecosystem/button.tsx
Normal file
121
src/components/ui/ecosystem/button.tsx
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import * as React from 'react';
|
||||
import { Slot, Slottable } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
effect: {
|
||||
expandIcon: 'group gap-0 relative',
|
||||
ringHover:
|
||||
'transition-all duration-300 hover:ring-2 hover:ring-primary/90 hover:ring-offset-2',
|
||||
shine:
|
||||
'before:animate-shine relative overflow-hidden before:absolute before:inset-0 before:rounded-[inherit] before:bg-[linear-gradient(45deg,transparent_25%,rgba(255,255,255,0.5)_50%,transparent_75%,transparent_100%)] before:bg-[length:250%_250%,100%_100%] before:bg-no-repeat background-position_0s_ease',
|
||||
shineHover:
|
||||
'relative overflow-hidden before:absolute before:inset-0 before:rounded-[inherit] before:bg-[linear-gradient(45deg,transparent_25%,rgba(255,255,255,0.5)_50%,transparent_75%,transparent_100%)] before:bg-[length:250%_250%,100%_100%] before:bg-[position:200%_0,0_0] before:bg-no-repeat before:transition-[background-position_0s_ease] hover:before:bg-[position:-100%_0,0_0] before:duration-1000',
|
||||
gooeyRight:
|
||||
'relative z-0 overflow-hidden transition-all duration-500 before:absolute before:inset-0 before:-z-10 before:translate-x-[150%] before:translate-y-[150%] before:scale-[2.5] before:rounded-[100%] before:bg-gradient-to-r from-white/40 before:transition-transform before:duration-1000 hover:before:translate-x-[0%] hover:before:translate-y-[0%]',
|
||||
gooeyLeft:
|
||||
'relative z-0 overflow-hidden transition-all duration-500 after:absolute after:inset-0 after:-z-10 after:translate-x-[-150%] after:translate-y-[150%] after:scale-[2.5] after:rounded-[100%] after:bg-gradient-to-l from-white/40 after:transition-transform after:duration-1000 hover:after:translate-x-[0%] hover:after:translate-y-[0%]',
|
||||
underline:
|
||||
'relative !no-underline after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-left after:scale-x-100 hover:after:origin-bottom-right hover:after:scale-x-0 after:transition-transform after:ease-in-out after:duration-300',
|
||||
hoverUnderline:
|
||||
'relative !no-underline after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-right after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100 after:transition-transform after:ease-in-out after:duration-300',
|
||||
gradientSlideShow:
|
||||
'bg-[size:400%] bg-[linear-gradient(-45deg,var(--gradient-lime),var(--gradient-ocean),var(--gradient-wine),var(--gradient-rust))] animate-gradient-flow',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface IconProps {
|
||||
icon: React.ElementType;
|
||||
iconPlacement: 'left' | 'right';
|
||||
}
|
||||
|
||||
interface IconRefProps {
|
||||
icon?: never;
|
||||
iconPlacement?: undefined;
|
||||
}
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export type ButtonIconProps = IconProps | IconRefProps;
|
||||
|
||||
const Button = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ButtonProps & ButtonIconProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant,
|
||||
effect,
|
||||
size,
|
||||
icon: Icon,
|
||||
iconPlacement,
|
||||
asChild = false,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, effect, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{Icon &&
|
||||
iconPlacement === 'left' &&
|
||||
(effect === 'expandIcon' ? (
|
||||
<div className="w-0 translate-x-[0%] pr-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:translate-x-100 group-hover:pr-2 group-hover:opacity-100">
|
||||
<Icon />
|
||||
</div>
|
||||
) : (
|
||||
<Icon />
|
||||
))}
|
||||
<Slottable>{props.children}</Slottable>
|
||||
{Icon &&
|
||||
iconPlacement === 'right' &&
|
||||
(effect === 'expandIcon' ? (
|
||||
<div className="w-0 translate-x-[100%] pl-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:translate-x-0 group-hover:pl-2 group-hover:opacity-100">
|
||||
<Icon />
|
||||
</div>
|
||||
) : (
|
||||
<Icon />
|
||||
))}
|
||||
</Comp>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button, buttonVariants };
|
||||
2
src/components/ui/ecosystem/index.ts
Normal file
2
src/components/ui/ecosystem/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './button';
|
||||
export * from './tabs';
|
||||
124
src/components/ui/ecosystem/tabs.tsx
Normal file
124
src/components/ui/ecosystem/tabs.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ComponentRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const [indicatorStyle, setIndicatorStyle] = useState({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
const tabsListRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const updateIndicator = React.useCallback(() => {
|
||||
if (!tabsListRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeTab = tabsListRef.current.querySelector<HTMLElement>(
|
||||
'[data-state="active"]',
|
||||
);
|
||||
|
||||
if (!activeTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeRect = activeTab.getBoundingClientRect();
|
||||
const tabsRect = tabsListRef.current.getBoundingClientRect();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
setIndicatorStyle({
|
||||
left: activeRect.left - tabsRect.left,
|
||||
top: activeRect.top - tabsRect.top,
|
||||
width: activeRect.width,
|
||||
height: activeRect.height,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(updateIndicator, 0);
|
||||
|
||||
window.addEventListener('resize', updateIndicator);
|
||||
const observer = new MutationObserver(updateIndicator);
|
||||
|
||||
if (tabsListRef.current) {
|
||||
observer.observe(tabsListRef.current, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener('resize', updateIndicator);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [updateIndicator]);
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden" ref={tabsListRef}>
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative inline-flex items-center justify-center rounded-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute rounded transition-all duration-300 ease-in-out pointer-events-none',
|
||||
'bg-primary',
|
||||
)}
|
||||
style={indicatorStyle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ComponentRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 z-10',
|
||||
'transition-colors duration-500 data-[state=active]:text-primary-foreground cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ComponentRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||
13
src/components/ui/skeleton.tsx
Normal file
13
src/components/ui/skeleton.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn('bg-accent animate-pulse rounded-md', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
|
|
@ -1,124 +1,66 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ComponentRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const [indicatorStyle, setIndicatorStyle] = useState({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
const tabsListRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const updateIndicator = React.useCallback(() => {
|
||||
if (!tabsListRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeTab = tabsListRef.current.querySelector<HTMLElement>(
|
||||
'[data-state="active"]',
|
||||
);
|
||||
|
||||
if (!activeTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeRect = activeTab.getBoundingClientRect();
|
||||
const tabsRect = tabsListRef.current.getBoundingClientRect();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
setIndicatorStyle({
|
||||
left: activeRect.left - tabsRect.left,
|
||||
top: activeRect.top - tabsRect.top,
|
||||
width: activeRect.width,
|
||||
height: activeRect.height,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(updateIndicator, 0);
|
||||
|
||||
window.addEventListener('resize', updateIndicator);
|
||||
const observer = new MutationObserver(updateIndicator);
|
||||
|
||||
if (tabsListRef.current) {
|
||||
observer.observe(tabsListRef.current, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener('resize', updateIndicator);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [updateIndicator]);
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<div className="relative" ref={tabsListRef}>
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative inline-flex items-center justify-center rounded-sm bg-primary p-0.5 text-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute rounded-sm transition-all duration-300 ease-in-out pointer-events-none',
|
||||
'bg-accent/5',
|
||||
)}
|
||||
style={indicatorStyle}
|
||||
/>
|
||||
</div>
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn('flex flex-col gap-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
}
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ComponentRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 z-10',
|
||||
'data-[state=active]:text-accent data-[state=active]:font-medium cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ComponentRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn('flex-1 outline-none', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
|
|
|
|||
128
src/components/usage/UsageCard.tsx
Normal file
128
src/components/usage/UsageCard.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAuth } from '@clerk/nextjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
|
||||
import { TelemetryEventName } from '@roo-code/types';
|
||||
|
||||
import { type TimePeriod, timePeriods } from '@/schemas';
|
||||
import { getUsage } from '@/actions/analytics';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from '@/components/ui';
|
||||
import { Button as EnhancedButton } from '@/components/ui/ecosystem';
|
||||
|
||||
export const UsageCard = () => {
|
||||
const t = useTranslations('DashboardIndex');
|
||||
const { orgId } = useAuth();
|
||||
const [timePeriod, setTimePeriod] = useState<TimePeriod>(7);
|
||||
|
||||
const path = usePathname();
|
||||
|
||||
const usage = useQuery({
|
||||
queryKey: ['usage', orgId, timePeriod],
|
||||
queryFn: () => getUsage({ orgId, timePeriod }),
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="relative">
|
||||
<CardTitle>{t('analytics_title')}</CardTitle>
|
||||
<CardDescription>{t('analytics_description')}</CardDescription>
|
||||
{path !== '/usage' && (
|
||||
<EnhancedButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
effect="expandIcon"
|
||||
icon={ArrowRightIcon}
|
||||
iconPlacement="right"
|
||||
className="absolute top-0 right-6"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href="/usage"
|
||||
className="text-sm text-primary hover:underline"
|
||||
>
|
||||
See all usage
|
||||
</Link>
|
||||
</EnhancedButton>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-row gap-2">
|
||||
{timePeriods.map((period) => (
|
||||
<Button
|
||||
key={period}
|
||||
variant={period === timePeriod ? 'default' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setTimePeriod(period)}
|
||||
>
|
||||
{t(`analytics_period_${period}_days`)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-5">
|
||||
<div className="rounded-lg bg-background p-3">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('analytics_active_developers')}
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-semibold">
|
||||
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.userCount ??
|
||||
'-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-background p-3">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('analytics_tasks_started')}
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-semibold">
|
||||
{usage.data?.[TelemetryEventName.TASK_CREATED]?.eventCount ??
|
||||
'-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-background p-3">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('analytics_tasks_completed')}
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-semibold">
|
||||
{usage.data?.[TelemetryEventName.TASK_COMPLETED]?.eventCount ??
|
||||
'-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-background p-3">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('analytics_tokens_consumed')}
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-semibold">
|
||||
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.inputTokens ??
|
||||
'-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-background p-3">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('analytics_llm_costs')}
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-semibold">
|
||||
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.cost?.toFixed(
|
||||
2,
|
||||
) ?? '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
@ -23,9 +23,7 @@ export const auditLogs = pgTable('audit_logs', {
|
|||
export enum AuditLogTargetType {
|
||||
PROVIDER_WHITELIST = 1,
|
||||
DEFAULT_PARAMETERS = 2,
|
||||
MEMBER_CHANGE = 3, // TODO: Currently no logs of this type are collected
|
||||
MEMBER_CHANGE = 3, // TODO: Currently no logs of this type are collected.
|
||||
}
|
||||
|
||||
export type AuditLogType = Omit<typeof auditLogs.$inferSelect, 'targetType'> & {
|
||||
targetType: AuditLogTargetType;
|
||||
};
|
||||
export type AuditLogType = typeof auditLogs.$inferSelect;
|
||||
|
|
|
|||
|
|
@ -27,8 +27,7 @@
|
|||
"analytics_tokens_consumed": "Tokens Consumed",
|
||||
"analytics_costs": "Costs (USD)",
|
||||
"analytics_llm_costs": "LLM Model Costs (USD)",
|
||||
"analytics_active_developers": "Active Developers",
|
||||
"analytics_view_details": "View detailed analytics"
|
||||
"analytics_active_developers": "Active Developers"
|
||||
},
|
||||
"UserProfile": {
|
||||
"title_bar": "User Profile",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
|
||||
|
||||
const isProtectedRoute = createRouteMatcher([
|
||||
'/dashboard(.*)',
|
||||
'/onboarding(.*)',
|
||||
'/api(.*)',
|
||||
const isUnprotectedRoute = createRouteMatcher([
|
||||
'/sign-in(.*)',
|
||||
'/sign-up(.*)',
|
||||
'/',
|
||||
]);
|
||||
|
||||
export default clerkMiddleware(
|
||||
async (auth, req) => {
|
||||
if (isProtectedRoute(req)) {
|
||||
if (!isUnprotectedRoute(req)) {
|
||||
console.log(`clerkMiddleware: PROTECTED -> ${req.nextUrl.pathname}`);
|
||||
await auth.protect();
|
||||
} else {
|
||||
console.log(`clerkMiddleware: UNPROTECTED -> ${req.nextUrl.pathname}`);
|
||||
}
|
||||
},
|
||||
{ debug: false },
|
||||
|
|
|
|||
|
|
@ -1,119 +0,0 @@
|
|||
@import 'tailwindcss';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue