Compare commits
170 Commits
@nhost/das
...
event-trig
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90db32fe96 | ||
|
|
2d5d8d10a9 | ||
|
|
4dad3fedca | ||
|
|
27e57061f7 | ||
|
|
dc6db00fb5 | ||
|
|
99fd1b1b90 | ||
|
|
49bdaea280 | ||
|
|
17725cb2cd | ||
|
|
a8c5a1f859 | ||
|
|
99ac1aee3a | ||
|
|
cb562b1609 | ||
|
|
8338650e34 | ||
|
|
b7daf8db84 | ||
|
|
490a4e5bc3 | ||
|
|
585f931391 | ||
|
|
ab3f103cec | ||
|
|
191f791aff | ||
|
|
bb9aaf2903 | ||
|
|
7363010934 | ||
|
|
a5cfb7c173 | ||
|
|
fdb6d8c719 | ||
|
|
9ce344522d | ||
|
|
654a1b3ea1 | ||
|
|
89dcf25f44 | ||
|
|
9626b4118e | ||
|
|
93394e7db6 | ||
|
|
2643c19f31 | ||
|
|
42e3e87fa5 | ||
|
|
cd1b65e72b | ||
|
|
21f1194317 | ||
|
|
3351e4dd7d | ||
|
|
ea608baa07 | ||
|
|
0e890f8c3b | ||
|
|
d3fee904cb | ||
|
|
60272df2fe | ||
|
|
0d1dcf11af | ||
|
|
05b0e6f57b | ||
|
|
87388e4c6d | ||
|
|
f07959bba7 | ||
|
|
2fd9cf27e6 | ||
|
|
a8b406d323 | ||
|
|
5689ca1faa | ||
|
|
98489335ab | ||
|
|
282917ff56 | ||
|
|
1685550f41 | ||
|
|
f0540a4510 | ||
|
|
07a125487a | ||
|
|
d4a2202693 | ||
|
|
1e4305e1af | ||
|
|
4971c15418 | ||
|
|
02398c1020 | ||
|
|
e3a3c4f1fb | ||
|
|
4379f13e82 | ||
|
|
9258527f2d | ||
|
|
e679cc6d73 | ||
|
|
8462ba40ef | ||
|
|
c1986e0f1e | ||
|
|
ed40f538c2 | ||
|
|
5e27742d70 | ||
|
|
76d0567680 | ||
|
|
bad25212a4 | ||
|
|
b4583e1a77 | ||
|
|
cf7d9f1a51 | ||
|
|
8ede558225 | ||
|
|
641029cde9 | ||
|
|
77ca6d656d | ||
|
|
eab8366e9e | ||
|
|
ccc8e76c11 | ||
|
|
7e186fb3f8 | ||
|
|
e5f5526afa | ||
|
|
7743e3e1b1 | ||
|
|
34e6e1c2c2 | ||
|
|
69661059f7 | ||
|
|
a923635b30 | ||
|
|
a973eae6e4 | ||
|
|
35be8ffd4d | ||
|
|
1cfc0f32c4 | ||
|
|
41f1be2f6d | ||
|
|
8e82edd0c6 | ||
|
|
4cb4a14f5b | ||
|
|
5861996fc6 | ||
|
|
009a5d38a7 | ||
|
|
621c8faaae | ||
|
|
4c4fc82074 | ||
|
|
b0256da33f | ||
|
|
351daa5fbe | ||
|
|
f0e3e76818 | ||
|
|
2b9801d8d3 | ||
|
|
eed0c45c93 | ||
|
|
6e11788c0d | ||
|
|
d6889c762d | ||
|
|
1102f586c3 | ||
|
|
4e9de6a764 | ||
|
|
9dab347348 | ||
|
|
6c044458ca | ||
|
|
c8e12ad96c | ||
|
|
45888bbf23 | ||
|
|
22879ab328 | ||
|
|
481726ed1e | ||
|
|
0ab2dd08ab | ||
|
|
9e8ad786ce | ||
|
|
5b98f4ece2 | ||
|
|
dc7d3fb4cc | ||
|
|
d15717b67a | ||
|
|
a3c7f89eda | ||
|
|
6692e0dfc0 | ||
|
|
24bba56e02 | ||
|
|
8541165781 | ||
|
|
5cd5ebbc65 | ||
|
|
1477d24319 | ||
|
|
5066ef708a | ||
|
|
edd8544447 | ||
|
|
a6a378c5a6 | ||
|
|
a3a3cf205d | ||
|
|
25bf8dda2f | ||
|
|
04e5365bb2 | ||
|
|
985157c60e | ||
|
|
0c457b683a | ||
|
|
0a0adb048d | ||
|
|
abab84474b | ||
|
|
e218fb1f4c | ||
|
|
eadf14203b | ||
|
|
c3fcb91244 | ||
|
|
b1cf1dc827 | ||
|
|
a4d0e37955 | ||
|
|
bac90afe37 | ||
|
|
f110d262a6 | ||
|
|
f20ef4117c | ||
|
|
30dede9ea0 | ||
|
|
f1c8059e8e | ||
|
|
1248fdafde | ||
|
|
3f09199f1f | ||
|
|
d84795ed3f | ||
|
|
3dab7b9146 | ||
|
|
78cfb90098 | ||
|
|
509d4f716a | ||
|
|
4f90ef18c5 | ||
|
|
29c9abd6c3 | ||
|
|
64bcf8b23a | ||
|
|
86e5bc2bf6 | ||
|
|
ab787cfc8c | ||
|
|
9e429a7310 | ||
|
|
156602392e | ||
|
|
ce2c8768c1 | ||
|
|
9d44850fb5 | ||
|
|
934de7bd10 | ||
|
|
7d45845500 | ||
|
|
a2f4a83ac3 | ||
|
|
0ccc471787 | ||
|
|
0109ddee6c | ||
|
|
12ff139212 | ||
|
|
804e0373b0 | ||
|
|
2f8cb6fcc8 | ||
|
|
4871d71eba | ||
|
|
244604fd83 | ||
|
|
eac4633434 | ||
|
|
e27b83a0f4 | ||
|
|
f9a66720cd | ||
|
|
51f32536ff | ||
|
|
dc4a6a3caa | ||
|
|
46a9de4ace | ||
|
|
6a650a0b67 | ||
|
|
4f3c1d5f9f | ||
|
|
3fd2e63db3 | ||
|
|
f5956f1b2e | ||
|
|
f3b397b0d8 | ||
|
|
b7940087ee | ||
|
|
3dae655858 | ||
|
|
2aa269734b | ||
|
|
bc91836f83 |
2
.github/workflows/dashboard_wf_release.yaml
vendored
2
.github/workflows/dashboard_wf_release.yaml
vendored
@@ -88,7 +88,7 @@ jobs:
|
||||
- name: Bump version in source code
|
||||
run: |
|
||||
find cli -type f -exec sed -i 's/"nhost\/dashboard:[^"]*"/"nhost\/dashboard:${{ inputs.VERSION }}"/g' {} +
|
||||
sed -i 's/"nhost\/dashboard:[^"]*"/"nhost\/dashboard:${{ inputs.VERSION }}"/g' docs/reference/cli/commands.mdx
|
||||
sed -i 's/nhost\/dashboard:[^)]*/nhost\/dashboard:${{ inputs.VERSION }}/g' docs/reference/cli/commands.mdx
|
||||
|
||||
- name: "Create Pull Request"
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
|
||||
2
.github/workflows/gen_ai_review.yaml
vendored
2
.github/workflows/gen_ai_review.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
id: pragent
|
||||
uses: Codium-ai/pr-agent@v0.30
|
||||
uses: Codium-ai/pr-agent@v0.31
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
|
||||
@@ -49,4 +49,4 @@ This repository is a monorepo that contains multiple packages and applications.
|
||||
- `tools/codegen` - Internal code generation tool to build the SDK
|
||||
- `tools/mintlify-openapi` - Internal tool to generate reference documentation for Mintlify from an OpenAPI spec.
|
||||
|
||||
For details about those projects and how to contribure, please refer to their respective `README.md` and `CONTRIBUTING.md` files.
|
||||
For details about those projects and how to contribute, please refer to their respective `README.md` and `CONTRIBUTING.md` files.
|
||||
|
||||
@@ -107,6 +107,7 @@ Nhost is frontend agnostic, which means Nhost works with all frontend frameworks
|
||||
# Resources
|
||||
|
||||
- Start developing locally with the [Nhost CLI](https://docs.nhost.io/platform/cli/local-development)
|
||||
|
||||
## Nhost Clients
|
||||
|
||||
- [JavaScript/TypeScript](https://docs.nhost.io/reference/javascript/nhost-js/main)
|
||||
@@ -137,7 +138,7 @@ Here are some ways of contributing to making Nhost better:
|
||||
|
||||
- **[Try out Nhost](https://docs.nhost.io)**, and think of ways to make the service better. Let us know here on GitHub.
|
||||
- Join our [Discord](https://discord.com/invite/9V7Qb2U) and connect with other members to share and learn from.
|
||||
- Send a pull request to any of our [open source repositories](https://github.com/nhost) on Github. Check our [contribution guide](https://github.com/nhost/nhost/blob/main/CONTRIBUTING.md) and our [developers guide](https://github.com/nhost/nhost/blob/main/DEVELOPERS.md) for more details about how to contribute. We're looking forward to your contribution!
|
||||
- Send a pull request to any of our [open source repositories](https://github.com/nhost) on Github. Check out our [contribution guide](https://github.com/nhost/nhost/blob/main/CONTRIBUTING.md) for more details about how to contribute. We're looking forward to your contribution!
|
||||
|
||||
### Contributors
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
|
||||
"moderate": true,
|
||||
"allowlist": [
|
||||
"GHSA-9965-vmph-33xx", // https://github.com/advisories/GHSA-9965-vmph-33xx Update package once have a fix
|
||||
"GHSA-7mvr-c777-76hp" // https://github.com/advisories/GHSA-7mvr-c777-76hp Update package once Nix side is also updated
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,28 @@
|
||||
## [cli@1.34.7] - 2025-11-13
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(cli)* Bump nhost/dashboard to 2.42.0 (#3693)
|
||||
|
||||
## [cli@1.34.6] - 2025-11-13
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(cli)* Mcp: specify items type for arrays in tools (#3687)
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(cli)* Update bindings (#3689)
|
||||
|
||||
## [cli@1.34.5] - 2025-11-06
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(nixops)* Bump go to 1.25.3 and nixpkgs due to CVEs (#3652)
|
||||
- *(cli)* Udpate certs and schema (#3675)
|
||||
- *(cli)* Bump nhost/dashboard to 2.41.0 (#3669)
|
||||
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
@@ -56,7 +56,7 @@ func CommandCloud() *cli.Command {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagDashboardVersion,
|
||||
Usage: "Dashboard version to use",
|
||||
Value: "nhost/dashboard:2.40.0",
|
||||
Value: "nhost/dashboard:2.42.0",
|
||||
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
|
||||
@@ -111,7 +111,7 @@ func CommandUp() *cli.Command { //nolint:funlen
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagDashboardVersion,
|
||||
Usage: "Dashboard version to use",
|
||||
Value: "nhost/dashboard:2.40.0",
|
||||
Value: "nhost/dashboard:2.42.0",
|
||||
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
|
||||
@@ -196,10 +196,12 @@ config validate after making changes to your nhost.toml file to ensure it is val
|
||||
"mutations": map[string]any{
|
||||
"description": string("list of mutations to fetch"),
|
||||
"type": string("array"),
|
||||
"items": map[string]any{"type": string("string")},
|
||||
},
|
||||
"queries": map[string]any{
|
||||
"description": string("list of queries to fetch"),
|
||||
"type": string("array"),
|
||||
"items": map[string]any{"type": string("string")},
|
||||
},
|
||||
"summary": map[string]any{
|
||||
"default": bool(true),
|
||||
|
||||
@@ -53,6 +53,7 @@ func expectedAuth() *Service {
|
||||
"AUTH_PROVIDER_APPLE_ENABLED": "true",
|
||||
"AUTH_PROVIDER_APPLE_KEY_ID": "appleKeyId",
|
||||
"AUTH_PROVIDER_APPLE_PRIVATE_KEY": "applePrivateKey",
|
||||
"AUTH_PROVIDER_APPLE_SCOPE": "",
|
||||
"AUTH_PROVIDER_APPLE_TEAM_ID": "appleTeamId",
|
||||
"AUTH_PROVIDER_AZUREAD_CLIENT_ID": "azureadClientId",
|
||||
"AUTH_PROVIDER_AZUREAD_CLIENT_SECRET": "azureadClientSecret",
|
||||
@@ -75,9 +76,12 @@ func expectedAuth() *Service {
|
||||
"AUTH_PROVIDER_FACEBOOK_CLIENT_SECRET": "facebookClientSecret",
|
||||
"AUTH_PROVIDER_FACEBOOK_ENABLED": "true",
|
||||
"AUTH_PROVIDER_FACEBOOK_SCOPE": "email",
|
||||
"AUTH_PROVIDER_GITHUB_AUDIENCE": "audience",
|
||||
"AUTH_PROVIDER_GITHUB_CLIENT_ID": "githubClientId",
|
||||
"AUTH_PROVIDER_GITHUB_CLIENT_SECRET": "githubClientSecret",
|
||||
"AUTH_PROVIDER_GITHUB_ENABLED": "true",
|
||||
"AUTH_PROVIDER_GITHUB_SCOPE": "user:email",
|
||||
"AUTH_PROVIDER_GITLAB_AUDIENCE": "audience",
|
||||
"AUTH_PROVIDER_GITLAB_CLIENT_ID": "gitlabClientId",
|
||||
"AUTH_PROVIDER_GITLAB_CLIENT_SECRET": "gitlabClientSecret",
|
||||
"AUTH_PROVIDER_GITLAB_ENABLED": "true",
|
||||
@@ -97,6 +101,7 @@ func expectedAuth() *Service {
|
||||
"AUTH_PROVIDER_SPOTIFY_CLIENT_SECRET": "spotifyClientSecret",
|
||||
"AUTH_PROVIDER_SPOTIFY_ENABLED": "true",
|
||||
"AUTH_PROVIDER_SPOTIFY_SCOPE": "user-read-email",
|
||||
"AUTH_PROVIDER_STRAVA_AUDIENCE": "audience",
|
||||
"AUTH_PROVIDER_STRAVA_CLIENT_ID": "stravaClientId",
|
||||
"AUTH_PROVIDER_STRAVA_CLIENT_SECRET": "stravaClientSecret",
|
||||
"AUTH_PROVIDER_STRAVA_ENABLED": "true",
|
||||
|
||||
@@ -223,7 +223,7 @@ import (
|
||||
// Releases:
|
||||
//
|
||||
// https://github.com/nhost/hasura-storage/releases
|
||||
version: string | *"0.8.2"
|
||||
version: string | *"0.9.1"
|
||||
|
||||
// Networking (custom domains at the moment) are not allowed as we need to do further
|
||||
// configurations in the CDN. We will enable it again in the future.
|
||||
@@ -311,7 +311,7 @@ import (
|
||||
// Releases:
|
||||
//
|
||||
// https://github.com/nhost/hasura-auth/releases
|
||||
version: string | *"0.42.4"
|
||||
version: string | *"0.43.0"
|
||||
|
||||
// Resources for the service
|
||||
resources?: #Resources
|
||||
|
||||
@@ -68,10 +68,12 @@ func (t *Tool) Register(mcpServer *server.MCPServer) {
|
||||
),
|
||||
mcp.WithArray(
|
||||
"queries",
|
||||
mcp.WithStringItems(),
|
||||
mcp.Description("list of queries to fetch"),
|
||||
),
|
||||
mcp.WithArray(
|
||||
"mutations",
|
||||
mcp.WithStringItems(),
|
||||
mcp.Description("list of mutations to fetch"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2247,6 +2247,14 @@ type AuthUserProvidersMinOrderBy struct {
|
||||
ProviderID *OrderBy `json:"providerId,omitempty"`
|
||||
}
|
||||
|
||||
// response of any mutation on the table "auth.user_providers"
|
||||
type AuthUserProvidersMutationResponse struct {
|
||||
// number of rows affected by the mutation
|
||||
AffectedRows int64 `json:"affected_rows"`
|
||||
// data from the rows affected by the mutation
|
||||
Returning []*AuthUserProviders `json:"returning"`
|
||||
}
|
||||
|
||||
// Ordering options when selecting data from "auth.user_providers".
|
||||
type AuthUserProvidersOrderBy struct {
|
||||
ID *OrderBy `json:"id,omitempty"`
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIERDCCA8mgAwIBAgISBmRex3kpZ4Mz1/1kq05iqja/MAoGCCqGSM49BAMDMDIx
|
||||
MIIERTCCA8ugAwIBAgISBWD/E+b14mP5jv4DGWRVYv8fMAoGCCqGSM49BAMDMDIx
|
||||
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
|
||||
ODAeFw0yNTEwMDIxMDUxNDBaFw0yNTEyMzExMDUxMzlaMB8xHTAbBgNVBAMTFGxv
|
||||
Y2FsLmF1dGgubmhvc3QucnVuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2cVM
|
||||
ojf8iXZGLneNfnke5LMJIxyTEeGbNOfCv4SOR4K/N4OkpvkUVbH2bRvX99uE9jaK
|
||||
515Y48PzPA/4+W1zTKOCAtAwggLMMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAU
|
||||
BggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUQqan
|
||||
raZoU5klAxsgkEVEMIkxmMQwHwYDVR0jBBgwFoAUjw0TovYuftFQbDMYOF1ZjiNy
|
||||
ODAeFw0yNTExMDYxMDUxMTBaFw0yNjAyMDQxMDUxMDlaMB8xHTAbBgNVBAMTFGxv
|
||||
Y2FsLmF1dGgubmhvc3QucnVuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOah5
|
||||
ZLuUQp3pdMBxBWnT6E6/amW9LerKKEEdy3Nc8iAwG9LlnPH0z3m7a9wgEhpFEdlL
|
||||
Rr+qO+NhSRnv6+UF5KOCAtIwggLOMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAU
|
||||
BggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUGyb1
|
||||
TVK/0vf3uHO4x3R094aG2rEwHwYDVR0jBBgwFoAUjw0TovYuftFQbDMYOF1ZjiNy
|
||||
kcowMgYIKwYBBQUHAQEEJjAkMCIGCCsGAQUFBzAChhZodHRwOi8vZTguaS5sZW5j
|
||||
ci5vcmcvMIHOBgNVHREEgcYwgcOCFGxvY2FsLmF1dGgubmhvc3QucnVughlsb2Nh
|
||||
bC5kYXNoYm9hcmQubmhvc3QucnVughJsb2NhbC5kYi5uaG9zdC5ydW6CGWxvY2Fs
|
||||
LmZ1bmN0aW9ucy5uaG9zdC5ydW6CF2xvY2FsLmdyYXBocWwubmhvc3QucnVughZs
|
||||
b2NhbC5oYXN1cmEubmhvc3QucnVughdsb2NhbC5tYWlsaG9nLm5ob3N0LnJ1boIX
|
||||
bG9jYWwuc3RvcmFnZS5uaG9zdC5ydW4wEwYDVR0gBAwwCjAIBgZngQwBAgEwLQYD
|
||||
VR0fBCYwJDAioCCgHoYcaHR0cDovL2U4LmMubGVuY3Iub3JnLzY0LmNybDCCAQIG
|
||||
CisGAQQB1nkCBAIEgfMEgfAA7gB1AO08S9boBsKkogBX28sk4jgB31Ev7cSGxXAP
|
||||
IN23Pj/gAAABmaTCI4YAAAQDAEYwRAIgXLRFL1EAXfvN6kd5m6udqlxfz4+5B6rq
|
||||
Cdhp/ZwDAZ8CIFYvalTkl5NEBEMD3vpPvrj8s1Yy2xsropEh/AvpavvLAHUAGYbU
|
||||
xyiqb/66A294Kk0BkarOLXIxD67OXXBBLSVMx9QAAAGZpMIjhwAABAMARjBEAiBk
|
||||
H1vqU9HNuBcf4UYL/xZ42BeUAARHStiFaIZtnR1kEgIgbIJ0CGqIpxmWuwCunl9p
|
||||
ar+rGLdQrCk9BZXq/VjPPAAwCgYIKoZIzj0EAwMDaQAwZgIxAKvk5a2zQsv7JLNj
|
||||
NO1ly+DI8qiy5nf4HQrOrHOjtmx5RUu0HSO9P0J0u069qAqXMgIxAMLdME9JUo2c
|
||||
TJo3pwWv5MRyg/MkOJ4ImKdDJXfIZNkEIUyP3vwTqImvZe07gJDsYg==
|
||||
VR0fBCYwJDAioCCgHoYcaHR0cDovL2U4LmMubGVuY3Iub3JnLzMyLmNybDCCAQQG
|
||||
CisGAQQB1nkCBAIEgfUEgfIA8AB2ABmG1Mcoqm/+ugNveCpNAZGqzi1yMQ+uzl1w
|
||||
QS0lTMfUAAABmlkAQokAAAQDAEcwRQIgWDtSxJfM2xcjvScVHOkn8bipzBhNhTnm
|
||||
B89TDh1/4XUCIQDe08W33PCx2D+akCdW9U9mZKQpIW6deLZSI3ZWpSNKMAB2AA5X
|
||||
lLzzrqk+MxssmQez95Dfm8I9cTIl3SGpJaxhxU4hAAABmlkAQn8AAAQDAEcwRQIg
|
||||
KnojmNTpNk1OFTQI0EnlPa2bpwqmUgmUCLeqE6SWfgoCIQCrhZbxYPHbGLF/HpRq
|
||||
vCTcOh24SRCuxlkqtaowbbfmKjAKBggqhkjOPQQDAwNoADBlAjEArstFIC+KAsfQ
|
||||
nLhtqsaNzkhftN5adDyr2CoE0WUPF1sLDi+xDnDO+JgIPL0YKAFNAjATJ4omhpc+
|
||||
I6/kWcef2RyO9YCGQQE9pdez5CYKb9o8YAntDSHM3b5nXXj3AX/USdQ=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEVjCCAj6gAwIBAgIQY5WTY8JOcIJxWRi/w9ftVjANBgkqhkiG9w0BAQsFADBP
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgfJZOkvawA0vBMw9W
|
||||
ph8i1Z+SJQrFscPbqSYpxngzEDahRANCAATZxUyiN/yJdkYud41+eR7kswkjHJMR
|
||||
4Zs058K/hI5Hgr83g6Sm+RRVsfZtG9f324T2NornXljjw/M8D/j5bXNM
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgInXN4JRnXNTjx7rM
|
||||
avurZrN1EV1iebQeNUlMlFp7VJ+hRANCAAQ5qHlku5RCnel0wHEFadPoTr9qZb0t
|
||||
6sooQR3Lc1zyIDAb0uWc8fTPebtr3CASGkUR2UtGv6o742FJGe/r5QXk
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEWDCCA96gAwIBAgISBbvrSsjDQm4zevwwjxFGmeTMMAoGCCqGSM49BAMDMDIx
|
||||
MIIEVzCCA92gAwIBAgISBm54VdkoqD8s8efq7ceHaTihMAoGCCqGSM49BAMDMDIx
|
||||
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
|
||||
NzAeFw0yNTEwMDIxMDUyNTdaFw0yNTEyMzExMDUyNTZaMCExHzAdBgNVBAMMFiou
|
||||
YXV0aC5sb2NhbC5uaG9zdC5ydW4wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATG
|
||||
x0o7t0pSrOoFc+pljtqJVxgaSW+w9D9C2WdysMeSKKOU+0MzaM4ynLUhETOpBs8E
|
||||
612mdcoeak+G1Emj6UVwo4IC4zCCAt8wDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQW
|
||||
MBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQ+
|
||||
lVsLiXSRLAECs9OgkCEBS7jMmzAfBgNVHSMEGDAWgBSuSJ7chx1EoG/aouVgdAR4
|
||||
wpwAgDAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly9lNy5pLmxl
|
||||
ODAeFw0yNTExMDYxMDUyMjBaFw0yNjAyMDQxMDUyMTlaMCExHzAdBgNVBAMMFiou
|
||||
YXV0aC5sb2NhbC5uaG9zdC5ydW4wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASI
|
||||
rTkZOM4ip42DCyDADXGc7oV3+OkimyTM3st2RIZWG28rFRwH0LebJV2cduq1Hdtl
|
||||
VxIEr+RhvyIL7gllueXUo4IC4jCCAt4wDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQW
|
||||
MBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTw
|
||||
bM86O381+aljU3oTUvwhZ90PCDAfBgNVHSMEGDAWgBSPDROi9i5+0VBsMxg4XVmO
|
||||
I3KRyjAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly9lOC5pLmxl
|
||||
bmNyLm9yZy8wgd4GA1UdEQSB1jCB04IWKi5hdXRoLmxvY2FsLm5ob3N0LnJ1boIb
|
||||
Ki5kYXNoYm9hcmQubG9jYWwubmhvc3QucnVughQqLmRiLmxvY2FsLm5ob3N0LnJ1
|
||||
boIbKi5mdW5jdGlvbnMubG9jYWwubmhvc3QucnVughkqLmdyYXBocWwubG9jYWwu
|
||||
bmhvc3QucnVughgqLmhhc3VyYS5sb2NhbC5uaG9zdC5ydW6CGSoubWFpbGhvZy5s
|
||||
b2NhbC5uaG9zdC5ydW6CGSouc3RvcmFnZS5sb2NhbC5uaG9zdC5ydW4wEwYDVR0g
|
||||
BAwwCjAIBgZngQwBAgEwLQYDVR0fBCYwJDAioCCgHoYcaHR0cDovL2U3LmMubGVu
|
||||
Y3Iub3JnLzc3LmNybDCCAQUGCisGAQQB1nkCBAIEgfYEgfMA8QB2AN3cyjSV1+EW
|
||||
BeeVMvrHn/g9HFDf2wA6FBJ2Ciysu8gqAAABmaTDUHkAAAQDAEcwRQIgWudJ8XKA
|
||||
BT5jq5Tl0xQLNb953pBi22Tb0TIWk+RSqHgCIQDsTrLVMFaQTV7EFCY1tFhi5qae
|
||||
SCpEwwdFcnom/nz6EAB3AO08S9boBsKkogBX28sk4jgB31Ev7cSGxXAPIN23Pj/g
|
||||
AAABmaTDWAsAAAQDAEgwRgIhALxIgIiutEwgNcGw7/cAdjFqUugct4HlZezIOLLP
|
||||
rg69AiEA8YCaK41rJDYztEKUIJEq2J2ktSqGYcl9gNKC+SiR4acwCgYIKoZIzj0E
|
||||
AwMDaAAwZQIwVG9yOiMRfKFFyFj1R8X/5U67QD84OhZ0oM0SZsVhezLedG5b8eFf
|
||||
/cWraREi8xbFAjEA/6RXweGzl08F7EtqBDoiqitScI2rbwGtP6s/evL0zXTABZD2
|
||||
ih7AGxjtg80IqIRe
|
||||
BAwwCjAIBgZngQwBAgEwLQYDVR0fBCYwJDAioCCgHoYcaHR0cDovL2U4LmMubGVu
|
||||
Y3Iub3JnLzM0LmNybDCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB2AEmcm2neHXzs
|
||||
/DbezYdkprhbrwqHgBnRVVL76esp3fjDAAABmlkBVgkAAAQDAEcwRQIhANH6Ml3u
|
||||
IM4nAzwAIjIjBjn8EWbn1ZHfgwO+rlSo5rzpAiATPKE8Mx5LK1IayG5VCK1eCDyc
|
||||
rzt1HNbP9WSrpuHx+gB2ABmG1Mcoqm/+ugNveCpNAZGqzi1yMQ+uzl1wQS0lTMfU
|
||||
AAABmlkBVgcAAAQDAEcwRQIgIT/DhsIj9Aw7qf/2lknJCr907dEqC3/+QN3zlcOj
|
||||
iKoCIQCTguinYjJPZwU2dblaRQ2q7MTCMT2ZENExltxwYG3GzjAKBggqhkjOPQQD
|
||||
AwNoADBlAjEA5nFoNrLyeC079YpRvdah/HZIA/lUBh+LOo/NcEBD3aTGs2z8hU8z
|
||||
H4vMy3OnfQ9TAjBxigm7zE5/3CAcGoSOr/P0TL52nh+lO4SUVxcbKgYB8A2yo6o/
|
||||
kUkG7PiRB0uUpNw=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEVzCCAj+gAwIBAgIRAKp18eYrjwoiCWbTi7/UuqEwDQYJKoZIhvcNAQELBQAw
|
||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
|
||||
WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
|
||||
RW5jcnlwdDELMAkGA1UEAxMCRTcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARB6AST
|
||||
CFh/vjcwDMCgQer+VtqEkz7JANurZxLP+U9TCeioL6sp5Z8VRvRbYk4P1INBmbef
|
||||
QHJFHCxcSjKmwtvGBWpl/9ra8HW0QDsUaJW2qOJqceJ0ZVFT3hbUHifBM/2jgfgw
|
||||
gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
|
||||
ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSuSJ7chx1EoG/aouVgdAR4
|
||||
wpwAgDAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB
|
||||
AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g
|
||||
BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu
|
||||
Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAjx66fDdLk5ywFn3CzA1w1qfylHUD
|
||||
aEf0QZpXcJseddJGSfbUUOvbNR9N/QQ16K1lXl4VFyhmGXDT5Kdfcr0RvIIVrNxF
|
||||
h4lqHtRRCP6RBRstqbZ2zURgqakn/Xip0iaQL0IdfHBZr396FgknniRYFckKORPG
|
||||
yM3QKnd66gtMst8I5nkRQlAg/Jb+Gc3egIvuGKWboE1G89NTsN9LTDD3PLj0dUMr
|
||||
OIuqVjLB8pEC6yk9enrlrqjXQgkLEYhXzq7dLafv5Vkig6Gl0nuuqjqfp0Q1bi1o
|
||||
yVNAlXe6aUXw92CcghC9bNsKEO1+M52YY5+ofIXlS/SEQbvVYYBLZ5yeiglV6t3S
|
||||
M6H+vTG0aP9YHzLn/KVOHzGQfXDP7qM5tkf+7diZe7o2fw6O7IvN6fsQXEQQj8TJ
|
||||
UXJxv2/uJhcuy/tSDgXwHM8Uk34WNbRT7zGTGkQRX0gsbjAea/jYAoWv0ZvQRwpq
|
||||
Pe79D/i7Cep8qWnA+7AE/3B3S/3dEEYmc0lpe1366A/6GEgk3ktr9PEoQrLChs6I
|
||||
tu3wnNLB2euC8IKGLQFpGtOO/2/hiAKjyajaBP25w1jF0Wl8Bbqne3uZ2q1GyPFJ
|
||||
YRmT7/OXpmOH/FVLtwS+8ng1cAmpCujPwteJZNcDG0sF2n/sc0+SQf49fdyUK0ty
|
||||
+VUwFj9tmWxyR/M=
|
||||
MIIEVjCCAj6gAwIBAgIQY5WTY8JOcIJxWRi/w9ftVjANBgkqhkiG9w0BAQsFADBP
|
||||
MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
|
||||
Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa
|
||||
Fw0yNzAzMTIyMzU5NTlaMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF
|
||||
bmNyeXB0MQswCQYDVQQDEwJFODB2MBAGByqGSM49AgEGBSuBBAAiA2IABNFl8l7c
|
||||
S7QMApzSsvru6WyrOq44ofTUOTIzxULUzDMMNMchIJBwXOhiLxxxs0LXeb5GDcHb
|
||||
R6EToMffgSZjO9SNHfY9gjMy9vQr5/WWOrQTZxh7az6NSNnq3u2ubT6HTKOB+DCB
|
||||
9TAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB
|
||||
MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFI8NE6L2Ln7RUGwzGDhdWY4j
|
||||
cpHKMB8GA1UdIwQYMBaAFHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEB
|
||||
BCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzATBgNVHSAE
|
||||
DDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veDEuYy5sZW5j
|
||||
ci5vcmcvMA0GCSqGSIb3DQEBCwUAA4ICAQBnE0hGINKsCYWi0Xx1ygxD5qihEjZ0
|
||||
RI3tTZz1wuATH3ZwYPIp97kWEayanD1j0cDhIYzy4CkDo2jB8D5t0a6zZWzlr98d
|
||||
AQFNh8uKJkIHdLShy+nUyeZxc5bNeMp1Lu0gSzE4McqfmNMvIpeiwWSYO9w82Ob8
|
||||
otvXcO2JUYi3svHIWRm3+707DUbL51XMcY2iZdlCq4Wa9nbuk3WTU4gr6LY8MzVA
|
||||
aDQG2+4U3eJ6qUF10bBnR1uuVyDYs9RhrwucRVnfuDj29CMLTsplM5f5wSV5hUpm
|
||||
Uwp/vV7M4w4aGunt74koX71n4EdagCsL/Yk5+mAQU0+tue0JOfAV/R6t1k+Xk9s2
|
||||
HMQFeoxppfzAVC04FdG9M+AC2JWxmFSt6BCuh3CEey3fE52Qrj9YM75rtvIjsm/1
|
||||
Hl+u//Wqxnu1ZQ4jpa+VpuZiGOlWrqSP9eogdOhCGisnyewWJwRQOqK16wiGyZeR
|
||||
xs/Bekw65vwSIaVkBruPiTfMOo0Zh4gVa8/qJgMbJbyrwwG97z/PRgmLKCDl8z3d
|
||||
tA0Z7qq7fta0Gl24uyuB05dqI5J1LvAzKuWdIjT1tP8qCoxSE/xpix8hX2dt3h+/
|
||||
jujUgFPFZ0EVZ0xSyBNRF3MboGZnYXFUxpNjTWPKpagDHJQmqrAcDmWJnMsFY3jS
|
||||
u1igv3OefnWjSQ==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrfNUSjLV/7j7LSBf
|
||||
zL/hvGEuv+uvf3/aimqjecO7vcShRANCAATGx0o7t0pSrOoFc+pljtqJVxgaSW+w
|
||||
9D9C2WdysMeSKKOU+0MzaM4ynLUhETOpBs8E612mdcoeak+G1Emj6UVw
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgcrhROXQT85e+S8h8
|
||||
RE3Z7TPo3+WA2RmzJsXJbXkbi5qhRANCAASIrTkZOM4ip42DCyDADXGc7oV3+Oki
|
||||
myTM3st2RIZWG28rFRwH0LebJV2cduq1HdtlVxIEr+RhvyIL7gllueXU
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
## [@nhost/dashboard@2.42.0] - 2025-11-12
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(dashboard)* Datatable design improvements (#3657)
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(dashboard)* Remove v2 ui components from datatable (#3568)
|
||||
|
||||
## [@nhost/dashboard@2.41.0] - 2025-11-04
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
@@ -102,7 +102,7 @@ test('should create a table with nullable columns', async ({
|
||||
page.getByRole('link', { name: tableName, exact: true }),
|
||||
).toBeVisible();
|
||||
await page
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
|
||||
.click();
|
||||
await page.getByText('Edit Table').click();
|
||||
await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
|
||||
@@ -143,7 +143,7 @@ test('should create a table with an identity column', async ({
|
||||
page.getByRole('link', { name: tableName, exact: true }),
|
||||
).toBeVisible();
|
||||
await page
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
|
||||
.click();
|
||||
await page.getByText('Edit Table').click();
|
||||
await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
|
||||
@@ -267,7 +267,7 @@ test('should be able to create a table with a composite key', async ({
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
|
||||
.click();
|
||||
await page.getByText('Edit Table').click();
|
||||
await expect(page.locator('div[data-testid="id"]')).toBeVisible();
|
||||
|
||||
@@ -41,7 +41,7 @@ test('should create a table with role permissions to select row', async ({
|
||||
|
||||
// Press three horizontal dots more options button next to the table name
|
||||
await page
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
|
||||
.click();
|
||||
|
||||
await page.getByRole('menuitem', { name: /edit permissions/i }).click();
|
||||
@@ -89,7 +89,7 @@ test('should create a table with role permissions and a custom check to select r
|
||||
|
||||
// Press three horizontal dots more options button next to the table name
|
||||
await page
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
|
||||
.click();
|
||||
|
||||
await page.getByRole('menuitem', { name: /edit permissions/i }).click();
|
||||
@@ -114,7 +114,7 @@ test('should create a table with role permissions and a custom check to select r
|
||||
|
||||
await page.getByText('Select variable...', { exact: true }).click();
|
||||
|
||||
const variableSelector = await page.locator('input[role="combobox"]');
|
||||
const variableSelector = page.locator('input[role="combobox"]');
|
||||
|
||||
await variableSelector.fill('X-Hasura-User-Id');
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"@graphiql/react": "^0.22.3",
|
||||
"@graphiql/toolkit": "^0.9.1",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
@@ -51,6 +52,7 @@
|
||||
"@radix-ui/react-accordion": "^1.2.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.3",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-hover-card": "^1.1.2",
|
||||
@@ -60,10 +62,10 @@
|
||||
"@radix-ui/react-radio-group": "^1.2.0",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@segment/analytics-next": "^1.77.0",
|
||||
"@simplewebauthn/browser": "^9.0.1",
|
||||
"@stripe/react-stripe-js": "^2.6.2",
|
||||
@@ -93,7 +95,7 @@
|
||||
"just-kebab-case": "^4.2.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lucide-react": "^0.416.0",
|
||||
"lucide-react": "^0.552.0",
|
||||
"next": "^14.2.31",
|
||||
"next-nprogress-bar": "^2.3.13",
|
||||
"next-seo": "^6.5.0",
|
||||
@@ -125,7 +127,7 @@
|
||||
"timezones-list": "^3.1.0",
|
||||
"utility-types": "^3.11.0",
|
||||
"uuid": "^9.0.1",
|
||||
"validator": "^13.11.0",
|
||||
"validator": "^13.15.20",
|
||||
"yup": "^1.4.0",
|
||||
"yup-password": "^0.2.2",
|
||||
"zod": "^3.23.8"
|
||||
@@ -231,7 +233,9 @@
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild@<=0.24.2": ">=0.25.0"
|
||||
"esbuild@<=0.24.2": ">=0.25.0",
|
||||
"js-yaml@<=4.1.0": ">=4.1.1",
|
||||
"glob@>=10.3.7 <=11.0.3": ">=11.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
576
dashboard/pnpm-lock.yaml
generated
576
dashboard/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1
dashboard/react-table-config.d.ts
vendored
1
dashboard/react-table-config.d.ts
vendored
@@ -64,7 +64,6 @@ declare module 'react-table' {
|
||||
|
||||
export interface Cell<
|
||||
D extends Record<string, unknown> = Record<string, unknown>,
|
||||
V = any,
|
||||
> extends UseGroupByCellProps<D>,
|
||||
UseRowStateCellProps<D> {}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/v3/alert-dialog';
|
||||
|
||||
interface DiscardChangesDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onDiscardChanges: () => void;
|
||||
}
|
||||
|
||||
export default function DiscardChangesDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onDiscardChanges,
|
||||
}: DiscardChangesDialogProps) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent className="text-foreground">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Unsaved changes</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You have unsaved local changes. Are you sure you want to discard
|
||||
them?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onDiscardChanges}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Discard
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DiscardChangesDialog } from './DiscardChangesDialog';
|
||||
@@ -0,0 +1,41 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
DragDropContext,
|
||||
Droppable,
|
||||
type DragDropContextProps,
|
||||
type DroppableProps,
|
||||
} from '@hello-pangea/dnd';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
type DragAndDropListProps = Omit<
|
||||
DroppableProps & DragDropContextProps,
|
||||
'children'
|
||||
> & {
|
||||
wrapperClassName?: string;
|
||||
};
|
||||
|
||||
function DragAndDropList({
|
||||
droppableId,
|
||||
onDragEnd,
|
||||
children,
|
||||
wrapperClassName,
|
||||
}: PropsWithChildren<DragAndDropListProps>) {
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId={droppableId}>
|
||||
{(provided) => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
className={cn(wrapperClassName)}
|
||||
>
|
||||
{children}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
}
|
||||
|
||||
export default DragAndDropList;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Draggable, type DraggableProps } from '@hello-pangea/dnd';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
export type DraggableItemProps = PropsWithChildren<
|
||||
Omit<DraggableProps, 'children'> & { className?: string }
|
||||
>;
|
||||
|
||||
function DraggableItem({
|
||||
children,
|
||||
className,
|
||||
...draggableProps
|
||||
}: DraggableItemProps) {
|
||||
return (
|
||||
<Draggable {...draggableProps}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className={cn(className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
export default DraggableItem;
|
||||
3
dashboard/src/components/common/DragAndDropList/index.ts
Normal file
3
dashboard/src/components/common/DragAndDropList/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as DragAndDropList } from './DragAndDropList';
|
||||
export * from './DraggableItem';
|
||||
export { default as DraggableItem } from './DraggableItem';
|
||||
@@ -123,7 +123,7 @@ const TimePickerInput = React.forwardRef<
|
||||
id={id || picker}
|
||||
name={name || picker}
|
||||
className={cn(
|
||||
'w-[48px] text-center font-mono text-base tabular-nums focus:bg-accent focus:text-accent-foreground [&::-webkit-inner-spin-button]:appearance-none',
|
||||
'w-[48px] text-center font-mono text-base tabular-nums focus:bg-accent-background focus:text-accent-foreground [&::-webkit-inner-spin-button]:appearance-none',
|
||||
className,
|
||||
)}
|
||||
value={value || calculatedValue}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { Spinner } from '@/components/ui/v3/spinner';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface FormActivityIndicatorProps extends BoxProps {}
|
||||
export interface FormActivityIndicatorProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function FormActivityIndicator({
|
||||
className,
|
||||
...props
|
||||
}: FormActivityIndicatorProps) {
|
||||
return (
|
||||
<Box
|
||||
<div
|
||||
{...props}
|
||||
className={twMerge(
|
||||
'grid items-center justify-center px-6 py-4',
|
||||
className={cn(
|
||||
'box grid h-full items-center justify-center px-6 py-4',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ActivityIndicator
|
||||
circularProgressProps={{ className: 'w-5 h-5' }}
|
||||
label="Loading form..."
|
||||
/>
|
||||
</Box>
|
||||
<Spinner className="h-5 w-5" wrapperClassName="flex-row gap-1">
|
||||
Loading form...
|
||||
</Spinner>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/v3/form';
|
||||
import { Input } from '@/components/ui/v3/input';
|
||||
import type { Control, FieldPath, FieldValues } from 'react-hook-form';
|
||||
import { Input, type InputProps } from '@/components/ui/v3/input';
|
||||
import { InfoTooltip } from '@/features/orgs/projects/common/components/InfoTooltip';
|
||||
import { cn, isNotEmptyValue } from '@/lib/utils';
|
||||
import {
|
||||
type ChangeEvent,
|
||||
type ForwardedRef,
|
||||
forwardRef,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import type {
|
||||
Control,
|
||||
ControllerRenderProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
PathValue,
|
||||
} from 'react-hook-form';
|
||||
import { mergeRefs } from 'react-merge-refs';
|
||||
|
||||
const inputClasses =
|
||||
'!bg-transparent aria-[invalid=true]:border-red-500 aria-[invalid=true]:focus:border-red-500 aria-[invalid=true]:focus:ring-red-500';
|
||||
@@ -17,43 +33,137 @@ interface FormInputProps<
|
||||
> {
|
||||
control: Control<TFieldValues>;
|
||||
name: TName;
|
||||
label: string;
|
||||
label: ReactNode;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
type?: string;
|
||||
inline?: boolean;
|
||||
helperText?: string | null;
|
||||
transformValue?: (
|
||||
value: PathValue<TFieldValues, TName>,
|
||||
) => PathValue<TFieldValues, TName>;
|
||||
disabled?: boolean;
|
||||
autoComplete?: InputProps['autoComplete'];
|
||||
infoTooltip?: string;
|
||||
}
|
||||
|
||||
function FormInput<
|
||||
function InnerFormInput<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
control,
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
className = '',
|
||||
type = 'text',
|
||||
}: FormInputProps<TFieldValues, TName>) {
|
||||
>(
|
||||
{
|
||||
control,
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
className = '',
|
||||
type = 'text',
|
||||
inline,
|
||||
helperText,
|
||||
transformValue,
|
||||
disabled,
|
||||
autoComplete,
|
||||
infoTooltip,
|
||||
}: FormInputProps<TFieldValues, TName>,
|
||||
ref: ForwardedRef<HTMLInputElement>,
|
||||
) {
|
||||
function getOnChangeHandlerAndValue(
|
||||
field: ControllerRenderProps<TFieldValues, TName>,
|
||||
): [
|
||||
PathValue<TFieldValues, TName>,
|
||||
(e: ChangeEvent<HTMLInputElement>) => void,
|
||||
] {
|
||||
const { onChange, value } = field;
|
||||
|
||||
function handleOnChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
let transformedValue = event.target.value;
|
||||
if (isNotEmptyValue(transformValue)) {
|
||||
transformedValue = transformValue(
|
||||
event.target.value as PathValue<TFieldValues, TName>,
|
||||
);
|
||||
}
|
||||
onChange(transformedValue);
|
||||
}
|
||||
|
||||
const transformedValue = isNotEmptyValue(transformValue)
|
||||
? transformValue(value)
|
||||
: value;
|
||||
|
||||
return [transformedValue, handleOnChange];
|
||||
}
|
||||
return (
|
||||
<FormField
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type={type}
|
||||
placeholder={placeholder || label}
|
||||
{...field}
|
||||
className={`${inputClasses} ${className}`}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
const { onChange, value, ...fieldProps } = field;
|
||||
|
||||
const [tValue, handleOnChange] = getOnChangeHandlerAndValue(field);
|
||||
return (
|
||||
<FormItem
|
||||
className={cn({ 'flex w-full items-center gap-4 py-3': inline })}
|
||||
>
|
||||
{infoTooltip ? (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<FormLabel
|
||||
className={cn({
|
||||
'mt-2 w-52 max-w-52 flex-shrink-0 self-start': inline,
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
<InfoTooltip>{infoTooltip}</InfoTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<FormLabel
|
||||
className={cn({
|
||||
'mt-2 w-52 max-w-52 flex-shrink-0 self-start': inline,
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
)}
|
||||
<div
|
||||
className={cn({
|
||||
'flex w-[calc(100%-13.5rem)] max-w-[calc(100%-13.5rem)] flex-col gap-2':
|
||||
inline,
|
||||
})}
|
||||
>
|
||||
<FormControl>
|
||||
<Input
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
onChange={handleOnChange}
|
||||
value={tValue}
|
||||
disabled={disabled}
|
||||
autoComplete={autoComplete}
|
||||
{...fieldProps}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
className={cn(inputClasses, className)}
|
||||
wrapperClassName={cn({ 'w-full': !inline })}
|
||||
/>
|
||||
</FormControl>
|
||||
{!!helperText && (
|
||||
<FormDescription className="break-all px-[1px]">
|
||||
{helperText}
|
||||
</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const FormInput = forwardRef(InnerFormInput) as <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>(
|
||||
props: FormInputProps<TFieldValues, TName> & {
|
||||
ref?: ForwardedRef<HTMLInputElement>;
|
||||
},
|
||||
) => ReturnType<typeof InnerFormInput>;
|
||||
|
||||
export default FormInput;
|
||||
|
||||
131
dashboard/src/components/form/FormSelect/FormSelect.tsx
Normal file
131
dashboard/src/components/form/FormSelect/FormSelect.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/v3/form';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/v3/select';
|
||||
import { cn, isNotEmptyValue } from '@/lib/utils';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import type {
|
||||
Control,
|
||||
ControllerRenderProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
PathValue,
|
||||
} from 'react-hook-form';
|
||||
|
||||
const selectClasses =
|
||||
'aria-[invalid=true]:border-red-500 aria-[invalid=true]:focus:border-red-500 aria-[invalid=true]:focus:ring-red-500';
|
||||
|
||||
interface FormSelectProps<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> {
|
||||
control: Control<TFieldValues>;
|
||||
name: TName;
|
||||
label: ReactNode;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
inline?: boolean;
|
||||
helperText?: string | null;
|
||||
disabled?: boolean;
|
||||
transformValue?: (
|
||||
value: PathValue<TFieldValues, TName>,
|
||||
) => PathValue<TFieldValues, TName>;
|
||||
}
|
||||
|
||||
function FormSelect<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
control,
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
className = '',
|
||||
inline,
|
||||
helperText,
|
||||
disabled,
|
||||
children,
|
||||
transformValue,
|
||||
}: PropsWithChildren<FormSelectProps<TFieldValues, TName>>) {
|
||||
function getOnChangeHandlerAndValue(
|
||||
field: ControllerRenderProps<TFieldValues, TName>,
|
||||
): [string, (v: string) => void] {
|
||||
const { onChange, value } = field;
|
||||
|
||||
function handleOnChange(newValue: string) {
|
||||
const transformedNewValue = isNotEmptyValue(transformValue)
|
||||
? transformValue(newValue as PathValue<TFieldValues, TName>)
|
||||
: newValue;
|
||||
|
||||
onChange(transformedNewValue);
|
||||
}
|
||||
|
||||
const transformedValue: string = isNotEmptyValue(transformValue)
|
||||
? transformValue(value as PathValue<TFieldValues, TName>)
|
||||
: value;
|
||||
|
||||
return [transformedValue, handleOnChange];
|
||||
}
|
||||
return (
|
||||
<FormField
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field }) => {
|
||||
const { onChange, value, ...selectProps } = field;
|
||||
const [tValue, handleOnChange] = getOnChangeHandlerAndValue(field);
|
||||
return (
|
||||
<FormItem
|
||||
className={cn({ 'flex w-full items-center gap-4 py-3': inline })}
|
||||
>
|
||||
<FormLabel
|
||||
className={cn({
|
||||
'w-52 max-w-52 flex-shrink-0': inline,
|
||||
'mt-2 self-start': inline && !!helperText,
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
<div
|
||||
className={cn({
|
||||
'flex w-[calc(100%-13.5rem)] max-w-[calc(100%-13.5rem)] flex-col gap-2':
|
||||
inline,
|
||||
})}
|
||||
>
|
||||
<Select
|
||||
onValueChange={handleOnChange}
|
||||
value={tValue}
|
||||
disabled={disabled}
|
||||
{...selectProps}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className={cn(selectClasses, className)}>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>{children}</SelectContent>
|
||||
</Select>
|
||||
{!!helperText && (
|
||||
<FormDescription className="break-all px-[1px]">
|
||||
{helperText}
|
||||
</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default FormSelect;
|
||||
1
dashboard/src/components/form/FormSelect/index.ts
Normal file
1
dashboard/src/components/form/FormSelect/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as FormSelect } from './FormSelect';
|
||||
97
dashboard/src/components/form/FormTextarea/FormTextarea.tsx
Normal file
97
dashboard/src/components/form/FormTextarea/FormTextarea.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/v3/form';
|
||||
import { Textarea } from '@/components/ui/v3/textarea';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { forwardRef, type ForwardedRef, type ReactNode } from 'react';
|
||||
import type { Control, FieldPath, FieldValues } from 'react-hook-form';
|
||||
import { mergeRefs } from 'react-merge-refs';
|
||||
|
||||
const inputClasses =
|
||||
'!bg-transparent aria-[invalid=true]:border-red-500 aria-[invalid=true]:focus:border-red-500 aria-[invalid=true]:focus:ring-red-500';
|
||||
|
||||
interface FormTextareaProps<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> {
|
||||
control: Control<TFieldValues>;
|
||||
name: TName;
|
||||
label: ReactNode;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
inline?: boolean;
|
||||
helperText?: string | null;
|
||||
}
|
||||
|
||||
function InnerFormTextarea<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>(
|
||||
{
|
||||
control,
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
className = '',
|
||||
inline,
|
||||
helperText,
|
||||
}: FormTextareaProps<TFieldValues, TName>,
|
||||
ref: ForwardedRef<HTMLTextAreaElement>,
|
||||
) {
|
||||
return (
|
||||
<FormField
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem
|
||||
className={cn({ 'flex w-full items-center gap-4 py-3': inline })}
|
||||
>
|
||||
<FormLabel
|
||||
className={cn({
|
||||
'mt-2 w-52 max-w-52 flex-shrink-0 self-start': inline,
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
<div
|
||||
className={cn({
|
||||
'flex w-[calc(100%-13.5rem)] max-w-[calc(100%-13.5rem)] flex-col gap-2':
|
||||
inline,
|
||||
})}
|
||||
>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={placeholder}
|
||||
{...field}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
className={cn(inputClasses, className)}
|
||||
/>
|
||||
</FormControl>
|
||||
{!!helperText && (
|
||||
<FormDescription className="break-all px-[1px]">
|
||||
{helperText}
|
||||
</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const FormTextarea = forwardRef(InnerFormTextarea) as <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>(
|
||||
props: FormTextareaProps<TFieldValues, TName> & {
|
||||
ref: ForwardedRef<HTMLTextAreaElement>;
|
||||
},
|
||||
) => ReturnType<typeof InnerFormTextarea>;
|
||||
|
||||
export default FormTextarea;
|
||||
1
dashboard/src/components/form/FormTextarea/index.ts
Normal file
1
dashboard/src/components/form/FormTextarea/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as FormTextarea } from './FormTextarea';
|
||||
@@ -130,9 +130,12 @@ export default function AuthenticatedLayout({
|
||||
{withMainNav && mainNavPinned && isMdOrLarger && <PinnedMainNav />}
|
||||
|
||||
<div
|
||||
className={cn('relative flex h-full w-full flex-row bg-accent', {
|
||||
'overflow-x-auto': mainNavPinned && isMdOrLarger && withMainNav,
|
||||
})}
|
||||
className={cn(
|
||||
'bg-accent-background relative flex h-full w-full flex-row',
|
||||
{
|
||||
'overflow-x-auto': mainNavPinned && isMdOrLarger && withMainNav,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{withMainNav && (!mainNavPinned || !isMdOrLarger) && (
|
||||
<div className="flex h-full w-6 justify-center">
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@/components/ui/v3/command';
|
||||
|
||||
import {
|
||||
CalendarDays,
|
||||
Check,
|
||||
ChevronsUpDown,
|
||||
CloudIcon,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
HomeIcon,
|
||||
RocketIcon,
|
||||
UserIcon,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { AIIcon } from '@/components/ui/v2/icons/AIIcon';
|
||||
@@ -81,6 +83,13 @@ export default function ProjectPagesComboBox() {
|
||||
slug: 'graphql',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: 'Events',
|
||||
value: 'events',
|
||||
icon: <Zap className="h-4 w-4" />,
|
||||
slug: 'events',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: 'Hasura',
|
||||
value: 'hasura',
|
||||
@@ -144,6 +153,13 @@ export default function ProjectPagesComboBox() {
|
||||
slug: 'metrics',
|
||||
disabled: !isPlatform,
|
||||
},
|
||||
{
|
||||
label: 'Events',
|
||||
value: 'events',
|
||||
icon: <CalendarDays className="h-4 w-4" />,
|
||||
slug: 'events',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
value: 'settings',
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Button } from '@/components/ui/v3/button';
|
||||
import { useOrgs, type Org } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { cn, isNotEmptyValue } from '@/lib/utils';
|
||||
import { getConfigServerUrl, isPlatform as getIsPlatform } from '@/utils/env';
|
||||
import { Box, ChevronDown, ChevronRight, Plus } from 'lucide-react';
|
||||
import { Box, ChevronDown, ChevronRight, Plus, Zap } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { type ReactElement } from 'react';
|
||||
|
||||
@@ -46,6 +46,12 @@ const projectPages = [
|
||||
route: 'graphql',
|
||||
slug: 'graphql',
|
||||
},
|
||||
{
|
||||
name: 'Events',
|
||||
icon: <Zap className="h-4 w-4" />,
|
||||
route: 'events',
|
||||
slug: 'events',
|
||||
},
|
||||
{
|
||||
name: 'Hasura',
|
||||
icon: <HasuraIcon className="h-4 w-4" />,
|
||||
@@ -157,6 +163,7 @@ const projectSettingsPages = [
|
||||
},
|
||||
{ name: 'AI', slug: 'ai', route: 'ai' },
|
||||
{ name: 'Observability', slug: 'metrics', route: 'metrics' },
|
||||
{ name: 'Events', slug: 'events', route: 'events' },
|
||||
{ name: 'Configuration Editor', slug: 'editor', route: 'editor' },
|
||||
];
|
||||
|
||||
@@ -482,9 +489,9 @@ export default function NavTree() {
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'flex h-8 w-full flex-row justify-start gap-1 bg-background px-1 text-foreground hover:bg-accent dark:hover:bg-muted',
|
||||
'flex h-8 w-full flex-row justify-start gap-1 bg-background px-1 text-foreground hover:bg-accent',
|
||||
{
|
||||
'bg-[#ebf3ff] hover:bg-[#ebf3ff] dark:bg-muted':
|
||||
'bg-[#ebf3ff] hover:bg-accent dark:bg-muted':
|
||||
context.isFocused,
|
||||
},
|
||||
item.data.disabled && 'pointer-events-none opacity-50',
|
||||
|
||||
@@ -100,7 +100,7 @@ export const CodeBlock = forwardRef(
|
||||
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.200',
|
||||
}}
|
||||
className={clsx(
|
||||
'not-prose relative mt-5 px-2',
|
||||
'not-prose relative mt-5 w-full min-w-0 max-w-full px-2',
|
||||
filename && 'pt-2',
|
||||
className,
|
||||
)}
|
||||
@@ -124,8 +124,8 @@ export const CodeBlock = forwardRef(
|
||||
className="absolute right-3 top-0"
|
||||
/>
|
||||
)}
|
||||
<pre className="overflow-x-auto">
|
||||
<code className="font-mono">{children}</code>
|
||||
<pre className="w-full max-w-full whitespace-pre-wrap break-words">
|
||||
<code className="break-all font-mono">{children}</code>
|
||||
</pre>
|
||||
</Box>
|
||||
),
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '@/components/ui/v3/hover-card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { HoverCardPortal } from '@radix-ui/react-hover-card';
|
||||
import { format } from 'date-fns';
|
||||
import { Copy } from 'lucide-react';
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
|
||||
type HoverCardContentProps = ComponentPropsWithoutRef<typeof HoverCardContent>;
|
||||
|
||||
interface HoverCardTimestampProps {
|
||||
date: Date;
|
||||
side?: HoverCardContentProps['side'];
|
||||
sideOffset?: HoverCardContentProps['sideOffset'];
|
||||
align?: HoverCardContentProps['align'];
|
||||
alignOffset?: HoverCardContentProps['alignOffset'];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Row({ value, label }: { value: string; label: string }) {
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
<div
|
||||
className="group flex items-center justify-between gap-4 text-sm"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copy(value, 'Timestamp');
|
||||
}}
|
||||
>
|
||||
<dt className="text-muted-foreground">{label}</dt>
|
||||
<dd className="flex items-center gap-1 truncate font-mono">
|
||||
<span className="invisible group-hover:visible">
|
||||
<Copy className="h-3 w-3" />
|
||||
</span>
|
||||
{value}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HoverCardTimestamp({
|
||||
date,
|
||||
side = 'right',
|
||||
align = 'start',
|
||||
alignOffset = -4,
|
||||
sideOffset,
|
||||
className,
|
||||
}: HoverCardTimestampProps) {
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
return (
|
||||
<HoverCard openDelay={0} closeDelay={150}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className={cn('whitespace-nowrap font-mono', className)}>
|
||||
{format(date, 'LLL dd, y HH:mm:ss')}
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent
|
||||
className="z-10 w-auto p-2"
|
||||
{...{ side, align, alignOffset, sideOffset }}
|
||||
>
|
||||
<dl className="flex flex-col gap-1">
|
||||
<Row value={String(date.getTime())} label="Timestamp" />
|
||||
<Row value={date.toISOString()} label="UTC" />
|
||||
<Row value={format(date, 'LLL dd, y HH:mm:ss')} label={timezone} />
|
||||
</dl>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as HoverCardTimestamp } from './HoverCardTimestamp';
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import type { TextProps } from '@/components/ui/v2/Text';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import type { DetailedHTMLProps, ForwardedRef, HTMLProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type {
|
||||
DetailedHTMLProps,
|
||||
ForwardedRef,
|
||||
HTMLAttributes,
|
||||
HTMLProps,
|
||||
} from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export type ReadOnlyToggleProps = Omit<
|
||||
DetailedHTMLProps<HTMLProps<HTMLSpanElement>, HTMLSpanElement>,
|
||||
@@ -24,7 +26,7 @@ export type ReadOnlyToggleProps = Omit<
|
||||
/**
|
||||
* Props passed to the label.
|
||||
*/
|
||||
label?: TextProps;
|
||||
label?: HTMLAttributes<HTMLSpanElement>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -36,58 +38,44 @@ function ReadOnlyToggle(
|
||||
<span
|
||||
{...props}
|
||||
{...(slotProps?.root || {})}
|
||||
className={twMerge(
|
||||
className={cn(
|
||||
'inline-grid h-full w-full grid-flow-col items-center justify-start gap-1.5',
|
||||
slotProps?.root?.className,
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
backgroundColor: (theme) => {
|
||||
if (checked) {
|
||||
return theme.palette.mode === 'dark' ? 'grey.400' : 'grey.700';
|
||||
}
|
||||
|
||||
return 'transparent';
|
||||
<span
|
||||
className={cn(
|
||||
'box-border inline-grid h-3 w-5 items-center rounded-full border-1 border-primary-text bg-transparent px-0.5',
|
||||
checked && 'justify-end',
|
||||
{
|
||||
'border-transparent bg-primary-text px-0.5 dark:bg-[#363a43]':
|
||||
checked,
|
||||
},
|
||||
borderColor: checked ? 'transparent' : 'grey.700',
|
||||
}}
|
||||
className={twMerge(
|
||||
'box-border inline-grid h-3 w-5 items-center rounded-full border-1 px-0.5',
|
||||
checked === true && 'justify-end',
|
||||
)}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
backgroundColor: (theme) => {
|
||||
if (checked) {
|
||||
return theme.palette.mode === 'dark' ? 'grey.700' : 'grey.200';
|
||||
}
|
||||
|
||||
return 'grey.700';
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block h-2 w-2 rounded-full border-primary-text bg-primary-text',
|
||||
{
|
||||
'border-transparent bg-data-cell-bg px-0.5 dark:bg-[#f4f7f9]':
|
||||
checked,
|
||||
'my-px h-px justify-self-center': checked === null,
|
||||
},
|
||||
}}
|
||||
className={twMerge(
|
||||
'inline-block h-2 w-2 rounded-full',
|
||||
checked === null && 'my-px h-px justify-self-center',
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</span>
|
||||
|
||||
<Text
|
||||
<span
|
||||
{...(slotProps?.label || {})}
|
||||
component="span"
|
||||
className={twMerge(
|
||||
className={cn(
|
||||
'truncate !text-xs font-normal',
|
||||
slotProps?.label?.className,
|
||||
)}
|
||||
>
|
||||
{String(checked)}
|
||||
</Text>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
|
||||
function KeyIcon(props: IconProps) {
|
||||
return (
|
||||
<SvgIcon
|
||||
width="16"
|
||||
height="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
aria-label="Key"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M5.823 7.677a4.496 4.496 0 1 1 2.5 2.5L7.5 11H6v1.5H4.5V14H2v-2.5l3.823-3.823Z"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
<path
|
||||
d="M10.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
KeyIcon.displayName = 'NhostKeyIcon';
|
||||
|
||||
export default KeyIcon;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as KeyIcon } from './KeyIcon';
|
||||
@@ -1,58 +1,66 @@
|
||||
'use client';
|
||||
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
||||
}
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn('border-b', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = 'AccordionItem';
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn('border-b last:border-b-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
className,
|
||||
)}
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
'flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium outline-none transition-all hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn('pb-4 pt-0', className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
<div className={cn('pb-4 pt-0', className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
);
|
||||
}
|
||||
|
||||
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
||||
|
||||
@@ -11,7 +11,7 @@ const badgeVariants = cva(
|
||||
default:
|
||||
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80 dark:text-white',
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
'border-transparent bg-card text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
|
||||
83
dashboard/src/components/ui/v3/button-group.tsx
Normal file
83
dashboard/src/components/ui/v3/button-group.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Separator } from "@/components/ui/v3/separator"
|
||||
|
||||
const buttonGroupVariants = cva(
|
||||
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal:
|
||||
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
||||
vertical:
|
||||
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "horizontal",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function ButtonGroup({
|
||||
className,
|
||||
orientation,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
data-orientation={orientation}
|
||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ButtonGroupText({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
"bg-muted shadow-xs flex items-center gap-2 rounded-md border px-4 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ButtonGroupSeparator({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="button-group-separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
ButtonGroup,
|
||||
ButtonGroupSeparator,
|
||||
ButtonGroupText,
|
||||
buttonGroupVariants,
|
||||
}
|
||||
@@ -16,8 +16,8 @@ const buttonVariants = cva(
|
||||
outline:
|
||||
'border 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',
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary-hover',
|
||||
ghost: 'hover:bg-accent text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
@@ -56,11 +56,13 @@ Button.displayName = 'Button';
|
||||
|
||||
const ButtonWithLoading = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ButtonProps & { loading?: boolean }
|
||||
>(({ loading, disabled, children, ...props }, ref) => {
|
||||
ButtonProps & { loading?: boolean; loaderClassName?: string }
|
||||
>(({ loading, disabled, children, loaderClassName, ...props }, ref) => {
|
||||
return (
|
||||
<Button disabled={loading || disabled} ref={ref} {...props}>
|
||||
{loading && <Loader2 className="mr-2 animate-spin" />}
|
||||
{loading && (
|
||||
<Loader2 className={cn('mr-2 animate-spin', loaderClassName)} />
|
||||
)}
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -11,15 +11,15 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:border-disabled disabled:bg-disabled disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn('flex items-center justify-center text-current')}
|
||||
className={cn('flex items-center justify-center text-white')}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
<Check width={16} height={16} strokeWidth={3} />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
|
||||
7
dashboard/src/components/ui/v3/collapsible.tsx
Normal file
7
dashboard/src/components/ui/v3/collapsible.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const { CollapsibleTrigger, CollapsibleContent } = CollapsiblePrimitive;
|
||||
|
||||
export { Collapsible, CollapsibleContent, CollapsibleTrigger };
|
||||
@@ -31,6 +31,7 @@ interface DialogContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
||||
disableOutsideClick?: boolean;
|
||||
hideCloseButton?: boolean;
|
||||
closeButtonClassName?: string;
|
||||
}
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
@@ -38,7 +39,14 @@ const DialogContent = React.forwardRef<
|
||||
DialogContentProps
|
||||
>(
|
||||
(
|
||||
{ className, children, disableOutsideClick, hideCloseButton, ...props },
|
||||
{
|
||||
className,
|
||||
children,
|
||||
disableOutsideClick,
|
||||
hideCloseButton,
|
||||
closeButtonClassName,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => (
|
||||
<DialogPortal>
|
||||
@@ -58,7 +66,12 @@ const DialogContent = React.forwardRef<
|
||||
>
|
||||
{children}
|
||||
{!hideCloseButton && (
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<DialogPrimitive.Close
|
||||
className={cn(
|
||||
'absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent-background data-[state=open]:text-muted-foreground',
|
||||
closeButtonClassName,
|
||||
)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
|
||||
@@ -5,11 +5,11 @@ export function InlineCode({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: PropsWithChildren<{ className?: string }>) {
|
||||
}: PropsWithChildren<React.HTMLAttributes<HTMLElement>>) {
|
||||
return (
|
||||
<code
|
||||
className={cn(
|
||||
'relative rounded bg-[#eaedf0] px-1 font-mono text-[11px] dark:bg-[#2f363d]',
|
||||
'relative max-w-xs truncate rounded bg-[#eaedf0] px-1 font-mono text-[11px] dark:bg-[#2f363d]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
177
dashboard/src/components/ui/v3/input-group.tsx
Normal file
177
dashboard/src/components/ui/v3/input-group.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import { Input } from '@/components/ui/v3/input';
|
||||
import { Textarea } from '@/components/ui/v3/textarea';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
className={cn(
|
||||
'group/input-group shadow-xs relative flex w-full items-center rounded-md border border-input outline-none transition-[color,box-shadow] dark:bg-input/30',
|
||||
'h-9 has-[>textarea]:h-auto',
|
||||
|
||||
// Variants based on alignment.
|
||||
'has-[>[data-align=inline-start]]:[&>input]:pl-2',
|
||||
'has-[>[data-align=inline-end]]:[&>input]:pr-2',
|
||||
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
|
||||
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
|
||||
|
||||
// Focus state.
|
||||
'has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot=input-group-control]:focus-visible]:ring-ring',
|
||||
|
||||
// Error state.
|
||||
'has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
|
||||
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const inputGroupAddonVariants = cva(
|
||||
"text-muted-foreground flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
align: {
|
||||
'inline-start':
|
||||
'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',
|
||||
'inline-end':
|
||||
'order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]',
|
||||
'block-start':
|
||||
'[.border-b]:pb-3 order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5',
|
||||
'block-end':
|
||||
'[.border-t]:pt-3 order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
align: 'inline-start',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function InputGroupAddon({
|
||||
className,
|
||||
align = 'inline-start',
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
data-slot="input-group-addon"
|
||||
data-align={align}
|
||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
if ((e.target as HTMLElement).closest('button')) {
|
||||
return;
|
||||
}
|
||||
e.currentTarget.parentElement?.querySelector('input')?.focus();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLDivElement).click();
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const inputGroupButtonVariants = cva(
|
||||
'flex items-center gap-2 text-sm shadow-none',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
||||
sm: 'h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5',
|
||||
'icon-xs':
|
||||
'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0',
|
||||
'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'xs',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function InputGroupButton({
|
||||
className,
|
||||
type = 'button',
|
||||
variant = 'ghost',
|
||||
size = 'xs',
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof Button>, 'size'> &
|
||||
VariantProps<typeof inputGroupButtonVariants>) {
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
data-size={size}
|
||||
variant={variant}
|
||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupInput({
|
||||
className,
|
||||
wrapperClassName,
|
||||
...props
|
||||
}: React.ComponentProps<'input'> & { wrapperClassName?: string }) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
|
||||
className,
|
||||
)}
|
||||
wrapperClassName={wrapperClassName}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupTextarea({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'textarea'>) {
|
||||
return (
|
||||
<Textarea
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
};
|
||||
@@ -5,12 +5,13 @@ import { cn } from '@/lib/utils';
|
||||
export interface InputProps
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
|
||||
prefix?: React.ReactNode;
|
||||
wrapperClassName?: string;
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, prefix, ...props }, ref) => {
|
||||
({ className, type, prefix, wrapperClassName, ...props }, ref) => {
|
||||
return (
|
||||
<div className="relative flex items-center">
|
||||
<div className={cn('relative flex items-center', wrapperClassName)}>
|
||||
{prefix && (
|
||||
<span className="pointer-events-none absolute left-3 flex items-center text-muted-foreground">
|
||||
{prefix}
|
||||
@@ -19,7 +20,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-accent',
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-accent-background',
|
||||
{ 'pl-6': prefix },
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -54,6 +54,7 @@ interface SheetContentProps
|
||||
VariantProps<typeof sheetVariants> {
|
||||
container?: HTMLElement | null;
|
||||
hideCloseButton?: boolean;
|
||||
showOverlay?: boolean;
|
||||
}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
@@ -67,11 +68,13 @@ const SheetContent = React.forwardRef<
|
||||
container = null,
|
||||
hideCloseButton,
|
||||
children,
|
||||
showOverlay = false,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => (
|
||||
<SheetPortal container={container}>
|
||||
{showOverlay && <SheetOverlay />}
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
|
||||
13
dashboard/src/components/ui/v3/skeleton.tsx
Normal file
13
dashboard/src/components/ui/v3/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('animate-pulse rounded-md bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
@@ -33,6 +33,7 @@ interface SpinnerContentProps
|
||||
VariantProps<typeof loaderVariants> {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
wrapperClassName?: string;
|
||||
}
|
||||
|
||||
export function Spinner({
|
||||
@@ -40,10 +41,12 @@ export function Spinner({
|
||||
show,
|
||||
children,
|
||||
className,
|
||||
wrapperClassName,
|
||||
}: SpinnerContentProps) {
|
||||
return (
|
||||
<span className={spinnerVariants({ show })}>
|
||||
<span className={cn(spinnerVariants({ show }), wrapperClassName)}>
|
||||
<Loader2
|
||||
role="progressbar"
|
||||
className={cn(
|
||||
loaderVariants({ size }),
|
||||
className,
|
||||
|
||||
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -3,26 +3,58 @@ import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
function TooltipProvider({
|
||||
delayDuration = 0, // eslint-disable-line react/prop-types
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
||||
|
||||
@@ -24,12 +24,14 @@ function SignInWithEmailAndPassword({ onSubmit, isLoading }: Props) {
|
||||
label="Email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
/>
|
||||
<FormInput
|
||||
control={form.control}
|
||||
label="Password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
/>
|
||||
<NextLink
|
||||
href="/password/new"
|
||||
|
||||
@@ -22,18 +22,25 @@ function SignUpWithEmailAndPasswordForm() {
|
||||
onSubmit={form.handleSubmit(onSignUpWithPassword)}
|
||||
className="grid grid-flow-row gap-4 bg-transparent"
|
||||
>
|
||||
<FormInput control={form.control} label="Name" name="displayName" />
|
||||
<FormInput
|
||||
control={form.control}
|
||||
label="Name"
|
||||
name="displayName"
|
||||
placeholder="Name"
|
||||
/>
|
||||
<FormInput
|
||||
control={form.control}
|
||||
label="Email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
/>
|
||||
<FormInput
|
||||
control={form.control}
|
||||
label="Password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -22,12 +22,18 @@ function SignUpWithSecurityKeyForm() {
|
||||
onSubmit={form.handleSubmit(onSignUpWithSecurityKey)}
|
||||
className="grid grid-flow-row gap-4 bg-transparent"
|
||||
>
|
||||
<FormInput control={form.control} label="Name" name="displayName" />
|
||||
<FormInput
|
||||
control={form.control}
|
||||
label="Name"
|
||||
name="displayName"
|
||||
placeholder="Name"
|
||||
/>
|
||||
<FormInput
|
||||
control={form.control}
|
||||
label="Email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function BillingDetails() {
|
||||
<AccordionContent className="border-t-1 pb-0">
|
||||
<div className="rounded-md">
|
||||
<Table>
|
||||
<TableHeader className="w-full bg-accent">
|
||||
<TableHeader className="w-full bg-accent-background">
|
||||
<TableRow>
|
||||
<TableHead colSpan={3} className="w-full rounded-tl-md">
|
||||
Item
|
||||
@@ -72,7 +72,7 @@ export default function BillingDetails() {
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter className="bg-accent">
|
||||
<TableFooter className="bg-accent-background">
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="rounded-bl-md">
|
||||
Total
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function ProjectsGrid({ projects }: ProjectGridProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto h-full overflow-auto bg-accent">
|
||||
<div className="mx-auto h-full overflow-auto bg-accent-background">
|
||||
<div className="flex w-full flex-shrink-0 flex-row items-center justify-between gap-2 border-b bg-background p-2">
|
||||
<Input
|
||||
placeholder="Find Project"
|
||||
@@ -85,7 +85,6 @@ export default function ProjectsGrid({ projects }: ProjectGridProps) {
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{filteredProjects.map((project) => (
|
||||
<ProjectCard key={project.id} project={project} />
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { DragAndDropList } from '@/components/common/DragAndDropList';
|
||||
|
||||
import { isEmptyValue } from '@/lib/utils';
|
||||
import type { DropResult } from '@hello-pangea/dnd';
|
||||
import type { ColumnInstance } from 'react-table';
|
||||
import ColumnCustomizerRow from './ColumnCustomizerRow';
|
||||
import ShowHideAllColumnsButtons from './ShowHideAllColumnsButtons';
|
||||
|
||||
type ColumnCustomizerProps = {
|
||||
columns: ColumnInstance[];
|
||||
onDragEnd: (columnsOrder: string[]) => void;
|
||||
onReset: () => void;
|
||||
onShowAllColumns: () => void;
|
||||
onHideAllColumns: () => void;
|
||||
};
|
||||
|
||||
function reorder(list: ColumnInstance[], startIndex: number, endIndex: number) {
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function ColumnCustomizer({
|
||||
columns,
|
||||
onDragEnd,
|
||||
onReset,
|
||||
onShowAllColumns,
|
||||
onHideAllColumns,
|
||||
}: ColumnCustomizerProps) {
|
||||
function handleDragEnd(result: DropResult) {
|
||||
if (isEmptyValue(result.destination)) {
|
||||
return;
|
||||
}
|
||||
const reordered = reorder(
|
||||
columns,
|
||||
result.source.index,
|
||||
result.destination!.index,
|
||||
).map(({ id }) => id);
|
||||
|
||||
onDragEnd(reordered);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex max-h-[calc(100%-15rem)] flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<h4 className="font-medium leading-none">Column Settings</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Reorder columns by dragging or show/hide them with checkboxes.
|
||||
</p>
|
||||
</div>
|
||||
<div className="self-center">
|
||||
<ShowHideAllColumnsButtons
|
||||
onShowAll={onShowAllColumns}
|
||||
onHideAll={onHideAllColumns}
|
||||
onReset={onReset}
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-scroll">
|
||||
<DragAndDropList droppableId="columnOrder" onDragEnd={handleDragEnd}>
|
||||
{columns.map((column, index) => (
|
||||
<ColumnCustomizerRow
|
||||
key={column.id}
|
||||
column={column}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</DragAndDropList>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ColumnCustomizer;
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
DraggableItem,
|
||||
type DraggableItemProps,
|
||||
} from '@/components/common/DragAndDropList';
|
||||
import { Checkbox } from '@/components/ui/v3/checkbox';
|
||||
import { useTablePath } from '@/features/orgs/projects/database/common/hooks/useTablePath';
|
||||
import PersistenDataTableConfigurationStorage from '@/features/orgs/projects/storage/dataGrid/utils/PersistenDataTableConfigurationStorage';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import type { ColumnInstance } from 'react-table';
|
||||
|
||||
type ColumnCustomizerProps = {
|
||||
column: ColumnInstance;
|
||||
} & Omit<DraggableItemProps, 'draggableId'>;
|
||||
|
||||
function ColumnCustomizerRow({ column, index }: ColumnCustomizerProps) {
|
||||
const tablePath = useTablePath();
|
||||
|
||||
function handleVisibilityChange() {
|
||||
PersistenDataTableConfigurationStorage.toggleColumnVisibility(
|
||||
tablePath,
|
||||
column.id,
|
||||
);
|
||||
column.toggleHidden();
|
||||
}
|
||||
|
||||
return (
|
||||
<DraggableItem draggableId={column.id} index={index} className="mb-3">
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between rounded-md bg-accent p-2',
|
||||
{ 'opacity-70': !column.isVisible },
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-5">
|
||||
<Checkbox
|
||||
checked={column.isVisible}
|
||||
className="h-[1.125rem] w-[1.125rem] border-[#21324b] data-[state=checked]:!border-transparent dark:border-[#dfecf5]"
|
||||
onCheckedChange={handleVisibilityChange}
|
||||
/>
|
||||
<span>{column.id}</span>
|
||||
</div>
|
||||
<GripVertical />
|
||||
</div>
|
||||
</DraggableItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default ColumnCustomizerRow;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import { ButtonGroup } from '@/components/ui/v3/button-group';
|
||||
|
||||
type ShowHideAllColumnsToggleProps = {
|
||||
onShowAll: () => void;
|
||||
onHideAll: () => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
function ShowHideAllColumnsButtons({
|
||||
onShowAll,
|
||||
onHideAll,
|
||||
onReset,
|
||||
}: ShowHideAllColumnsToggleProps) {
|
||||
return (
|
||||
<ButtonGroup className="w-full">
|
||||
<Button variant="outline" className="flex-1" onClick={onShowAll}>
|
||||
Show all columns
|
||||
</Button>
|
||||
<Button variant="outline" className="flex-1" onClick={onHideAll}>
|
||||
Hide all columns
|
||||
</Button>
|
||||
<Button variant="outline" className="flex-1" onClick={onReset}>
|
||||
Reset
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShowHideAllColumnsButtons;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ColumnCustomizer } from './ColumnCustomizer';
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@/components/ui/v3/sheet';
|
||||
import { useTablePath } from '@/features/orgs/projects/database/common/hooks/useTablePath';
|
||||
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
|
||||
import PersistenDataTableConfigurationStorage from '@/features/orgs/projects/storage/dataGrid/utils/PersistenDataTableConfigurationStorage';
|
||||
import { ColumnCustomizer } from './ColumnCustomizer';
|
||||
import { useDataGridCustomizerOpenStateContext } from './DataGridCustomizerOpenStateProvider';
|
||||
import DataGridCustomizerTrigger from './DataGridCustomizerTrigger';
|
||||
import RowDensityCustomizer from './RowDensityCustomizer';
|
||||
|
||||
function DataGridCustomizerControls() {
|
||||
const { allColumns, setColumnOrder, setHiddenColumns } = useDataGridConfig();
|
||||
const tablePath = useTablePath();
|
||||
const { open, setOpen } = useDataGridCustomizerOpenStateContext();
|
||||
|
||||
const columns = allColumns.filter(({ id }) => id !== 'selection-column');
|
||||
|
||||
function saveHiddenCols(cols: string[]) {
|
||||
setHiddenColumns(cols);
|
||||
PersistenDataTableConfigurationStorage.saveHiddenColumns(tablePath, cols);
|
||||
}
|
||||
|
||||
function showOriginalOrder() {
|
||||
setColumnOrder([]);
|
||||
PersistenDataTableConfigurationStorage.saveColumnOrder(tablePath, []);
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
showOriginalOrder();
|
||||
saveHiddenCols([]);
|
||||
}
|
||||
|
||||
function handleDragEnd(newOrder: string[]) {
|
||||
setColumnOrder(newOrder);
|
||||
PersistenDataTableConfigurationStorage.saveColumnOrder(tablePath, newOrder);
|
||||
}
|
||||
|
||||
function hideAllColumns() {
|
||||
const allColumnsId = columns.map(({ id }) => id);
|
||||
saveHiddenCols(allColumnsId);
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<DataGridCustomizerTrigger />
|
||||
</SheetTrigger>
|
||||
|
||||
<SheetContent
|
||||
className="box flex w-full flex-col rounded-none md:w-[26rem] md:max-w-[26rem]"
|
||||
onInteractOutside={() => setOpen(false)}
|
||||
showOverlay
|
||||
>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Customize Table View</SheetTitle>
|
||||
<SheetDescription className="sr-only">
|
||||
Customize columns
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full flex-col gap-8">
|
||||
<RowDensityCustomizer />
|
||||
<ColumnCustomizer
|
||||
columns={columns}
|
||||
onDragEnd={handleDragEnd}
|
||||
onReset={handleReset}
|
||||
onShowAllColumns={() => saveHiddenCols([])}
|
||||
onHideAllColumns={hideAllColumns}
|
||||
/>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataGridCustomizerControls;
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
createContext,
|
||||
type Dispatch,
|
||||
type PropsWithChildren,
|
||||
type SetStateAction,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
type DataGridCustomizerOpenStateContextProps = {
|
||||
open: boolean;
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const DataGridCustomizerOpenStateContext =
|
||||
createContext<DataGridCustomizerOpenStateContextProps>({
|
||||
open: false,
|
||||
setOpen: () => {},
|
||||
});
|
||||
|
||||
function DataGridCustomizerOpenStateProvider({ children }: PropsWithChildren) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
open,
|
||||
setOpen,
|
||||
}),
|
||||
[open],
|
||||
);
|
||||
return (
|
||||
<DataGridCustomizerOpenStateContext.Provider value={value}>
|
||||
{children}
|
||||
</DataGridCustomizerOpenStateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useDataGridCustomizerOpenStateContext() {
|
||||
const context = useContext(DataGridCustomizerOpenStateContext);
|
||||
return context;
|
||||
}
|
||||
|
||||
export default DataGridCustomizerOpenStateProvider;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Button, type ButtonProps } from '@/components/ui/v3/button';
|
||||
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Columns3 } from 'lucide-react';
|
||||
import { type ForwardedRef, forwardRef } from 'react';
|
||||
|
||||
function DataBrowserCustomizerTrigger(
|
||||
props: ButtonProps,
|
||||
ref: ForwardedRef<HTMLButtonElement>,
|
||||
) {
|
||||
const { allColumns } = useDataGridConfig();
|
||||
const numberOfHiddenColumns = allColumns.filter(
|
||||
({ isVisible }) => !isVisible,
|
||||
).length;
|
||||
const hasHiddenColumns = numberOfHiddenColumns !== 0;
|
||||
|
||||
const { className, ...buttonProps } = props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn('relative', className)}
|
||||
{...(hasHiddenColumns && {
|
||||
title: `${numberOfHiddenColumns} ${numberOfHiddenColumns === 1 ? ' column is' : ' columns are'} hidden`,
|
||||
})}
|
||||
{...buttonProps}
|
||||
>
|
||||
<Columns3 />
|
||||
{hasHiddenColumns && (
|
||||
<span className="absolute bottom-[8px] right-[6px] w-[0.625rem] rounded-full bg-primary-text p-0 text-[0.625rem] leading-none text-paper">
|
||||
!
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(DataBrowserCustomizerTrigger);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Label } from '@/components/ui/v3/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/v3/radio-group';
|
||||
import { useDataTableDesignContext } from '@/features/orgs/projects/storage/dataGrid/providers/DataTableDesignProvider';
|
||||
|
||||
function RowDensityCustomizer() {
|
||||
const context = useDataTableDesignContext();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 pt-2">
|
||||
<div className="flex flex-col gap-4">
|
||||
<h4 className="font-medium leading-none">Density</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Set row height across all tables
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<RadioGroup
|
||||
className="flex flex-col space-y-1"
|
||||
defaultValue={context.rowDensity}
|
||||
value={context.rowDensity}
|
||||
onValueChange={context.setRowDensity}
|
||||
>
|
||||
<div className="flex justify-start gap-3">
|
||||
<RadioGroupItem value="comfortable" id="height1" />
|
||||
<Label htmlFor="height1" className="hover:cursor-pointer">
|
||||
Comfortable
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex justify-start gap-3">
|
||||
<RadioGroupItem value="compact" id="height2" />
|
||||
<Label htmlFor="height2" className="hover:cursor-pointer">
|
||||
Compact
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RowDensityCustomizer;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DataGridCustomizerControls } from './DataGridCustomizerControls';
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/v3/tooltip';
|
||||
import { Info } from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface InfoTooltipProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function InfoTooltip({ children }: InfoTooltipProps) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="size-4 text-primary" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{children}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as InfoTooltip } from './InfoTooltip';
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/v3/tooltip';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface TextWithTooltipProps {
|
||||
text: string | number | ReactNode;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
slotProps?: {
|
||||
container?: React.HTMLAttributes<HTMLDivElement>;
|
||||
};
|
||||
}
|
||||
|
||||
export default function TextWithTooltip({
|
||||
text,
|
||||
containerClassName,
|
||||
className,
|
||||
slotProps,
|
||||
}: TextWithTooltipProps) {
|
||||
const [isTruncated, setIsTruncated] = useState<boolean>(false);
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkTruncation = () => {
|
||||
if (textRef.current) {
|
||||
const { scrollWidth, clientWidth } = textRef.current;
|
||||
setIsTruncated(scrollWidth > clientWidth);
|
||||
}
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
checkTruncation();
|
||||
});
|
||||
|
||||
if (textRef.current) {
|
||||
resizeObserver.observe(textRef.current);
|
||||
}
|
||||
|
||||
checkTruncation();
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={containerClassName} {...slotProps?.container}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger disabled={!isTruncated} asChild>
|
||||
<div
|
||||
ref={textRef}
|
||||
className={cn(
|
||||
'truncate',
|
||||
!isTruncated && 'pointer-events-none',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{text}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as TextWithTooltip } from './TextWithTooltip';
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useGetDataSources } from './useGetDataSources';
|
||||
@@ -0,0 +1,52 @@
|
||||
import { fetchExportMetadata } from '@/features/orgs/projects/common/utils/fetchExportMetadata';
|
||||
import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/generateAppServiceUrl';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import type { ExportMetadataResponse } from '@/utils/hasura-api/generated/schemas';
|
||||
import type { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export interface UseGetDataSourcesOptions {
|
||||
/**
|
||||
* Props passed to the underlying query hook.
|
||||
*/
|
||||
queryOptions?: UseQueryOptions<ExportMetadataResponse, unknown, string[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook gets the data sources names from the metadata.
|
||||
*
|
||||
* @param options - Options to use for the query.
|
||||
* @returns The result of the query.
|
||||
*/
|
||||
export default function useGetDataSources({
|
||||
queryOptions,
|
||||
}: UseGetDataSourcesOptions = {}) {
|
||||
const { project } = useProject();
|
||||
|
||||
const query = useQuery<ExportMetadataResponse, unknown, string[]>(
|
||||
['export-metadata', project?.subdomain],
|
||||
() => {
|
||||
const appUrl = generateAppServiceUrl(
|
||||
project!.subdomain,
|
||||
project!.region,
|
||||
'hasura',
|
||||
);
|
||||
|
||||
const adminSecret = project?.config?.hasura.adminSecret!;
|
||||
|
||||
return fetchExportMetadata({ appUrl, adminSecret });
|
||||
},
|
||||
{
|
||||
...queryOptions,
|
||||
select: (data) =>
|
||||
data.metadata?.sources?.reduce<string[]>((acc, source) => {
|
||||
if (source.name) {
|
||||
acc.push(source.name);
|
||||
}
|
||||
return acc;
|
||||
}, []) ?? [],
|
||||
},
|
||||
);
|
||||
|
||||
return query;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useGetMetadata } from './useGetMetadata';
|
||||
@@ -0,0 +1,57 @@
|
||||
import { fetchExportMetadata } from '@/features/orgs/projects/common/utils/fetchExportMetadata';
|
||||
import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/generateAppServiceUrl';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import type {
|
||||
ExportMetadataResponse,
|
||||
ExportMetadataResponseMetadata,
|
||||
} from '@/utils/hasura-api/generated/schemas';
|
||||
import type { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export interface UseGetMetadataOptions {
|
||||
/**
|
||||
* Props passed to the underlying query hook.
|
||||
*/
|
||||
queryOptions?: UseQueryOptions<
|
||||
ExportMetadataResponse,
|
||||
unknown,
|
||||
ExportMetadataResponseMetadata
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook gets the metadata from the Hasura API.
|
||||
*
|
||||
* @param options - Options to use for the query.
|
||||
* @returns The result of the query.
|
||||
*/
|
||||
export default function useGetMetadata({
|
||||
queryOptions,
|
||||
}: UseGetMetadataOptions = {}) {
|
||||
const { project } = useProject();
|
||||
|
||||
const query = useQuery<
|
||||
ExportMetadataResponse,
|
||||
unknown,
|
||||
ExportMetadataResponseMetadata
|
||||
>(
|
||||
['export-metadata', project?.subdomain],
|
||||
() => {
|
||||
const appUrl = generateAppServiceUrl(
|
||||
project!.subdomain,
|
||||
project!.region,
|
||||
'hasura',
|
||||
);
|
||||
|
||||
const adminSecret = project?.config?.hasura.adminSecret!;
|
||||
|
||||
return fetchExportMetadata({ appUrl, adminSecret });
|
||||
},
|
||||
{
|
||||
...queryOptions,
|
||||
select: (data) => data.metadata,
|
||||
},
|
||||
);
|
||||
|
||||
return query;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type RowDensity = 'comfortable' | 'compact';
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MetadataOperationOptions } from '@/features/orgs/projects/remote-schemas/types';
|
||||
import { metadataOperation } from '@/utils/hasura-api/generated/default/default';
|
||||
import type { ExportMetadataResponse } from '@/utils/hasura-api/generated/schemas';
|
||||
import type { MetadataOperationOptions } from '@/utils/hasura-api/types';
|
||||
|
||||
export default async function fetchExportMetadata({
|
||||
appUrl,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { ButtonWithLoading as Button } from '@/components/ui/v3/button';
|
||||
import { DatabaseRecordInputGroup } from '@/features/orgs/projects/database/dataGrid/components/DatabaseRecordInputGroup';
|
||||
import type {
|
||||
ColumnInsertOptions,
|
||||
DataBrowserGridColumn,
|
||||
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { useEffect } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
@@ -153,16 +153,19 @@ export default function BaseRecordForm({
|
||||
description="These columns are nullable and don't require a value."
|
||||
columns={optionalColumns}
|
||||
autoFocusFirstInput={requiredColumns.length === 0}
|
||||
sx={{ borderTopWidth: requiredColumns.length > 0 ? 1 : 0 }}
|
||||
className="px-6 pt-3"
|
||||
className={cn(
|
||||
'px-6 pt-3',
|
||||
requiredColumns.length > 0 ? 'border-t-1' : 'border-t-0',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Box className="grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 p-2">
|
||||
<div className="box grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 p-2">
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
variant="outline"
|
||||
className="border-none"
|
||||
size="sm"
|
||||
onClick={onCancel}
|
||||
tabIndex={isDirty ? -1 : 0}
|
||||
>
|
||||
@@ -172,12 +175,13 @@ export default function BaseRecordForm({
|
||||
<Button
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
size="sm"
|
||||
type="submit"
|
||||
className="justify-self-end"
|
||||
>
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
</Box>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,10 @@ export default function ColumnEditorTable() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div role="table" className="col-span-8 overflow-x-auto">
|
||||
<div
|
||||
role="table"
|
||||
className="col-span-8 overflow-x-auto min-[900px]:overflow-x-visible"
|
||||
>
|
||||
<div className="sticky top-0 z-10 flex w-full gap-2 pb-2 pt-1">
|
||||
<div role="columnheader" className="w-52 flex-none">
|
||||
<InputLabel as="span">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Alert } from '@/components/ui/v3/alert';
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import { useTablePath } from '@/features/orgs/projects/database/common/hooks/useTablePath';
|
||||
import type { BaseRecordFormProps } from '@/features/orgs/projects/database/dataGrid/components/BaseRecordForm';
|
||||
import { BaseRecordForm } from '@/features/orgs/projects/database/dataGrid/components/BaseRecordForm';
|
||||
import { useCreateRecordMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useCreateRecordMutation';
|
||||
@@ -7,6 +8,7 @@ import type { ColumnInsertOptions } from '@/features/orgs/projects/database/data
|
||||
import { createDynamicValidationSchema } from '@/features/orgs/projects/database/dataGrid/utils/validationSchemaHelpers';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
export interface CreateRecordFormProps
|
||||
@@ -15,14 +17,20 @@ export interface CreateRecordFormProps
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit?: (args?: any) => Promise<any>;
|
||||
currentOffset: number;
|
||||
sortByString: string;
|
||||
}
|
||||
|
||||
export default function CreateRecordForm({
|
||||
onSubmit,
|
||||
currentOffset,
|
||||
sortByString,
|
||||
...props
|
||||
}: CreateRecordFormProps) {
|
||||
const { mutateAsync: insertRow, error, reset } = useCreateRecordMutation();
|
||||
const validationSchema = createDynamicValidationSchema(props.columns);
|
||||
const currentTablePath = useTablePath();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: props.columns.reduce((defaultValues, column) => {
|
||||
@@ -42,6 +50,12 @@ export default function CreateRecordForm({
|
||||
|
||||
if (onSubmit) {
|
||||
await onSubmit();
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [currentTablePath, currentOffset],
|
||||
});
|
||||
await queryClient.refetchQueries({
|
||||
queryKey: [currentTablePath, currentOffset],
|
||||
});
|
||||
}
|
||||
|
||||
triggerToast('The row has been inserted successfully.');
|
||||
@@ -55,18 +69,17 @@ export default function CreateRecordForm({
|
||||
{error && error instanceof Error ? (
|
||||
<div className="-mt-3 mb-4 px-6">
|
||||
<Alert
|
||||
severity="error"
|
||||
className="grid grid-flow-col items-center justify-between px-4 py-3"
|
||||
variant="destructive"
|
||||
className="grid grid-flow-col items-center justify-between border-none bg-[#f1315433] px-4 py-3"
|
||||
>
|
||||
<span className="text-left">
|
||||
<strong>Error:</strong> {error.message}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={reset}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="bg-transparent text-[#c91737] hover:bg-[#f131541a]"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { cn } from '@/lib/utils';
|
||||
import Image from 'next/image';
|
||||
import type { DetailedHTMLProps, HTMLProps, ReactNode } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface DataBrowserEmptyStateProps
|
||||
extends Omit<
|
||||
@@ -26,7 +25,7 @@ export default function DataBrowserEmptyState({
|
||||
}: DataBrowserEmptyStateProps) {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
className={cn(
|
||||
'grid w-full place-content-center gap-2 px-4 py-16 text-center',
|
||||
className,
|
||||
)}
|
||||
@@ -41,12 +40,10 @@ export default function DataBrowserEmptyState({
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Text variant="h3" component="h1">
|
||||
<h1 className="font-inter-var text-[1.125rem] font-medium !leading-6">
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
<Text>{description}</Text>
|
||||
</h1>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,17 +15,17 @@ import { normalizeDefaultValue } from '@/features/orgs/projects/database/dataGri
|
||||
import {
|
||||
POSTGRESQL_CHARACTER_TYPES,
|
||||
POSTGRESQL_DATE_TIME_TYPES,
|
||||
POSTGRESQL_DECIMAL_TYPES,
|
||||
POSTGRESQL_INTEGER_TYPES,
|
||||
POSTGRESQL_JSON_TYPES,
|
||||
POSTGRESQL_NUMERIC_TYPES,
|
||||
} from '@/features/orgs/projects/database/dataGrid/utils/postgresqlConstants';
|
||||
import type { DataGridProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid';
|
||||
import { DataGrid } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid';
|
||||
import { DataGridBooleanCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridBooleanCell';
|
||||
import { DataGridDateCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridDateCell';
|
||||
import { DataGridDecimalCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridDecimalCell';
|
||||
import { DataGridIntegerCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridIntegerCell';
|
||||
import { DataGridNumericCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridNumericCell';
|
||||
import { DataGridTextCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridTextCell';
|
||||
import { isNotEmptyValue } from '@/lib/utils';
|
||||
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { KeyRound } from 'lucide-react';
|
||||
import dynamic from 'next/dynamic';
|
||||
@@ -68,6 +68,7 @@ export function createDataGridColumn(
|
||||
isEditable,
|
||||
type: 'text',
|
||||
specificType: column.full_data_type,
|
||||
dataType: column.data_type,
|
||||
maxLength: column.character_maximum_length,
|
||||
Cell: DataGridTextCell,
|
||||
isPrimary: column.is_primary,
|
||||
@@ -82,21 +83,13 @@ export function createDataGridColumn(
|
||||
foreignKeyRelation: column.foreign_key_relation,
|
||||
};
|
||||
|
||||
if (POSTGRESQL_INTEGER_TYPES.includes(column.data_type)) {
|
||||
if (POSTGRESQL_NUMERIC_TYPES.includes(column.data_type)) {
|
||||
return {
|
||||
...defaultColumnConfiguration,
|
||||
type: 'number',
|
||||
isCopiable: true,
|
||||
width: 250,
|
||||
Cell: DataGridIntegerCell,
|
||||
};
|
||||
}
|
||||
|
||||
if (POSTGRESQL_DECIMAL_TYPES.includes(column.data_type)) {
|
||||
return {
|
||||
...defaultColumnConfiguration,
|
||||
type: 'text',
|
||||
width: 250,
|
||||
Cell: DataGridDecimalCell,
|
||||
Cell: DataGridNumericCell,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -137,6 +130,7 @@ export function createDataGridColumn(
|
||||
...defaultColumnConfiguration,
|
||||
type: 'date',
|
||||
width: 200,
|
||||
isCopiable: true,
|
||||
Cell: DataGridDateCell,
|
||||
};
|
||||
}
|
||||
@@ -166,8 +160,12 @@ export default function DataBrowserGrid({
|
||||
|
||||
const { mutateAsync: updateRow } = useUpdateRecordWithToastMutation();
|
||||
|
||||
const sortByString = isNotEmptyValue(sortBy?.[0])
|
||||
? `${sortBy[0].id}.${sortBy[0].desc}`
|
||||
: 'default-order';
|
||||
|
||||
const { data, status, error, refetch } = useTableQuery(
|
||||
[currentTablePath, limit, currentOffset, sortBy],
|
||||
[currentTablePath, currentOffset, sortByString],
|
||||
{
|
||||
limit,
|
||||
offset: currentOffset * limit,
|
||||
@@ -274,6 +272,8 @@ export default function DataBrowserGrid({
|
||||
// TODO: Create proper typings for data browser columns
|
||||
columns={memoizedColumns as unknown as DataBrowserGridColumn[]}
|
||||
onSubmit={refetch}
|
||||
currentOffset={currentOffset}
|
||||
sortByString={sortByString}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Chip } from '@/components/ui/v2/Chip';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { Badge } from '@/components/ui/v3/badge';
|
||||
import { ButtonWithLoading as Button } from '@/components/ui/v3/button';
|
||||
import { DataGridCustomizerControls } from '@/features/orgs/projects/common/components/DataGridCustomizerControls';
|
||||
import { InvokeEventTriggerButton } from '@/features/orgs/projects/database/dataGrid/components/InvokeEventTriggerButton';
|
||||
import { useDeleteRecordMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useDeleteRecordMutation';
|
||||
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
|
||||
import type { DataGridPaginationProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPagination';
|
||||
import { DataGridPagination } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPagination';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import type { Row } from 'react-table';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface DataBrowserGridControlsProps extends BoxProps {
|
||||
export interface DataBrowserGridControlsProps {
|
||||
/**
|
||||
* Props passed to the pagination component.
|
||||
*/
|
||||
@@ -33,11 +34,9 @@ export interface DataBrowserGridControlsProps extends BoxProps {
|
||||
// TODO: Get rid of Data Browser related code from here. This component should
|
||||
// be generic and not depend on Data Browser related data types and logic.
|
||||
export default function DataBrowserGridControls({
|
||||
className,
|
||||
paginationProps,
|
||||
refetchData,
|
||||
onInsertRowClick,
|
||||
...props
|
||||
}: DataBrowserGridControlsProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { openAlertDialog } = useDialog();
|
||||
@@ -98,28 +97,26 @@ export default function DataBrowserGridControls({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={twMerge('sticky top-0 z-20 border-b-1 p-2', className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="box sticky top-0 z-20 border-b-1 p-2">
|
||||
<div
|
||||
className={twMerge(
|
||||
'mx-auto grid min-h-[38px] grid-flow-col items-center gap-3',
|
||||
className={cn(
|
||||
'mx-auto grid min-h-10 grid-flow-col items-center gap-3',
|
||||
numberOfSelectedRows > 0 ? 'justify-between' : 'justify-end',
|
||||
)}
|
||||
>
|
||||
{numberOfSelectedRows > 0 && (
|
||||
<div className="grid grid-flow-col place-content-start items-center gap-2">
|
||||
<Chip
|
||||
size="small"
|
||||
color="info"
|
||||
label={`${numberOfSelectedRows} selected`}
|
||||
/>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="!bg-[#ebf3ff] text-primary dark:!bg-[#1b2534]"
|
||||
>
|
||||
{`${numberOfSelectedRows} selected`}
|
||||
</Badge>
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="error"
|
||||
size="small"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-none text-destructive hover:bg-[#f131541a] hover:text-destructive"
|
||||
loading={status === 'loading'}
|
||||
onClick={() =>
|
||||
openAlertDialog({
|
||||
@@ -146,6 +143,11 @@ export default function DataBrowserGridControls({
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
{numberOfSelectedRows === 1 && (
|
||||
<InvokeEventTriggerButton
|
||||
selectedValues={selectedRows[0].values}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -160,17 +162,13 @@ export default function DataBrowserGridControls({
|
||||
{...restPaginationProps}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
startIcon={<PlusIcon className="h-4 w-4" />}
|
||||
size="small"
|
||||
onClick={onInsertRowClick}
|
||||
>
|
||||
Insert row
|
||||
<DataGridCustomizerControls />
|
||||
<Button onClick={onInsertRowClick} size="sm">
|
||||
<Plus className="h-4 w-4" /> Insert row
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,42 +1,32 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { NavLink } from '@/components/common/NavLink';
|
||||
import { FormActivityIndicator } from '@/components/form/FormActivityIndicator';
|
||||
import { InlineCode } from '@/components/presentational/InlineCode';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Backdrop } from '@/components/ui/v2/Backdrop';
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Chip } from '@/components/ui/v2/Chip';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { Dropdown } from '@/components/ui/v2/Dropdown';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { DotsHorizontalIcon } from '@/components/ui/v2/icons/DotsHorizontalIcon';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { LockIcon } from '@/components/ui/v2/icons/LockIcon';
|
||||
import { PencilIcon } from '@/components/ui/v2/icons/PencilIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { TerminalIcon } from '@/components/ui/v2/icons/TerminalIcon';
|
||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { UsersIcon } from '@/components/ui/v2/icons/UsersIcon';
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { Select } from '@/components/ui/v2/Select';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Badge } from '@/components/ui/v3/badge';
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import { InlineCode } from '@/components/ui/v3/inline-code';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/v3/select';
|
||||
import { Spinner } from '@/components/ui/v3/spinner';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useDatabaseQuery } from '@/features/orgs/projects/database/dataGrid/hooks/useDatabaseQuery';
|
||||
import { useDeleteTableWithToastMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useDeleteTableMutation';
|
||||
import { isSchemaLocked } from '@/features/orgs/projects/database/dataGrid/utils/schemaHelpers';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { isEmptyValue, isNotEmptyValue } from '@/lib/utils';
|
||||
import { cn, isEmptyValue, isNotEmptyValue } from '@/lib/utils';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Info, Lock, Plus, Terminal } from 'lucide-react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Image from 'next/image';
|
||||
import NextLink from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import TableActions from './TableActions';
|
||||
|
||||
const CreateTableForm = dynamic(
|
||||
() =>
|
||||
@@ -71,16 +61,17 @@ const EditPermissionsForm = dynamic(
|
||||
},
|
||||
);
|
||||
|
||||
export interface DataBrowserSidebarProps extends Omit<BoxProps, 'children'> {
|
||||
/**
|
||||
* Function to be called when a sidebar item is clicked.
|
||||
*/
|
||||
export interface DataBrowserSidebarProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface DataBrowserSidebarContentProps {
|
||||
onSidebarItemClick?: (tablePath?: string) => void;
|
||||
}
|
||||
|
||||
function DataBrowserSidebarContent({
|
||||
onSidebarItemClick,
|
||||
}: Pick<DataBrowserSidebarProps, 'onSidebarItemClick'>) {
|
||||
}: DataBrowserSidebarContentProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { openDrawer, openAlertDialog } = useDialog();
|
||||
const { project } = useProject();
|
||||
@@ -136,11 +127,12 @@ function DataBrowserSidebarContent({
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading schemas and tables..."
|
||||
className="justify-center"
|
||||
/>
|
||||
<Spinner
|
||||
wrapperClassName="flex-row text-[12px] leading-[1.66] font-normal gap-1"
|
||||
className="h-4 w-4 justify-center"
|
||||
>
|
||||
Loading schemas and tables...
|
||||
</Spinner>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -252,7 +244,12 @@ function DataBrowserSidebarContent({
|
||||
<span className="inline-grid grid-flow-col items-center gap-2">
|
||||
Permissions
|
||||
<InlineCode className="!text-sm+ font-normal">{table}</InlineCode>
|
||||
<Chip label="Preview" size="small" color="info" component="span" />
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-[#ebf3ff] text-primary dark:bg-[#1b2534]"
|
||||
>
|
||||
Preview
|
||||
</Badge>
|
||||
</span>
|
||||
),
|
||||
component: (
|
||||
@@ -271,59 +268,46 @@ function DataBrowserSidebarContent({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="flex h-full flex-col justify-between">
|
||||
<Box className="flex flex-col px-2">
|
||||
<div className="flex h-full flex-col justify-between">
|
||||
<div className="box flex flex-col px-2">
|
||||
{schemas && schemas.length > 0 && (
|
||||
<Select
|
||||
renderValue={(option) => (
|
||||
<span className="grid grid-flow-col items-center gap-1">
|
||||
{option?.label}
|
||||
</span>
|
||||
)}
|
||||
slotProps={{
|
||||
listbox: { className: 'max-w-[220px] min-w-[initial] w-full' },
|
||||
popper: { className: 'max-w-[220px] min-w-[initial] w-full' },
|
||||
}}
|
||||
value={selectedSchema}
|
||||
onChange={(_event, value) => setSelectedSchema(value as string)}
|
||||
>
|
||||
{schemas.map((schema) => (
|
||||
<Option
|
||||
className="grid grid-flow-col items-center gap-1"
|
||||
value={schema.schema_name}
|
||||
key={schema.schema_name}
|
||||
>
|
||||
<Text className="text-sm">
|
||||
<Text component="span" color="disabled">
|
||||
schema.
|
||||
</Text>
|
||||
<Text component="span" className="font-medium">
|
||||
{schema.schema_name}
|
||||
</Text>
|
||||
</Text>
|
||||
{(isSchemaLocked(schema.schema_name) || isGitHubConnected) && (
|
||||
<LockIcon
|
||||
className="h-3 w-3"
|
||||
sx={{ color: 'text.secondary' }}
|
||||
/>
|
||||
)}
|
||||
</Option>
|
||||
))}
|
||||
<Select value={selectedSchema} onValueChange={setSelectedSchema}>
|
||||
<SelectTrigger className="w-full min-w-[initial] max-w-[220px]">
|
||||
<SelectValue placeholder="Is null?" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{schemas.map((schema) => (
|
||||
<SelectItem value={schema.schema_name} key={schema.schema_name}>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm">
|
||||
<span className="text-disabled">schema.</span>
|
||||
<span className="font-medium">{schema.schema_name}</span>
|
||||
</p>
|
||||
{(isSchemaLocked(schema.schema_name) ||
|
||||
isGitHubConnected) && (
|
||||
<Lock
|
||||
className="text-[#556378] dark:text-[#a2b3be]"
|
||||
size={12}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
{isGitHubConnected && (
|
||||
<Box className="mt-1.5 flex items-center gap-1 px-2">
|
||||
<InfoIcon className="h-4 w-4" sx={{ color: 'text.secondary' }} />
|
||||
<Text className="text-xs" color="secondary">
|
||||
<div className="box mt-1.5 flex items-center gap-1 px-2">
|
||||
<Info className="h-4 w-4 text-disabled" />
|
||||
<p className="text-xs text-disabled">
|
||||
GitHub connected - use the CLI for schema changes
|
||||
</Text>
|
||||
</Box>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!isSelectedSchemaLocked && (
|
||||
<Button
|
||||
variant="borderless"
|
||||
endIcon={<PlusIcon />}
|
||||
className="mt-1 w-full justify-between px-2"
|
||||
variant="link"
|
||||
className="mt-1 flex w-full justify-between px-[0.625rem] !text-sm+ text-primary hover:bg-accent hover:no-underline disabled:text-disabled"
|
||||
onClick={() => {
|
||||
openDrawer({
|
||||
title: 'Create a New Table',
|
||||
@@ -335,202 +319,136 @@ function DataBrowserSidebarContent({
|
||||
}}
|
||||
disabled={isGitHubConnected}
|
||||
>
|
||||
New Table
|
||||
New Table <Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{isNotEmptyValue(schemas) && isEmptyValue(tablesInSelectedSchema) && (
|
||||
<Text className="px-2 py-1.5 text-xs" color="disabled">
|
||||
No tables found.
|
||||
</Text>
|
||||
<p className="px-2 py-1.5 text-xs text-disabled">No tables found.</p>
|
||||
)}
|
||||
<nav aria-label="Database navigation">
|
||||
{isNotEmptyValue(tablesInSelectedSchema) && (
|
||||
<List className="grid gap-1 pb-6">
|
||||
<ul className="w-full max-w-full pb-6">
|
||||
{tablesInSelectedSchema.map((table) => {
|
||||
const tablePath = `${table.table_schema}.${table.table_name}`;
|
||||
const isSelected = `${schemaSlug}.${tableSlug}` === tablePath;
|
||||
const isSidebarMenuOpen = sidebarMenuTable === tablePath;
|
||||
return (
|
||||
<ListItem.Root
|
||||
className="group"
|
||||
key={tablePath}
|
||||
secondaryAction={
|
||||
<Dropdown.Root
|
||||
id="table-management-menu"
|
||||
onOpen={() => setSidebarMenuTable(tablePath)}
|
||||
onClose={() => setSidebarMenuTable(undefined)}
|
||||
>
|
||||
<Dropdown.Trigger
|
||||
asChild
|
||||
hideChevron
|
||||
disabled={tablePath === removableTable}
|
||||
>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color={isSelected ? 'primary' : 'secondary'}
|
||||
className={twMerge(
|
||||
!isSelected &&
|
||||
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100',
|
||||
)}
|
||||
>
|
||||
<DotsHorizontalIcon />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Content
|
||||
menu
|
||||
PaperProps={{ className: 'w-52' }}
|
||||
>
|
||||
{isGitHubConnected ? (
|
||||
<Dropdown.Item
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
onClick={() =>
|
||||
handleEditPermissionClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
true,
|
||||
)
|
||||
}
|
||||
>
|
||||
<UsersIcon
|
||||
className="h-4 w-4"
|
||||
sx={{ color: 'text.secondary' }}
|
||||
/>
|
||||
<span>View Permissions</span>
|
||||
</Dropdown.Item>
|
||||
) : (
|
||||
[
|
||||
!isSelectedSchemaLocked && (
|
||||
<Dropdown.Item
|
||||
key="edit-table"
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
onClick={() =>
|
||||
openDrawer({
|
||||
title: 'Edit Table',
|
||||
component: (
|
||||
<EditTableForm
|
||||
onSubmit={async (tableName) => {
|
||||
await queryClient.refetchQueries([
|
||||
`${dataSourceSlug}.${table.table_schema}.${tableName}`,
|
||||
]);
|
||||
await refetch();
|
||||
}}
|
||||
schema={table.table_schema}
|
||||
table={table}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
<PencilIcon
|
||||
className="h-4 w-4"
|
||||
sx={{ color: 'text.secondary' }}
|
||||
/>
|
||||
<span>Edit Table</span>
|
||||
</Dropdown.Item>
|
||||
),
|
||||
!isSelectedSchemaLocked && (
|
||||
<Divider
|
||||
key="edit-table-separator"
|
||||
component="li"
|
||||
/>
|
||||
),
|
||||
<Dropdown.Item
|
||||
key="edit-permissions"
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
onClick={() =>
|
||||
handleEditPermissionClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
)
|
||||
}
|
||||
>
|
||||
<UsersIcon
|
||||
className="h-4 w-4"
|
||||
sx={{ color: 'text.secondary' }}
|
||||
/>
|
||||
<span>Edit Permissions</span>
|
||||
</Dropdown.Item>,
|
||||
!isSelectedSchemaLocked && (
|
||||
<Divider
|
||||
key="edit-permissions-separator"
|
||||
component="li"
|
||||
/>
|
||||
),
|
||||
!isSelectedSchemaLocked && (
|
||||
<Dropdown.Item
|
||||
key="delete-table"
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
sx={{ color: 'error.main' }}
|
||||
onClick={() =>
|
||||
handleDeleteTableClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
)
|
||||
}
|
||||
>
|
||||
<TrashIcon
|
||||
className="h-4 w-4"
|
||||
sx={{ color: 'error.main' }}
|
||||
/>
|
||||
<span>Delete Table</span>
|
||||
</Dropdown.Item>
|
||||
),
|
||||
]
|
||||
)}
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
}
|
||||
>
|
||||
<ListItem.Button
|
||||
dense
|
||||
selected={isSelected}
|
||||
<li className="group pb-1" key={tablePath}>
|
||||
<Button
|
||||
asChild
|
||||
variant="link"
|
||||
size="sm"
|
||||
disabled={tablePath === removableTable}
|
||||
className="group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9"
|
||||
sx={{
|
||||
paddingRight:
|
||||
(isSelected || isSidebarMenuOpen) &&
|
||||
'2.25rem !important',
|
||||
}}
|
||||
component={NavLink}
|
||||
href={`/orgs/${orgSlug}/projects/${appSubdomain}/database/browser/default/${table.table_schema}/${table.table_name}`}
|
||||
onClick={() => {
|
||||
if (onSidebarItemClick) {
|
||||
onSidebarItemClick(`default.${tablePath}`);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'flex w-full max-w-full justify-between pl-0 text-sm+ hover:bg-accent hover:no-underline',
|
||||
{
|
||||
'bg-table-selected': isSelected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<ListItem.Text>{table.table_name}</ListItem.Text>
|
||||
</ListItem.Button>
|
||||
</ListItem.Root>
|
||||
<div>
|
||||
<NextLink
|
||||
className={cn(
|
||||
'flex h-full w-[calc(100%-1.6rem)] items-center p-[0.625rem] pr-0 text-left',
|
||||
{
|
||||
'text-primary-main': isSelected,
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
if (onSidebarItemClick) {
|
||||
onSidebarItemClick(`default.${tablePath}`);
|
||||
}
|
||||
}}
|
||||
href={`/orgs/${orgSlug}/projects/${appSubdomain}/database/browser/default/${table.table_schema}/${table.table_name}`}
|
||||
>
|
||||
<span className="!truncate text-ellipsis">
|
||||
{table.table_name}
|
||||
</span>
|
||||
</NextLink>
|
||||
<TableActions
|
||||
tableName={table.table_name}
|
||||
disabled={tablePath === removableTable}
|
||||
open={isSidebarMenuOpen}
|
||||
onOpen={() => setSidebarMenuTable(tablePath)}
|
||||
onClose={() => setSidebarMenuTable(undefined)}
|
||||
className={cn(
|
||||
'relative z-10 opacity-0 group-hover:opacity-100',
|
||||
{
|
||||
'opacity-100': isSelected || isSidebarMenuOpen,
|
||||
},
|
||||
)}
|
||||
isSelectedNotSchemaLocked={!isSelectedSchemaLocked}
|
||||
onViewPermissions={() =>
|
||||
handleEditPermissionClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
true,
|
||||
)
|
||||
}
|
||||
onEditTable={() =>
|
||||
openDrawer({
|
||||
title: 'Edit Table',
|
||||
component: (
|
||||
<EditTableForm
|
||||
onSubmit={async (tableName) => {
|
||||
await queryClient.refetchQueries([
|
||||
`${dataSourceSlug}.${table.table_schema}.${tableName}`,
|
||||
]);
|
||||
await refetch();
|
||||
}}
|
||||
schema={table.table_schema}
|
||||
table={table}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
onEditPermissions={() =>
|
||||
handleEditPermissionClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
)
|
||||
}
|
||||
onDelete={() =>
|
||||
handleDeleteTableClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</ul>
|
||||
)}
|
||||
</nav>
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
<Box className="border-t">
|
||||
<ListItem.Button
|
||||
dense
|
||||
selected={asPath === sqlEditorHref}
|
||||
className="flex border group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9"
|
||||
component={NavLink}
|
||||
href={sqlEditorHref}
|
||||
<div className="box border-t">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
asChild
|
||||
className={cn(
|
||||
'flex rounded-none border text-sm+ hover:bg-accent hover:no-underline group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9',
|
||||
{ 'bg-table-selected text-primary-main': asPath === sqlEditorHref },
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-center space-x-4">
|
||||
<TerminalIcon />
|
||||
<span className="flex">SQL Editor</span>
|
||||
</div>
|
||||
</ListItem.Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<NextLink href={sqlEditorHref}>
|
||||
<div className="flex w-full flex-row items-center justify-center space-x-4">
|
||||
<Terminal />
|
||||
<span className="flex">SQL Editor</span>
|
||||
</div>
|
||||
</NextLink>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DataBrowserSidebar({
|
||||
className,
|
||||
onSidebarItemClick,
|
||||
...props
|
||||
}: DataBrowserSidebarProps) {
|
||||
const isPlatform = useIsPlatform();
|
||||
const { project } = useProject();
|
||||
@@ -541,11 +459,7 @@ export default function DataBrowserSidebar({
|
||||
setExpanded(!expanded);
|
||||
}
|
||||
|
||||
function handleSidebarItemClick(tablePath?: string) {
|
||||
if (onSidebarItemClick && tablePath) {
|
||||
onSidebarItemClick(tablePath);
|
||||
}
|
||||
|
||||
function handleSidebarItemClick() {
|
||||
setExpanded(false);
|
||||
}
|
||||
|
||||
@@ -586,24 +500,24 @@ export default function DataBrowserSidebar({
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box
|
||||
component="aside"
|
||||
className={twMerge(
|
||||
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 pb-17 pt-2 motion-safe:transition-transform sm:relative sm:z-0 sm:h-full sm:pb-0 sm:pt-2.5 sm:transition-none',
|
||||
<aside
|
||||
className={cn(
|
||||
'box absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 pb-17 pt-2 motion-safe:transition-transform sm:relative sm:z-0 sm:h-full sm:pb-0 sm:pt-2.5 sm:transition-none',
|
||||
expanded ? 'translate-x-0' : '-translate-x-full sm:translate-x-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RetryableErrorBoundary>
|
||||
<DataBrowserSidebarContent
|
||||
onSidebarItemClick={handleSidebarItemClick}
|
||||
/>
|
||||
</RetryableErrorBoundary>
|
||||
</Box>
|
||||
</aside>
|
||||
|
||||
<IconButton
|
||||
className="absolute bottom-4 left-8 z-[38] h-11 w-11 rounded-full md:hidden"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="absolute bottom-4 left-8 z-[38] h-11 w-11 rounded-full bg-primary md:hidden"
|
||||
onClick={toggleExpanded}
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
@@ -613,7 +527,7 @@ export default function DataBrowserSidebar({
|
||||
src="/assets/table.svg"
|
||||
alt="A monochrome table"
|
||||
/>
|
||||
</IconButton>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/v3/dropdown-menu';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Ellipsis, SquarePen, Trash2, Users } from 'lucide-react';
|
||||
|
||||
const menuItemClassName =
|
||||
'flex hover:cursor-pointer hover:bg-data-cell-bg h-9 font-medium items-center justify-start gap-2 rounded-none border border-b-1 text-sm+ leading-4';
|
||||
|
||||
type Props = {
|
||||
tableName: string;
|
||||
open: boolean;
|
||||
className?: string;
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
disabled: boolean;
|
||||
isSelectedNotSchemaLocked: boolean;
|
||||
onDelete: () => void;
|
||||
onEditPermissions: () => void;
|
||||
onViewPermissions: () => void;
|
||||
onEditTable: () => void;
|
||||
};
|
||||
|
||||
function TableActions({
|
||||
tableName,
|
||||
open,
|
||||
className,
|
||||
onClose,
|
||||
onOpen,
|
||||
disabled,
|
||||
isSelectedNotSchemaLocked,
|
||||
onDelete,
|
||||
onEditPermissions,
|
||||
onViewPermissions,
|
||||
onEditTable,
|
||||
}: Props) {
|
||||
const { project } = useProject();
|
||||
const isGitHubConnected = !!project?.githubRepository;
|
||||
|
||||
function handleOnOpenChange(newOpenState: boolean) {
|
||||
if (newOpenState) {
|
||||
onOpen();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={handleOnOpenChange}>
|
||||
<DropdownMenuTrigger
|
||||
className={cn(className)}
|
||||
disabled={disabled}
|
||||
asChild
|
||||
>
|
||||
<Button
|
||||
id={`table-management-menu-${tableName}`}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-6 w-6 border-none bg-transparent px-0 hover:bg-transparent"
|
||||
>
|
||||
<Ellipsis />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="start" className="w-52 p-0">
|
||||
{isGitHubConnected ? (
|
||||
<DropdownMenuItem
|
||||
className={menuItemClassName}
|
||||
onClick={onViewPermissions}
|
||||
>
|
||||
<Users className="h-4 w-4" /> <span>View Permissions</span>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<>
|
||||
{isSelectedNotSchemaLocked && (
|
||||
<DropdownMenuItem
|
||||
className={menuItemClassName}
|
||||
onClick={onEditTable}
|
||||
>
|
||||
<SquarePen className="h-4 w-4" /> <span>Edit Table</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className={menuItemClassName}
|
||||
onClick={onEditPermissions}
|
||||
>
|
||||
<Users className="h-4 w-4" /> <span>Edit Permissions</span>
|
||||
</DropdownMenuItem>
|
||||
{isSelectedNotSchemaLocked && (
|
||||
<DropdownMenuItem
|
||||
className={cn(
|
||||
menuItemClassName,
|
||||
'!text-sm+ font-medium !text-destructive',
|
||||
)}
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span>Delete Table</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export default TableActions;
|
||||
@@ -1,19 +1,18 @@
|
||||
import { InlineCode } from '@/components/presentational/InlineCode';
|
||||
import { FormInput } from '@/components/form/FormInput';
|
||||
import { FormSelect } from '@/components/form/FormSelect';
|
||||
import { FormTextarea } from '@/components/form/FormTextarea';
|
||||
import { ReadOnlyToggle } from '@/components/presentational/ReadOnlyToggle';
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { KeyIcon } from '@/components/ui/v2/icons/KeyIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { Select } from '@/components/ui/v2/Select';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { InlineCode } from '@/components/ui/v3/inline-code';
|
||||
import { SelectItem } from '@/components/ui/v3/select';
|
||||
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import { getInputType } from '@/features/orgs/projects/database/dataGrid/utils/inputHelpers';
|
||||
import { normalizeDefaultValue } from '@/features/orgs/projects/database/dataGrid/utils/normalizeDefaultValue';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { KeyRound } from 'lucide-react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
export interface DatabaseRecordInputGroupProps extends BoxProps {
|
||||
export interface DatabaseRecordInputGroupProps {
|
||||
/**
|
||||
* List of columns for which input fields should be generated.
|
||||
*/
|
||||
@@ -30,6 +29,25 @@ export interface DatabaseRecordInputGroupProps extends BoxProps {
|
||||
* Determines whether the first input field should be focused.
|
||||
*/
|
||||
autoFocusFirstInput?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function getBooleanValueTransformer(isNullable: boolean) {
|
||||
return function transformBooleanValue(value: string | null) {
|
||||
let convertedValue = value;
|
||||
|
||||
if (convertedValue === null) {
|
||||
convertedValue = isNullable ? 'null' : '';
|
||||
} else if (convertedValue === 'null' || convertedValue === '') {
|
||||
convertedValue = null;
|
||||
}
|
||||
|
||||
return convertedValue;
|
||||
};
|
||||
}
|
||||
|
||||
function convertNullToEmptyString(value: string | null) {
|
||||
return value === null ? '' : value;
|
||||
}
|
||||
|
||||
function getPlaceholder(
|
||||
@@ -71,28 +89,30 @@ export default function DatabaseRecordInputGroup({
|
||||
columns,
|
||||
autoFocusFirstInput,
|
||||
className,
|
||||
...props
|
||||
}: DatabaseRecordInputGroupProps) {
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
const { control } = useFormContext();
|
||||
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
function getRef(index: number) {
|
||||
return (element: HTMLTextAreaElement | HTMLInputElement | null) => {
|
||||
if (element && index === 0 && autoFocusFirstInput) {
|
||||
inputRef.current = element;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Box component="section" className={twMerge('py-3', className)} {...props}>
|
||||
{title && (
|
||||
<Text variant="h2" className="mb-1.5 mt-3 text-sm+ font-bold">
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<section className={cn('box py-3 font-display', className)}>
|
||||
{title && <h2 className="mb-1.5 mt-3 text-sm+ font-bold">{title}</h2>}
|
||||
{description && (
|
||||
<Text className="mb-3 text-xs" color="secondary">
|
||||
{description}
|
||||
</Text>
|
||||
<p className="mb-3 text-xs text-secondary">{description}</p>
|
||||
)}
|
||||
|
||||
<div>
|
||||
{columns.map(
|
||||
(
|
||||
@@ -122,99 +142,77 @@ export default function DatabaseRecordInputGroup({
|
||||
isNullable,
|
||||
);
|
||||
|
||||
const InputLabel = (
|
||||
const inputLabel = (
|
||||
<span className="inline-grid grid-flow-col gap-1">
|
||||
<span className="inline-grid grid-flow-col items-center gap-1">
|
||||
{isPrimary && <KeyIcon className="text-base text-inherit" />}
|
||||
|
||||
<span className="inline-grid grid-flow-col items-center gap-1 break-all">
|
||||
{isPrimary && (
|
||||
<KeyRound className="text-base text-inherit" size={13} />
|
||||
)}
|
||||
<span>{columnId}</span>
|
||||
</span>
|
||||
|
||||
<InlineCode className="h-[18px]">
|
||||
<InlineCode
|
||||
className="h-[1.125rem] overflow-hidden whitespace-nowrap leading-[1.125rem]"
|
||||
style={{ textOverflow: 'clip' }}
|
||||
title={specificType}
|
||||
>
|
||||
{specificType}
|
||||
{maxLength ? `(${maxLength})` : null}
|
||||
</InlineCode>
|
||||
</span>
|
||||
);
|
||||
|
||||
const commonFormControlProps = {
|
||||
label: InputLabel,
|
||||
error: Boolean(errors[columnId]),
|
||||
helperText:
|
||||
comment ||
|
||||
(typeof errors[columnId]?.message === 'string'
|
||||
? (errors[columnId]?.message as string)
|
||||
: null),
|
||||
hideEmptyHelperText: true,
|
||||
fullWidth: true,
|
||||
className: 'py-3',
|
||||
};
|
||||
|
||||
const commonLabelProps = {
|
||||
className: 'grid grid-flow-row justify-items-start gap-1',
|
||||
};
|
||||
|
||||
if (type === 'boolean') {
|
||||
return (
|
||||
<Controller
|
||||
<FormSelect
|
||||
key={columnId}
|
||||
inline
|
||||
name={columnId}
|
||||
control={control}
|
||||
key={columnId}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...commonFormControlProps}
|
||||
{...field}
|
||||
onChange={(_event, value) => field.onChange(value)}
|
||||
variant="inline"
|
||||
id={columnId}
|
||||
value={field.value || 'null'}
|
||||
placeholder="Select an option"
|
||||
className={twMerge(
|
||||
!field.value && 'text-sm font-normal',
|
||||
'py-3',
|
||||
)}
|
||||
autoFocus={index === 0 && autoFocusFirstInput}
|
||||
slotProps={{ label: commonLabelProps }}
|
||||
>
|
||||
<Option value="true">
|
||||
<ReadOnlyToggle checked />
|
||||
</Option>
|
||||
label={inputLabel}
|
||||
placeholder="Select an option"
|
||||
helperText={comment}
|
||||
transformValue={getBooleanValueTransformer(!!isNullable)}
|
||||
>
|
||||
<SelectItem value="true">
|
||||
<ReadOnlyToggle checked />
|
||||
</SelectItem>
|
||||
|
||||
<Option value="false">
|
||||
<ReadOnlyToggle checked={false} />
|
||||
</Option>
|
||||
<SelectItem value="false">
|
||||
<ReadOnlyToggle checked={false} />
|
||||
</SelectItem>
|
||||
|
||||
{isNullable && (
|
||||
<Option value="null">
|
||||
<ReadOnlyToggle checked={null} />
|
||||
</Option>
|
||||
)}
|
||||
</Select>
|
||||
{isNullable && (
|
||||
<SelectItem value="null">
|
||||
<ReadOnlyToggle checked={null} />
|
||||
</SelectItem>
|
||||
)}
|
||||
/>
|
||||
</FormSelect>
|
||||
);
|
||||
}
|
||||
|
||||
const InputComponent = isMultiline ? FormTextarea : FormInput;
|
||||
return (
|
||||
<Input
|
||||
{...commonFormControlProps}
|
||||
{...register(columnId)}
|
||||
variant="inline"
|
||||
id={columnId}
|
||||
<InputComponent
|
||||
ref={getRef(index)}
|
||||
key={columnId}
|
||||
type={getInputType({ type, specificType })}
|
||||
inline
|
||||
name={columnId}
|
||||
control={control}
|
||||
label={inputLabel}
|
||||
placeholder={placeholder}
|
||||
multiline={isMultiline}
|
||||
rows={5}
|
||||
autoFocus={index === 0 && autoFocusFirstInput}
|
||||
slotProps={{
|
||||
label: commonLabelProps,
|
||||
}}
|
||||
helperText={comment}
|
||||
transformValue={convertNullToEmptyString}
|
||||
className={cn(
|
||||
{ 'resize-none': isMultiline },
|
||||
'focus-visible:ring-0',
|
||||
)}
|
||||
type={getInputType({ type, specificType })}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import { Dialog } from '@/components/ui/v3/dialog';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/v3/dropdown-menu';
|
||||
import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/generateAppServiceUrl';
|
||||
import { InvocationLogDetailsDialogContent } from '@/features/orgs/projects/events/event-triggers/components/InvocationLogDetailsDialogContent';
|
||||
import { DEFAULT_RETRY_TIMEOUT_SECONDS } from '@/features/orgs/projects/events/event-triggers/constants';
|
||||
import fetchEventAndInvocationLogsById from '@/features/orgs/projects/events/event-triggers/hooks/useGetEventAndInvocationLogsById/fetchEventAndInvocationLogsById';
|
||||
import { useGetEventTriggersByTable } from '@/features/orgs/projects/events/event-triggers/hooks/useGetEventTriggersByTable';
|
||||
import { useInvokeEventTriggerMutation } from '@/features/orgs/projects/events/event-triggers/hooks/useInvokeEventTriggerMutation';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import type { EventInvocationLogEntry } from '@/utils/hasura-api/generated/schemas/eventInvocationLogEntry';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useRef, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export interface InvokeEventTriggerButtonProps {
|
||||
selectedValues: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export default function InvokeEventTriggerButton({
|
||||
selectedValues,
|
||||
}: InvokeEventTriggerButtonProps) {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [newLog, setNewLog] = useState<EventInvocationLogEntry | null>(null);
|
||||
const { project } = useProject();
|
||||
const appUrl = generateAppServiceUrl(
|
||||
project?.subdomain!,
|
||||
project?.region!,
|
||||
'hasura',
|
||||
);
|
||||
|
||||
const adminSecret = project?.config?.hasura.adminSecret!;
|
||||
|
||||
const router = useRouter();
|
||||
const { dataSourceSlug, schemaSlug, tableSlug } = router.query;
|
||||
|
||||
const { data: eventTriggerNames, isLoading } = useGetEventTriggersByTable({
|
||||
table: { name: tableSlug as string, schema: schemaSlug as string },
|
||||
dataSource: dataSourceSlug as string,
|
||||
queryOptions: {
|
||||
enabled:
|
||||
typeof tableSlug === 'string' &&
|
||||
typeof schemaSlug === 'string' &&
|
||||
typeof dataSourceSlug === 'string',
|
||||
},
|
||||
});
|
||||
|
||||
const handleDialogOpenChange = (newOpen: boolean) => {
|
||||
if (!newOpen) {
|
||||
setNewLog(null);
|
||||
}
|
||||
setShowDialog(newOpen);
|
||||
};
|
||||
|
||||
const { mutateAsync: invokeEventTrigger } = useInvokeEventTriggerMutation();
|
||||
|
||||
const resetState = () => {
|
||||
setNewLog(null);
|
||||
setShowDialog(false);
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleInvokeEventTrigger = async (name: string) => {
|
||||
let eventId: string;
|
||||
try {
|
||||
const response = await invokeEventTrigger({
|
||||
args: {
|
||||
name,
|
||||
source: dataSourceSlug as string,
|
||||
payload: selectedValues,
|
||||
},
|
||||
});
|
||||
eventId = response.event_id;
|
||||
toast.success(
|
||||
'Event trigger invoked successfully, fetching invocation logs...',
|
||||
getToastStyleProps(),
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error('Failed to invoke event trigger', getToastStyleProps());
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
setShowDialog(true);
|
||||
|
||||
const start = Date.now();
|
||||
const timeoutMs = DEFAULT_RETRY_TIMEOUT_SECONDS * 1000; // TODO: Get from retry_conf.timeout_sec
|
||||
intervalRef.current = setInterval(async () => {
|
||||
const elapsed = Date.now() - start;
|
||||
if (elapsed >= timeoutMs && intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const newData = await fetchEventAndInvocationLogsById({
|
||||
appUrl,
|
||||
adminSecret,
|
||||
args: {
|
||||
event_id: eventId,
|
||||
source: dataSourceSlug as string,
|
||||
},
|
||||
});
|
||||
const firstInvocation = newData?.invocations?.[0];
|
||||
if (firstInvocation) {
|
||||
setNewLog(firstInvocation);
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to fetch invocation logs', getToastStyleProps());
|
||||
resetState();
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" aria-label="Open menu" size="sm">
|
||||
Invoke Event Trigger
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-40" align="end">
|
||||
<DropdownMenuLabel>Invoke</DropdownMenuLabel>
|
||||
<DropdownMenuGroup>
|
||||
{isLoading ? (
|
||||
<DropdownMenuItem disabled>
|
||||
<Loader2 className="mr-2 size-4 animate-spin" />
|
||||
Loading...
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
eventTriggerNames?.map((name) => (
|
||||
<DropdownMenuItem
|
||||
key={name}
|
||||
onSelect={() => handleInvokeEventTrigger(name)}
|
||||
>
|
||||
{name}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Dialog open={showDialog} onOpenChange={handleDialogOpenChange}>
|
||||
<InvocationLogDetailsDialogContent log={newLog} isLoading={!newLog} />
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as InvokeEventTriggerButton } from './InvokeEventTriggerButton';
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -70,9 +69,9 @@ export default function RuleGroupControls({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Text className="p-2 !font-medium">
|
||||
<p className="p-2 !font-medium">
|
||||
{operatorDictionary[currentOperator]}
|
||||
</Text>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -147,7 +147,7 @@ function RuleValueInput({
|
||||
if (operator === '_is_null') {
|
||||
const defaultValue = comboboxValue ?? undefined;
|
||||
const triggerClasses =
|
||||
'border hover:bg-accent hover:text-accent-foreground focus:ring-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2';
|
||||
'border hover:bg-accent-background hover:text-accent-foreground focus:ring-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2';
|
||||
return (
|
||||
<Select
|
||||
disabled={disabled}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { showLoadingToast } from '@/utils/toast';
|
||||
import { useEffect, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import type { UseUpdateRecordMutationOptions } from './useUpdateRecordMutation';
|
||||
@@ -24,6 +25,7 @@ export default function useUpdateRecordWithToastMutation(
|
||||
if (status === 'loading') {
|
||||
const loadingToastId = showLoadingToast('Saving data...', {
|
||||
id: 'data-browser-data-save',
|
||||
...getToastStyleProps(),
|
||||
});
|
||||
|
||||
setToastId(loadingToastId);
|
||||
@@ -35,9 +37,13 @@ export default function useUpdateRecordWithToastMutation(
|
||||
}
|
||||
|
||||
if (status === 'success' && toastId) {
|
||||
toast.remove(toastId);
|
||||
|
||||
triggerToast('Your changes were successfully saved.');
|
||||
setTimeout(() => {
|
||||
toast.remove(toastId);
|
||||
toast.success(
|
||||
'Your changes were successfully saved.',
|
||||
getToastStyleProps(),
|
||||
);
|
||||
}, 300);
|
||||
}
|
||||
}, [status, toastId]);
|
||||
|
||||
|
||||
@@ -480,6 +480,7 @@ export interface DataBrowserGridColumn<TData extends object = {}>
|
||||
* Determines whether or not the cell content is copiable.
|
||||
*/
|
||||
isCopiable?: boolean;
|
||||
dataType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,7 +19,7 @@ export const POSTGRESQL_ERROR_CODES = {
|
||||
*
|
||||
* @docs https://www.postgresql.org/docs/current/datatype-numeric.html
|
||||
*/
|
||||
export const POSTGRESQL_INTEGER_TYPES = [
|
||||
export const POSTGRESQL_NUMERIC_TYPES = [
|
||||
'smallint',
|
||||
'integer',
|
||||
'bigint',
|
||||
@@ -27,16 +27,21 @@ export const POSTGRESQL_INTEGER_TYPES = [
|
||||
'serial',
|
||||
'bigserial',
|
||||
'oid',
|
||||
'numeric',
|
||||
'real',
|
||||
'double precision',
|
||||
];
|
||||
|
||||
export const POSTGRESQL_DECIMAL_TYPES = ['numeric', 'real', 'double precision'];
|
||||
|
||||
/**
|
||||
* Character data types in PostgreSQL.
|
||||
*
|
||||
* @docs https://www.postgresql.org/docs/current/datatype-character.html
|
||||
*/
|
||||
export const POSTGRESQL_CHARACTER_TYPES = ['varchar', 'character', 'text'];
|
||||
export const POSTGRESQL_CHARACTER_TYPES = [
|
||||
'character varying',
|
||||
'character',
|
||||
'text',
|
||||
];
|
||||
|
||||
/**
|
||||
* JSON data types in PostgreSQL.
|
||||
|
||||
@@ -108,7 +108,6 @@ export function createDynamicValidationSchema(
|
||||
[column.id]: createUUIDValidationSchema(details),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
column.type === 'date' &&
|
||||
['time', 'timetz', 'interval'].includes(column.specificType as string)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user