Compare commits
23 Commits
cli@1.34.5
...
release/pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c9afc8980 | ||
|
|
ab890d8593 | ||
|
|
ee2d9763f7 | ||
|
|
f7ea20db61 | ||
|
|
99ac1aee3a | ||
|
|
bb9aaf2903 | ||
|
|
8e82edd0c6 | ||
|
|
b0256da33f | ||
|
|
351daa5fbe | ||
|
|
4e9de6a764 | ||
|
|
9dab347348 | ||
|
|
6c044458ca | ||
|
|
5b98f4ece2 | ||
|
|
dc7d3fb4cc | ||
|
|
d15717b67a | ||
|
|
a3c7f89eda | ||
|
|
6692e0dfc0 | ||
|
|
8541165781 | ||
|
|
5cd5ebbc65 | ||
|
|
5066ef708a | ||
|
|
a6a378c5a6 | ||
|
|
a3a3cf205d | ||
|
|
3fd2e63db3 |
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,20 @@
|
||||
## [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
|
||||
|
||||
@@ -56,7 +56,7 @@ func CommandCloud() *cli.Command {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagDashboardVersion,
|
||||
Usage: "Dashboard version to use",
|
||||
Value: "nhost/dashboard:2.41.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.41.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),
|
||||
|
||||
@@ -250,7 +250,7 @@ func traefik(subdomain, projectName string, port uint, dotnhostfolder string) (*
|
||||
}
|
||||
|
||||
return &Service{
|
||||
Image: "traefik:v3.1",
|
||||
Image: "traefik:v3.6",
|
||||
DependsOn: nil,
|
||||
EntryPoint: nil,
|
||||
Command: []string{
|
||||
|
||||
@@ -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"`
|
||||
@@ -4183,32 +4191,34 @@ type Plans struct {
|
||||
// An array relationship
|
||||
Organizations []*Organizations `json:"organizations"`
|
||||
Price int64 `json:"price"`
|
||||
SLALevel SLALevelEnum `json:"slaLevel"`
|
||||
Sort int64 `json:"sort"`
|
||||
UpatedAt time.Time `json:"upatedAt"`
|
||||
}
|
||||
|
||||
// Boolean expression to filter rows from the table "plans". All fields are combined with a logical 'AND'.
|
||||
type PlansBoolExp struct {
|
||||
And []*PlansBoolExp `json:"_and,omitempty"`
|
||||
Not *PlansBoolExp `json:"_not,omitempty"`
|
||||
Or []*PlansBoolExp `json:"_or,omitempty"`
|
||||
Apps *AppsBoolExp `json:"apps,omitempty"`
|
||||
CreatedAt *TimestamptzComparisonExp `json:"createdAt,omitempty"`
|
||||
Deprecated *BooleanComparisonExp `json:"deprecated,omitempty"`
|
||||
FeatureBackupEnabled *BooleanComparisonExp `json:"featureBackupEnabled,omitempty"`
|
||||
FeatureCustomDomainsEnabled *BooleanComparisonExp `json:"featureCustomDomainsEnabled,omitempty"`
|
||||
FeatureCustomEmailTemplatesEnabled *BooleanComparisonExp `json:"featureCustomEmailTemplatesEnabled,omitempty"`
|
||||
FeatureMaxDbSize *IntComparisonExp `json:"featureMaxDbSize,omitempty"`
|
||||
ID *UUIDComparisonExp `json:"id,omitempty"`
|
||||
Individual *BooleanComparisonExp `json:"individual,omitempty"`
|
||||
IsDefault *BooleanComparisonExp `json:"isDefault,omitempty"`
|
||||
IsFree *BooleanComparisonExp `json:"isFree,omitempty"`
|
||||
IsPublic *BooleanComparisonExp `json:"isPublic,omitempty"`
|
||||
Name *StringComparisonExp `json:"name,omitempty"`
|
||||
Organizations *OrganizationsBoolExp `json:"organizations,omitempty"`
|
||||
Price *IntComparisonExp `json:"price,omitempty"`
|
||||
Sort *IntComparisonExp `json:"sort,omitempty"`
|
||||
UpatedAt *TimestamptzComparisonExp `json:"upatedAt,omitempty"`
|
||||
And []*PlansBoolExp `json:"_and,omitempty"`
|
||||
Not *PlansBoolExp `json:"_not,omitempty"`
|
||||
Or []*PlansBoolExp `json:"_or,omitempty"`
|
||||
Apps *AppsBoolExp `json:"apps,omitempty"`
|
||||
CreatedAt *TimestamptzComparisonExp `json:"createdAt,omitempty"`
|
||||
Deprecated *BooleanComparisonExp `json:"deprecated,omitempty"`
|
||||
FeatureBackupEnabled *BooleanComparisonExp `json:"featureBackupEnabled,omitempty"`
|
||||
FeatureCustomDomainsEnabled *BooleanComparisonExp `json:"featureCustomDomainsEnabled,omitempty"`
|
||||
FeatureCustomEmailTemplatesEnabled *BooleanComparisonExp `json:"featureCustomEmailTemplatesEnabled,omitempty"`
|
||||
FeatureMaxDbSize *IntComparisonExp `json:"featureMaxDbSize,omitempty"`
|
||||
ID *UUIDComparisonExp `json:"id,omitempty"`
|
||||
Individual *BooleanComparisonExp `json:"individual,omitempty"`
|
||||
IsDefault *BooleanComparisonExp `json:"isDefault,omitempty"`
|
||||
IsFree *BooleanComparisonExp `json:"isFree,omitempty"`
|
||||
IsPublic *BooleanComparisonExp `json:"isPublic,omitempty"`
|
||||
Name *StringComparisonExp `json:"name,omitempty"`
|
||||
Organizations *OrganizationsBoolExp `json:"organizations,omitempty"`
|
||||
Price *IntComparisonExp `json:"price,omitempty"`
|
||||
SLALevel *SLALevelEnumComparisonExp `json:"slaLevel,omitempty"`
|
||||
Sort *IntComparisonExp `json:"sort,omitempty"`
|
||||
UpatedAt *TimestamptzComparisonExp `json:"upatedAt,omitempty"`
|
||||
}
|
||||
|
||||
// Ordering options when selecting data from "plans".
|
||||
@@ -4228,6 +4238,7 @@ type PlansOrderBy struct {
|
||||
Name *OrderBy `json:"name,omitempty"`
|
||||
OrganizationsAggregate *OrganizationsAggregateOrderBy `json:"organizations_aggregate,omitempty"`
|
||||
Price *OrderBy `json:"price,omitempty"`
|
||||
SLALevel *OrderBy `json:"slaLevel,omitempty"`
|
||||
Sort *OrderBy `json:"sort,omitempty"`
|
||||
UpatedAt *OrderBy `json:"upatedAt,omitempty"`
|
||||
}
|
||||
@@ -4242,21 +4253,22 @@ type PlansStreamCursorInput struct {
|
||||
|
||||
// Initial value of the column from where the streaming should start
|
||||
type PlansStreamCursorValueInput struct {
|
||||
CreatedAt *time.Time `json:"createdAt,omitempty"`
|
||||
Deprecated *bool `json:"deprecated,omitempty"`
|
||||
FeatureBackupEnabled *bool `json:"featureBackupEnabled,omitempty"`
|
||||
FeatureCustomDomainsEnabled *bool `json:"featureCustomDomainsEnabled,omitempty"`
|
||||
FeatureCustomEmailTemplatesEnabled *bool `json:"featureCustomEmailTemplatesEnabled,omitempty"`
|
||||
FeatureMaxDbSize *int64 `json:"featureMaxDbSize,omitempty"`
|
||||
ID *string `json:"id,omitempty"`
|
||||
Individual *bool `json:"individual,omitempty"`
|
||||
IsDefault *bool `json:"isDefault,omitempty"`
|
||||
IsFree *bool `json:"isFree,omitempty"`
|
||||
IsPublic *bool `json:"isPublic,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Price *int64 `json:"price,omitempty"`
|
||||
Sort *int64 `json:"sort,omitempty"`
|
||||
UpatedAt *time.Time `json:"upatedAt,omitempty"`
|
||||
CreatedAt *time.Time `json:"createdAt,omitempty"`
|
||||
Deprecated *bool `json:"deprecated,omitempty"`
|
||||
FeatureBackupEnabled *bool `json:"featureBackupEnabled,omitempty"`
|
||||
FeatureCustomDomainsEnabled *bool `json:"featureCustomDomainsEnabled,omitempty"`
|
||||
FeatureCustomEmailTemplatesEnabled *bool `json:"featureCustomEmailTemplatesEnabled,omitempty"`
|
||||
FeatureMaxDbSize *int64 `json:"featureMaxDbSize,omitempty"`
|
||||
ID *string `json:"id,omitempty"`
|
||||
Individual *bool `json:"individual,omitempty"`
|
||||
IsDefault *bool `json:"isDefault,omitempty"`
|
||||
IsFree *bool `json:"isFree,omitempty"`
|
||||
IsPublic *bool `json:"isPublic,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Price *int64 `json:"price,omitempty"`
|
||||
SLALevel *SLALevelEnum `json:"slaLevel,omitempty"`
|
||||
Sort *int64 `json:"sort,omitempty"`
|
||||
UpatedAt *time.Time `json:"upatedAt,omitempty"`
|
||||
}
|
||||
|
||||
type QueryRoot struct {
|
||||
@@ -4686,6 +4698,15 @@ type RunServiceStreamCursorValueInput struct {
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
|
||||
}
|
||||
|
||||
// Boolean expression to compare columns of type "sla_level_enum". All fields are combined with logical 'AND'.
|
||||
type SLALevelEnumComparisonExp struct {
|
||||
Eq *SLALevelEnum `json:"_eq,omitempty"`
|
||||
In []SLALevelEnum `json:"_in,omitempty"`
|
||||
IsNull *bool `json:"_is_null,omitempty"`
|
||||
Neq *SLALevelEnum `json:"_neq,omitempty"`
|
||||
Nin []SLALevelEnum `json:"_nin,omitempty"`
|
||||
}
|
||||
|
||||
// Boolean expression to compare columns of type "software_type_enum". All fields are combined with logical 'AND'.
|
||||
type SoftwareTypeEnumComparisonExp struct {
|
||||
Eq *SoftwareTypeEnum `json:"_eq,omitempty"`
|
||||
@@ -8534,6 +8555,8 @@ const (
|
||||
// column name
|
||||
PlansSelectColumnPrice PlansSelectColumn = "price"
|
||||
// column name
|
||||
PlansSelectColumnSLALevel PlansSelectColumn = "slaLevel"
|
||||
// column name
|
||||
PlansSelectColumnSort PlansSelectColumn = "sort"
|
||||
// column name
|
||||
PlansSelectColumnUpatedAt PlansSelectColumn = "upatedAt"
|
||||
@@ -8553,13 +8576,14 @@ var AllPlansSelectColumn = []PlansSelectColumn{
|
||||
PlansSelectColumnIsPublic,
|
||||
PlansSelectColumnName,
|
||||
PlansSelectColumnPrice,
|
||||
PlansSelectColumnSLALevel,
|
||||
PlansSelectColumnSort,
|
||||
PlansSelectColumnUpatedAt,
|
||||
}
|
||||
|
||||
func (e PlansSelectColumn) IsValid() bool {
|
||||
switch e {
|
||||
case PlansSelectColumnCreatedAt, PlansSelectColumnDeprecated, PlansSelectColumnFeatureBackupEnabled, PlansSelectColumnFeatureCustomDomainsEnabled, PlansSelectColumnFeatureCustomEmailTemplatesEnabled, PlansSelectColumnFeatureMaxDbSize, PlansSelectColumnID, PlansSelectColumnIndividual, PlansSelectColumnIsDefault, PlansSelectColumnIsFree, PlansSelectColumnIsPublic, PlansSelectColumnName, PlansSelectColumnPrice, PlansSelectColumnSort, PlansSelectColumnUpatedAt:
|
||||
case PlansSelectColumnCreatedAt, PlansSelectColumnDeprecated, PlansSelectColumnFeatureBackupEnabled, PlansSelectColumnFeatureCustomDomainsEnabled, PlansSelectColumnFeatureCustomEmailTemplatesEnabled, PlansSelectColumnFeatureMaxDbSize, PlansSelectColumnID, PlansSelectColumnIndividual, PlansSelectColumnIsDefault, PlansSelectColumnIsFree, PlansSelectColumnIsPublic, PlansSelectColumnName, PlansSelectColumnPrice, PlansSelectColumnSLALevel, PlansSelectColumnSort, PlansSelectColumnUpatedAt:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -8946,6 +8970,66 @@ func (e RunServiceSelectColumn) MarshalJSON() ([]byte, error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type SLALevelEnum string
|
||||
|
||||
const (
|
||||
// No SLA
|
||||
SLALevelEnumNone SLALevelEnum = "none"
|
||||
// Premium SLA
|
||||
SLALevelEnumPremium SLALevelEnum = "premium"
|
||||
// Standard SLA
|
||||
SLALevelEnumStandard SLALevelEnum = "standard"
|
||||
)
|
||||
|
||||
var AllSLALevelEnum = []SLALevelEnum{
|
||||
SLALevelEnumNone,
|
||||
SLALevelEnumPremium,
|
||||
SLALevelEnumStandard,
|
||||
}
|
||||
|
||||
func (e SLALevelEnum) IsValid() bool {
|
||||
switch e {
|
||||
case SLALevelEnumNone, SLALevelEnumPremium, SLALevelEnumStandard:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e SLALevelEnum) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *SLALevelEnum) UnmarshalGQL(v any) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = SLALevelEnum(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid sla_level_enum", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e SLALevelEnum) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
func (e *SLALevelEnum) UnmarshalJSON(b []byte) error {
|
||||
s, err := strconv.Unquote(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.UnmarshalGQL(s)
|
||||
}
|
||||
|
||||
func (e SLALevelEnum) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
e.MarshalGQL(&buf)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type SoftwareTypeEnum string
|
||||
|
||||
const (
|
||||
|
||||
@@ -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",
|
||||
@@ -60,7 +61,7 @@
|
||||
"@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",
|
||||
@@ -93,7 +94,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 +126,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 +232,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
269
dashboard/pnpm-lock.yaml
generated
269
dashboard/pnpm-lock.yaml
generated
@@ -6,6 +6,8 @@ settings:
|
||||
|
||||
overrides:
|
||||
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'
|
||||
|
||||
packageExtensionsChecksum: sha256-gRFeykwiwMfEE6etcYx6N48XwVeKzxbqNveL7KTQgSQ=
|
||||
|
||||
@@ -55,6 +57,9 @@ importers:
|
||||
'@headlessui/react':
|
||||
specifier: ^1.7.18
|
||||
version: 1.7.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@hello-pangea/dnd':
|
||||
specifier: ^18.0.1
|
||||
version: 18.0.1(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@heroicons/react':
|
||||
specifier: ^1.0.6
|
||||
version: 1.0.6(react@18.2.0)
|
||||
@@ -122,8 +127,8 @@ importers:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.2(@types/react@18.2.73)(react@18.2.0)
|
||||
specifier: ^1.1.2
|
||||
version: 1.2.3(@types/react@18.2.73)(react@18.2.0)
|
||||
'@radix-ui/react-switch':
|
||||
specifier: ^1.2.6
|
||||
version: 1.2.6(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -221,8 +226,8 @@ importers:
|
||||
specifier: ^4.0.8
|
||||
version: 4.0.8
|
||||
lucide-react:
|
||||
specifier: ^0.416.0
|
||||
version: 0.416.0(react@18.2.0)
|
||||
specifier: ^0.552.0
|
||||
version: 0.552.0(react@18.2.0)
|
||||
next:
|
||||
specifier: ^14.2.31
|
||||
version: 14.2.32(@babel/core@7.26.10)(@playwright/test@1.54.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -317,8 +322,8 @@ importers:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
validator:
|
||||
specifier: ^13.11.0
|
||||
version: 13.12.0
|
||||
specifier: ^13.15.20
|
||||
version: 13.15.23
|
||||
yup:
|
||||
specifier: ^1.4.0
|
||||
version: 1.5.0
|
||||
@@ -2063,6 +2068,12 @@ packages:
|
||||
react: ^16 || ^17 || ^18
|
||||
react-dom: ^16 || ^17 || ^18
|
||||
|
||||
'@hello-pangea/dnd@18.0.1':
|
||||
resolution: {integrity: sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
|
||||
'@heroicons/react@1.0.6':
|
||||
resolution: {integrity: sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ==}
|
||||
peerDependencies:
|
||||
@@ -2236,6 +2247,14 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@isaacs/balanced-match@4.0.1':
|
||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2609,10 +2628,6 @@ packages:
|
||||
'@orval/zod@7.11.2':
|
||||
resolution: {integrity: sha512-4MzTg5Wms8/LlM3CbYu80dvCbP88bVlQjnYsBdFXuEv0K2GYkBCAhVOrmXCVrPXE89neV6ABkvWQeuKZQpkdxQ==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@playwright/test@1.54.1':
|
||||
resolution: {integrity: sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -4048,6 +4063,9 @@ packages:
|
||||
'@types/urijs@1.19.25':
|
||||
resolution: {integrity: sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==}
|
||||
|
||||
'@types/use-sync-external-store@0.0.6':
|
||||
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||
|
||||
'@types/uuid@9.0.8':
|
||||
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
|
||||
|
||||
@@ -4964,6 +4982,9 @@ packages:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
css-box-model@1.2.1:
|
||||
resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==}
|
||||
|
||||
css.escape@1.5.1:
|
||||
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
|
||||
|
||||
@@ -5634,8 +5655,8 @@ packages:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
foreground-child@3.1.1:
|
||||
resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.4:
|
||||
@@ -5764,8 +5785,9 @@ packages:
|
||||
glob-to-regexp@0.4.1:
|
||||
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||
|
||||
glob@10.4.5:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
glob@12.0.0:
|
||||
resolution: {integrity: sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==}
|
||||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
glob@7.1.7:
|
||||
@@ -6273,9 +6295,9 @@ packages:
|
||||
iterator.prototype@1.1.2:
|
||||
resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==}
|
||||
|
||||
jackspeak@3.2.3:
|
||||
resolution: {integrity: sha512-htOzIMPbpLid/Gq9/zaz9SfExABxqRe1sSCdxntlO/aMD6u0issZQiY25n2GKQUtJ02j7z5sfptlAOMpWWOmvw==}
|
||||
engines: {node: '>=14'}
|
||||
jackspeak@4.1.1:
|
||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
jest-diff@29.7.0:
|
||||
resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==}
|
||||
@@ -6322,8 +6344,8 @@ packages:
|
||||
js-tokens@9.0.1:
|
||||
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
||||
|
||||
js-yaml@4.1.0:
|
||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||
js-yaml@4.1.1:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
|
||||
jsdom@22.1.0:
|
||||
@@ -6540,15 +6562,15 @@ packages:
|
||||
lowlight@3.1.0:
|
||||
resolution: {integrity: sha512-CEbNVoSikAxwDMDPjXlqlFYiZLkDJHwyGu/MfOsJnF3d7f3tds5J3z8s/l9TMXhzfsJCCJEAsD78842mwmg0PQ==}
|
||||
|
||||
lru-cache@10.2.2:
|
||||
resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==}
|
||||
engines: {node: 14 || >=16.14}
|
||||
lru-cache@11.2.2:
|
||||
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
lucide-react@0.416.0:
|
||||
resolution: {integrity: sha512-wPWxTzdss1CTz2aqcNWNlbh4YSnH9neJWP3RaeXepxpLCTW+pmu7WcT/wxJe+Q7Y7DqGOxAqakJv0pIK3431Ag==}
|
||||
lucide-react@0.552.0:
|
||||
resolution: {integrity: sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
@@ -6783,6 +6805,10 @@ packages:
|
||||
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
|
||||
hasBin: true
|
||||
|
||||
minimatch@10.1.1:
|
||||
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
@@ -6794,10 +6820,6 @@ packages:
|
||||
resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minimatch@9.0.4:
|
||||
resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minimatch@9.0.5:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
@@ -7166,9 +7188,9 @@ packages:
|
||||
resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
path-scurry@2.0.1:
|
||||
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
path-to-regexp@6.3.0:
|
||||
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
|
||||
@@ -7407,6 +7429,9 @@ packages:
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
raf-schd@4.0.3:
|
||||
resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
|
||||
|
||||
randombytes@2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
|
||||
@@ -7481,6 +7506,18 @@ packages:
|
||||
react:
|
||||
optional: true
|
||||
|
||||
react-redux@9.2.0:
|
||||
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
|
||||
peerDependencies:
|
||||
'@types/react': ^18.2.25 || ^19
|
||||
react: ^18.0 || ^19
|
||||
redux: ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
redux:
|
||||
optional: true
|
||||
|
||||
react-refresh@0.17.0:
|
||||
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -7598,6 +7635,9 @@ packages:
|
||||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
redux@5.0.1:
|
||||
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -8206,6 +8246,9 @@ packages:
|
||||
tiny-case@1.0.3:
|
||||
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
@@ -8558,8 +8601,8 @@ packages:
|
||||
v8-compile-cache-lib@3.0.1:
|
||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||
|
||||
validator@13.12.0:
|
||||
resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==}
|
||||
validator@13.15.23:
|
||||
resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
vfile-message@4.0.2:
|
||||
@@ -8576,7 +8619,7 @@ packages:
|
||||
vite-tsconfig-paths@4.3.2:
|
||||
resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==}
|
||||
peerDependencies:
|
||||
vite: '>=4.5.14'
|
||||
vite: '*'
|
||||
peerDependenciesMeta:
|
||||
vite:
|
||||
optional: true
|
||||
@@ -8900,7 +8943,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jsdevtools/ono': 7.1.3
|
||||
'@types/json-schema': 7.0.15
|
||||
js-yaml: 4.1.0
|
||||
js-yaml: 4.1.1
|
||||
|
||||
'@apidevtools/openapi-schemas@2.1.0': {}
|
||||
|
||||
@@ -8946,7 +8989,7 @@ snapshots:
|
||||
'@babel/core': 7.26.10
|
||||
'@babel/generator': 7.28.3
|
||||
'@babel/parser': 7.28.3
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@babel/traverse': 7.28.3
|
||||
'@babel/types': 7.28.2
|
||||
babel-preset-fbjs: 3.4.0(@babel/core@7.26.10)
|
||||
@@ -10094,7 +10137,7 @@ snapshots:
|
||||
'@emotion/babel-plugin@11.11.0':
|
||||
dependencies:
|
||||
'@babel/helper-module-imports': 7.27.1
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@emotion/hash': 0.9.1
|
||||
'@emotion/memoize': 0.8.1
|
||||
'@emotion/serialize': 1.1.4
|
||||
@@ -10299,7 +10342,7 @@ snapshots:
|
||||
globals: 13.24.0
|
||||
ignore: 5.3.2
|
||||
import-fresh: 3.3.0
|
||||
js-yaml: 4.1.0
|
||||
js-yaml: 4.1.1
|
||||
minimatch: 3.1.2
|
||||
strip-json-comments: 3.1.1
|
||||
transitivePeerDependencies:
|
||||
@@ -10934,6 +10977,18 @@ snapshots:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
'@hello-pangea/dnd@18.0.1(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
css-box-model: 1.2.1
|
||||
raf-schd: 4.0.3
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-redux: 9.2.0(@types/react@18.2.73)(react@18.2.0)(redux@5.0.1)
|
||||
redux: 5.0.1
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@heroicons/react@1.0.6(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
@@ -10971,7 +11026,7 @@ snapshots:
|
||||
loglevel: 1.9.2
|
||||
loglevel-plugin-prefix: 0.8.4
|
||||
minimatch: 6.2.0
|
||||
validator: 13.12.0
|
||||
validator: 13.15.23
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
@@ -11104,11 +11159,17 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.8
|
||||
|
||||
'@isaacs/balanced-match@4.0.1': {}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
dependencies:
|
||||
'@isaacs/balanced-match': 4.0.1
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
string-width-cjs: string-width@4.2.3
|
||||
strip-ansi: 7.1.0
|
||||
strip-ansi: 7.1.2
|
||||
strip-ansi-cjs: strip-ansi@6.0.1
|
||||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
@@ -11273,7 +11334,7 @@ snapshots:
|
||||
|
||||
'@mui/base@5.0.0-beta.40(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@floating-ui/react-dom': 2.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@mui/types': 7.2.14(@types/react@18.2.73)
|
||||
'@mui/utils': 5.15.14(@types/react@18.2.73)(react@18.2.0)
|
||||
@@ -11310,7 +11371,7 @@ snapshots:
|
||||
|
||||
'@mui/private-theming@5.15.14(@types/react@18.2.73)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@mui/utils': 5.15.14(@types/react@18.2.73)(react@18.2.0)
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
@@ -11319,7 +11380,7 @@ snapshots:
|
||||
|
||||
'@mui/styled-engine@5.15.14(@emotion/react@11.11.4(@types/react@18.2.73)(react@18.2.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.2.73)(react@18.2.0))(@types/react@18.2.73)(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@emotion/cache': 11.11.0
|
||||
csstype: 3.1.3
|
||||
prop-types: 15.8.1
|
||||
@@ -11350,7 +11411,7 @@ snapshots:
|
||||
|
||||
'@mui/utils@5.15.14(@types/react@18.2.73)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@types/prop-types': 15.7.12
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
@@ -11556,9 +11617,6 @@ snapshots:
|
||||
- openapi-types
|
||||
- supports-color
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@playwright/test@1.54.1':
|
||||
dependencies:
|
||||
playwright: 1.54.1
|
||||
@@ -11571,7 +11629,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/primitive@1.0.1':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
|
||||
'@radix-ui/primitive@1.1.0': {}
|
||||
|
||||
@@ -11698,7 +11756,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.73
|
||||
@@ -11723,7 +11781,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-context@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.73
|
||||
@@ -11748,7 +11806,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@radix-ui/primitive': 1.0.1
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||
@@ -11799,7 +11857,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@radix-ui/primitive': 1.0.1
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -11854,7 +11912,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.73
|
||||
@@ -11873,7 +11931,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||
@@ -11924,7 +11982,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-id@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
@@ -12033,7 +12091,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
@@ -12063,7 +12121,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
@@ -12094,7 +12152,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.2.73)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.73)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
@@ -12257,7 +12315,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-slot@1.0.2(@types/react@18.2.73)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
@@ -12344,7 +12402,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.73
|
||||
@@ -12357,7 +12415,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
@@ -12387,7 +12445,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.73)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.73)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
@@ -12402,7 +12460,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.73)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.73
|
||||
@@ -13041,6 +13099,8 @@ snapshots:
|
||||
|
||||
'@types/urijs@1.19.25': {}
|
||||
|
||||
'@types/use-sync-external-store@0.0.6': {}
|
||||
|
||||
'@types/uuid@9.0.8': {}
|
||||
|
||||
'@types/validator@13.11.10': {}
|
||||
@@ -13743,7 +13803,7 @@ snapshots:
|
||||
|
||||
babel-plugin-macros@3.1.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
cosmiconfig: 7.1.0
|
||||
resolve: 1.22.10
|
||||
|
||||
@@ -13834,7 +13894,7 @@ snapshots:
|
||||
'@babel/preset-env': 7.24.7(@babel/core@7.26.10)
|
||||
'@babel/preset-react': 7.24.6(@babel/core@7.26.10)
|
||||
'@babel/preset-typescript': 7.24.7(@babel/core@7.26.10)
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
babel-plugin-macros: 3.1.0
|
||||
babel-plugin-transform-react-remove-prop-types: 0.4.24
|
||||
transitivePeerDependencies:
|
||||
@@ -14148,7 +14208,7 @@ snapshots:
|
||||
cosmiconfig@8.3.6(typescript@5.8.3):
|
||||
dependencies:
|
||||
import-fresh: 3.3.1
|
||||
js-yaml: 4.1.0
|
||||
js-yaml: 4.1.1
|
||||
parse-json: 5.2.0
|
||||
path-type: 4.0.0
|
||||
optionalDependencies:
|
||||
@@ -14158,7 +14218,7 @@ snapshots:
|
||||
dependencies:
|
||||
env-paths: 2.2.1
|
||||
import-fresh: 3.3.1
|
||||
js-yaml: 4.1.0
|
||||
js-yaml: 4.1.1
|
||||
parse-json: 5.2.0
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
@@ -14183,6 +14243,10 @@ snapshots:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
css-box-model@1.2.1:
|
||||
dependencies:
|
||||
tiny-invariant: 1.3.3
|
||||
|
||||
css.escape@1.5.1: {}
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
@@ -14354,7 +14418,7 @@ snapshots:
|
||||
|
||||
dom-helpers@5.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
csstype: 3.1.3
|
||||
|
||||
domexception@4.0.0:
|
||||
@@ -15008,7 +15072,7 @@ snapshots:
|
||||
imurmurhash: 0.1.4
|
||||
is-glob: 4.0.3
|
||||
is-path-inside: 3.0.3
|
||||
js-yaml: 4.1.0
|
||||
js-yaml: 4.1.1
|
||||
json-stable-stringify-without-jsonify: 1.0.1
|
||||
levn: 0.4.1
|
||||
lodash.merge: 4.6.2
|
||||
@@ -15187,7 +15251,7 @@ snapshots:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
||||
foreground-child@3.1.1:
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
@@ -15328,14 +15392,14 @@ snapshots:
|
||||
|
||||
glob-to-regexp@0.4.1: {}
|
||||
|
||||
glob@10.4.5:
|
||||
glob@12.0.0:
|
||||
dependencies:
|
||||
foreground-child: 3.1.1
|
||||
jackspeak: 3.2.3
|
||||
minimatch: 9.0.4
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 4.1.1
|
||||
minimatch: 10.1.1
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
path-scurry: 2.0.1
|
||||
|
||||
glob@7.1.7:
|
||||
dependencies:
|
||||
@@ -15858,11 +15922,9 @@ snapshots:
|
||||
reflect.getprototypeof: 1.0.8
|
||||
set-function-name: 2.0.2
|
||||
|
||||
jackspeak@3.2.3:
|
||||
jackspeak@4.1.1:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jest-diff@29.7.0:
|
||||
dependencies:
|
||||
@@ -15919,7 +15981,7 @@ snapshots:
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
|
||||
js-yaml@4.1.0:
|
||||
js-yaml@4.1.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
@@ -16156,13 +16218,13 @@ snapshots:
|
||||
devlop: 1.1.0
|
||||
highlight.js: 11.9.0
|
||||
|
||||
lru-cache@10.2.2: {}
|
||||
lru-cache@11.2.2: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
lucide-react@0.416.0(react@18.2.0):
|
||||
lucide-react@0.552.0(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
|
||||
@@ -16586,6 +16648,10 @@ snapshots:
|
||||
|
||||
mini-svg-data-uri@1.4.4: {}
|
||||
|
||||
minimatch@10.1.1:
|
||||
dependencies:
|
||||
'@isaacs/brace-expansion': 5.0.0
|
||||
|
||||
minimatch@3.1.2:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
@@ -16598,10 +16664,6 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@9.0.4:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@9.0.5:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
@@ -17040,9 +17102,9 @@ snapshots:
|
||||
dependencies:
|
||||
path-root-regex: 0.1.2
|
||||
|
||||
path-scurry@1.11.1:
|
||||
path-scurry@2.0.1:
|
||||
dependencies:
|
||||
lru-cache: 10.2.2
|
||||
lru-cache: 11.2.2
|
||||
minipass: 7.1.2
|
||||
|
||||
path-to-regexp@6.3.0: {}
|
||||
@@ -17202,6 +17264,8 @@ snapshots:
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
raf-schd@4.0.3: {}
|
||||
|
||||
randombytes@2.1.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
@@ -17277,6 +17341,15 @@ snapshots:
|
||||
optionalDependencies:
|
||||
react: 18.2.0
|
||||
|
||||
react-redux@9.2.0(@types/react@18.2.73)(react@18.2.0)(redux@5.0.1):
|
||||
dependencies:
|
||||
'@types/use-sync-external-store': 0.0.6
|
||||
react: 18.2.0
|
||||
use-sync-external-store: 1.4.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.73
|
||||
redux: 5.0.1
|
||||
|
||||
react-refresh@0.17.0: {}
|
||||
|
||||
react-remove-scroll-bar@2.3.8(@types/react@18.2.73)(react@18.2.0):
|
||||
@@ -17339,7 +17412,7 @@ snapshots:
|
||||
|
||||
react-transition-group@4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
dom-helpers: 5.2.1
|
||||
loose-envify: 1.4.0
|
||||
prop-types: 15.8.1
|
||||
@@ -17395,6 +17468,8 @@ snapshots:
|
||||
indent-string: 4.0.0
|
||||
strip-indent: 3.0.0
|
||||
|
||||
redux@5.0.1: {}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -17429,7 +17504,7 @@ snapshots:
|
||||
|
||||
regenerator-transform@0.15.2:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
|
||||
regexp.prototype.flags@1.5.3:
|
||||
dependencies:
|
||||
@@ -17477,7 +17552,7 @@ snapshots:
|
||||
|
||||
relay-runtime@12.0.0(encoding@0.1.13):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@babel/runtime': 7.28.4
|
||||
fbjs: 3.0.5(encoding@0.1.13)
|
||||
invariant: 2.2.4
|
||||
transitivePeerDependencies:
|
||||
@@ -17875,7 +17950,7 @@ snapshots:
|
||||
dependencies:
|
||||
eastasianwidth: 0.2.0
|
||||
emoji-regex: 9.2.2
|
||||
strip-ansi: 7.1.0
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
string-width@7.2.0:
|
||||
dependencies:
|
||||
@@ -17994,7 +18069,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
commander: 4.1.1
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
lines-and-columns: 1.2.4
|
||||
mz: 2.7.0
|
||||
pirates: 4.0.6
|
||||
@@ -18096,7 +18171,7 @@ snapshots:
|
||||
test-exclude@7.0.1:
|
||||
dependencies:
|
||||
'@istanbuljs/schema': 0.1.3
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
minimatch: 9.0.5
|
||||
|
||||
text-table@0.2.0: {}
|
||||
@@ -18122,6 +18197,8 @@ snapshots:
|
||||
|
||||
tiny-case@1.0.3: {}
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
@@ -18466,7 +18543,7 @@ snapshots:
|
||||
|
||||
v8-compile-cache-lib@3.0.1: {}
|
||||
|
||||
validator@13.12.0: {}
|
||||
validator@13.15.23: {}
|
||||
|
||||
vfile-message@4.0.2:
|
||||
dependencies:
|
||||
@@ -18720,9 +18797,9 @@ snapshots:
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.1
|
||||
ansi-styles: 6.2.3
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.1.0
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
wrap-ansi@9.0.0:
|
||||
dependencies:
|
||||
|
||||
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,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,27 @@
|
||||
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 { 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 +32,116 @@ 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>;
|
||||
}
|
||||
|
||||
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,
|
||||
}: 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 })}
|
||||
>
|
||||
<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}
|
||||
{...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;
|
||||
|
||||
125
dashboard/src/components/form/FormSelect/FormSelect.tsx
Normal file
125
dashboard/src/components/form/FormSelect/FormSelect.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
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';
|
||||
|
||||
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;
|
||||
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,
|
||||
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}
|
||||
{...selectProps}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className={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">
|
||||
|
||||
@@ -482,9 +482,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',
|
||||
|
||||
@@ -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';
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
));
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 @@
|
||||
export type RowDensity = 'comfortable' | 'compact';
|
||||
@@ -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,21 @@
|
||||
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 { 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 +33,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 +96,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({
|
||||
@@ -160,17 +156,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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -21,23 +21,22 @@ import { ReplicasFormSection } from '@/features/orgs/projects/services/component
|
||||
import { StorageFormSection } from '@/features/orgs/projects/services/components/ServiceForm/components/StorageFormSection';
|
||||
|
||||
import {
|
||||
defaultServiceFormValues,
|
||||
validationSchema,
|
||||
type Port,
|
||||
type ServiceFormProps,
|
||||
type ServiceFormValues,
|
||||
} from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
|
||||
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { getFormattedServiceConfig } from '@/features/orgs/projects/services/utils/getFormattedServiceConfig';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
useInsertRunServiceConfigMutation,
|
||||
useReplaceRunServiceConfigMutation,
|
||||
type ConfigRunServiceConfigInsertInput,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { removeTypename } from '@/utils/helpers';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
@@ -69,14 +68,7 @@ export default function ServiceForm({
|
||||
useState<Error | null>(null);
|
||||
|
||||
const form = useForm<ServiceFormValues>({
|
||||
defaultValues: initialData ?? {
|
||||
compute: {
|
||||
cpu: 62,
|
||||
memory: 128,
|
||||
},
|
||||
replicas: 1,
|
||||
autoscaler: null,
|
||||
},
|
||||
defaultValues: initialData ?? defaultServiceFormValues,
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
@@ -142,66 +134,8 @@ export default function ServiceForm({
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
const getFormattedConfig = (values: ServiceFormValues) => {
|
||||
// Remove any __typename property from the values
|
||||
const sanitizedValues = removeTypename(values) as ServiceFormValues;
|
||||
const sanitizedInitialDataPorts: Port[] = initialData?.ports
|
||||
? removeTypename(initialData.ports)
|
||||
: [];
|
||||
|
||||
const config: ConfigRunServiceConfigInsertInput = {
|
||||
name: sanitizedValues.name,
|
||||
image: {
|
||||
image: sanitizedValues.image,
|
||||
pullCredentials: sanitizedValues.pullCredentials,
|
||||
},
|
||||
command: sanitizedValues.command?.map((arg) => arg.argument),
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: sanitizedValues.compute?.cpu,
|
||||
memory: sanitizedValues.compute?.memory,
|
||||
},
|
||||
storage: sanitizedValues.storage?.map((item) => ({
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
capacity: item.capacity,
|
||||
})),
|
||||
replicas: sanitizedValues.replicas,
|
||||
autoscaler: sanitizedValues.autoscaler
|
||||
? {
|
||||
maxReplicas: sanitizedValues.autoscaler?.maxReplicas,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
environment: sanitizedValues.environment?.map((item) => ({
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
})),
|
||||
ports: sanitizedValues.ports?.map((item) => ({
|
||||
port: item.port,
|
||||
type: item.type,
|
||||
publish: item.publish,
|
||||
ingresses: item.ingresses as any, // cannot be changed on the UI always null type checking can be skipped.
|
||||
rateLimit:
|
||||
sanitizedInitialDataPorts.find(
|
||||
(port) => port.port === item.port && port.type === item.type,
|
||||
)?.rateLimit ?? (null as any), // cannot be changed on the UI always null type checking can be skipped.
|
||||
})),
|
||||
healthCheck: sanitizedValues.healthCheck
|
||||
? {
|
||||
port: sanitizedValues.healthCheck?.port,
|
||||
initialDelaySeconds:
|
||||
sanitizedValues.healthCheck?.initialDelaySeconds,
|
||||
probePeriodSeconds: sanitizedValues.healthCheck?.probePeriodSeconds,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
const createOrUpdateService = async (values: ServiceFormValues) => {
|
||||
const config = getFormattedConfig(values);
|
||||
const config = getFormattedServiceConfig({ values, initialData });
|
||||
|
||||
if (serviceID) {
|
||||
// Update service config
|
||||
@@ -292,7 +226,10 @@ export default function ServiceForm({
|
||||
};
|
||||
|
||||
const copyConfig = () => {
|
||||
const config = getFormattedConfig(formValues);
|
||||
const config = getFormattedServiceConfig({
|
||||
values: formValues,
|
||||
initialData,
|
||||
});
|
||||
|
||||
const base64Config = btoa(JSON.stringify(config));
|
||||
|
||||
|
||||
@@ -87,6 +87,15 @@ export type ServiceFormInitialData = Omit<ServiceFormValues, 'ports'> & {
|
||||
}[];
|
||||
};
|
||||
|
||||
export const defaultServiceFormValues = {
|
||||
compute: {
|
||||
cpu: 62,
|
||||
memory: 128,
|
||||
},
|
||||
replicas: 1,
|
||||
autoscaler: null,
|
||||
};
|
||||
|
||||
export interface ServiceFormProps extends DialogFormProps {
|
||||
/**
|
||||
* To use in conjunction with initialData to allow for updating the service
|
||||
|
||||
@@ -15,7 +15,10 @@ import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatfo
|
||||
import { type RunService } from '@/features/orgs/projects/common/hooks/useRunServices';
|
||||
import { ServiceForm } from '@/features/orgs/projects/services/components/ServiceForm';
|
||||
import { type PortTypes } from '@/features/orgs/projects/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
|
||||
import type { ServiceFormInitialData } from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
|
||||
import {
|
||||
defaultServiceFormValues,
|
||||
type ServiceFormInitialData,
|
||||
} from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
||||
@@ -74,12 +77,15 @@ export default function ServicesList({
|
||||
ingresses: item.ingresses,
|
||||
rateLimit: item.rateLimit,
|
||||
})),
|
||||
compute: service.config?.resources?.compute ?? {
|
||||
cpu: 62,
|
||||
memory: 128,
|
||||
},
|
||||
replicas: service.config?.resources?.replicas,
|
||||
autoscaler: service?.config?.resources?.autoscaler,
|
||||
compute:
|
||||
service.config?.resources?.compute ??
|
||||
defaultServiceFormValues.compute,
|
||||
replicas:
|
||||
service.config?.resources?.replicas ??
|
||||
defaultServiceFormValues.replicas,
|
||||
autoscaler:
|
||||
service?.config?.resources?.autoscaler ??
|
||||
defaultServiceFormValues.autoscaler,
|
||||
storage: service.config?.resources?.storage,
|
||||
} as ServiceFormInitialData
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { PortTypes } from '@/features/orgs/projects/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
|
||||
import getFormattedServiceConfig from './getFormattedServiceConfig';
|
||||
|
||||
describe('getFormattedServiceConfig', () => {
|
||||
it('pghero config should be formatted correctly', () => {
|
||||
const pgheroFormValues = {
|
||||
name: 'pghero',
|
||||
image: 'docker.io/ankane/pghero:latest',
|
||||
command: [],
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: 125,
|
||||
memory: 256,
|
||||
},
|
||||
storage: [],
|
||||
replicas: 1,
|
||||
},
|
||||
environment: [
|
||||
{
|
||||
name: 'DATABASE_URL',
|
||||
value:
|
||||
'postgres://postgres:[PASSWORD]@postgres-service:5432/[SUBDOMAIN]?sslmode=disable',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_USERNAME',
|
||||
value: '[USER]',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_PASSWORD',
|
||||
value: '[PASSWORD]',
|
||||
},
|
||||
],
|
||||
ports: [
|
||||
{
|
||||
port: 8080,
|
||||
type: PortTypes.HTTP,
|
||||
publish: true,
|
||||
},
|
||||
],
|
||||
autoscaler: null,
|
||||
compute: {
|
||||
cpu: 125,
|
||||
memory: 256,
|
||||
},
|
||||
replicas: 1,
|
||||
storage: [],
|
||||
};
|
||||
|
||||
const formattedConfig = getFormattedServiceConfig({
|
||||
values: pgheroFormValues,
|
||||
});
|
||||
|
||||
const expected = {
|
||||
name: 'pghero',
|
||||
image: {
|
||||
image: 'docker.io/ankane/pghero:latest',
|
||||
},
|
||||
command: [],
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: 125,
|
||||
memory: 256,
|
||||
},
|
||||
storage: [],
|
||||
replicas: 1,
|
||||
autoscaler: null,
|
||||
},
|
||||
environment: [
|
||||
{
|
||||
name: 'DATABASE_URL',
|
||||
value:
|
||||
'postgres://postgres:[PASSWORD]@postgres-service:5432/[SUBDOMAIN]?sslmode=disable',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_USERNAME',
|
||||
value: '[USER]',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_PASSWORD',
|
||||
value: '[PASSWORD]',
|
||||
},
|
||||
],
|
||||
ports: [
|
||||
{
|
||||
port: 8080,
|
||||
type: 'http',
|
||||
publish: true,
|
||||
rateLimit: null,
|
||||
},
|
||||
],
|
||||
healthCheck: null,
|
||||
};
|
||||
|
||||
expect(formattedConfig).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import type {
|
||||
Port,
|
||||
ServiceFormInitialData,
|
||||
ServiceFormValues,
|
||||
} from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
|
||||
import type { ConfigRunServiceConfigInsertInput } from '@/utils/__generated__/graphql';
|
||||
import { removeTypename } from '@/utils/helpers';
|
||||
|
||||
export interface GetFormattedServiceConfigProps {
|
||||
values: ServiceFormValues;
|
||||
initialData?: ServiceFormInitialData;
|
||||
}
|
||||
|
||||
export default function getFormattedServiceConfig({
|
||||
values,
|
||||
initialData,
|
||||
}: GetFormattedServiceConfigProps) {
|
||||
// Remove any __typename property from the values
|
||||
const sanitizedValues = removeTypename(values) as ServiceFormValues;
|
||||
const sanitizedInitialDataPorts: Port[] = initialData?.ports
|
||||
? removeTypename(initialData.ports)
|
||||
: [];
|
||||
|
||||
const config: ConfigRunServiceConfigInsertInput = {
|
||||
name: sanitizedValues.name,
|
||||
image: {
|
||||
image: sanitizedValues.image,
|
||||
pullCredentials: sanitizedValues.pullCredentials,
|
||||
},
|
||||
command: sanitizedValues.command?.map((arg) => arg.argument),
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: sanitizedValues.compute?.cpu,
|
||||
memory: sanitizedValues.compute?.memory,
|
||||
},
|
||||
storage: sanitizedValues.storage?.map((item) => ({
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
capacity: item.capacity,
|
||||
})),
|
||||
replicas: sanitizedValues.replicas,
|
||||
autoscaler: sanitizedValues.autoscaler
|
||||
? {
|
||||
maxReplicas: sanitizedValues.autoscaler?.maxReplicas,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
environment: sanitizedValues.environment?.map((item) => ({
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
})),
|
||||
ports: sanitizedValues.ports?.map((item) => ({
|
||||
port: item.port,
|
||||
type: item.type,
|
||||
publish: item.publish,
|
||||
ingresses: item.ingresses as any, // cannot be changed on the UI always null type checking can be skipped.
|
||||
rateLimit:
|
||||
sanitizedInitialDataPorts.find(
|
||||
(port) => port.port === item.port && port.type === item.type,
|
||||
)?.rateLimit ?? (null as any), // cannot be changed on the UI always null type checking can be skipped.
|
||||
})),
|
||||
healthCheck: sanitizedValues.healthCheck
|
||||
? {
|
||||
port: sanitizedValues.healthCheck?.port,
|
||||
initialDelaySeconds: sanitizedValues.healthCheck?.initialDelaySeconds,
|
||||
probePeriodSeconds: sanitizedValues.healthCheck?.probePeriodSeconds,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as getFormattedServiceConfig } from './getFormattedServiceConfig';
|
||||
@@ -0,0 +1 @@
|
||||
export { default as parseConfigFromInstallLink } from './parseConfigFromInstallLink';
|
||||
@@ -0,0 +1,156 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import parseConfigFromInstallLink from './parseConfigFromInstallLink';
|
||||
|
||||
describe('parseConfigFromInstallLink', () => {
|
||||
it('pghero config without autoscaler should be formatted correctly', () => {
|
||||
const pgheroBase64Config =
|
||||
'eyJuYW1lIjoicGdoZXJvIiwiaW1hZ2UiOnsiaW1hZ2UiOiJkb2NrZXIuaW8vYW5rYW5lL3BnaGVybzpsYXRlc3QifSwiY29tbWFuZCI6W10sInJlc291cmNlcyI6eyJjb21wdXRlIjp7ImNwdSI6MTI1LCJtZW1vcnkiOjI1Nn0sInN0b3JhZ2UiOltdLCJyZXBsaWNhcyI6MX0sImVudmlyb25tZW50IjpbeyJuYW1lIjoiREFUQUJBU0VfVVJMIiwidmFsdWUiOiJwb3N0Z3JlczovL3Bvc3RncmVzOltQQVNTV09SRF1AcG9zdGdyZXMtc2VydmljZTo1NDMyL1tTVUJET01BSU5dP3NzbG1vZGU9ZGlzYWJsZSJ9LHsibmFtZSI6IlBHSEVST19VU0VSTkFNRSIsInZhbHVlIjoiW1VTRVJdIn0seyJuYW1lIjoiUEdIRVJPX1BBU1NXT1JEIiwidmFsdWUiOiJbUEFTU1dPUkRdIn1dLCJwb3J0cyI6W3sicG9ydCI6ODA4MCwidHlwZSI6Imh0dHAiLCJwdWJsaXNoIjp0cnVlfV19';
|
||||
|
||||
const config = parseConfigFromInstallLink(pgheroBase64Config);
|
||||
|
||||
const expected = {
|
||||
name: 'pghero',
|
||||
image: 'docker.io/ankane/pghero:latest',
|
||||
command: [],
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: 125,
|
||||
memory: 256,
|
||||
},
|
||||
storage: [],
|
||||
replicas: 1,
|
||||
},
|
||||
environment: [
|
||||
{
|
||||
name: 'DATABASE_URL',
|
||||
value:
|
||||
'postgres://postgres:[PASSWORD]@postgres-service:5432/[SUBDOMAIN]?sslmode=disable',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_USERNAME',
|
||||
value: '[USER]',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_PASSWORD',
|
||||
value: '[PASSWORD]',
|
||||
},
|
||||
],
|
||||
ports: [
|
||||
{
|
||||
port: 8080,
|
||||
type: 'http',
|
||||
publish: true,
|
||||
},
|
||||
],
|
||||
autoscaler: null,
|
||||
compute: {
|
||||
cpu: 125,
|
||||
memory: 256,
|
||||
},
|
||||
replicas: 1,
|
||||
storage: [],
|
||||
};
|
||||
|
||||
expect(config).toEqual(expected);
|
||||
});
|
||||
|
||||
it('antivirus config without autoscaler should be formatted correctly', () => {
|
||||
const antivirusBase64Config =
|
||||
'eyJuYW1lIjoiY2xhbWF2IiwiaW1hZ2UiOnsiaW1hZ2UiOiJkb2NrZXIuaW8vbmhvc3QvY2xhbWF2OjAuMS4xIn0sImNvbW1hbmQiOltdLCJyZXNvdXJjZXMiOnsiY29tcHV0ZSI6eyJjcHUiOjEwMDAsIm1lbW9yeSI6MjA0OH0sInN0b3JhZ2UiOltdLCJyZXBsaWNhcyI6MX0sImVudmlyb25tZW50IjpbXSwicG9ydHMiOlt7InBvcnQiOiIzMzEwIiwidHlwZSI6InRjcCIsInB1Ymxpc2giOmZhbHNlfV19';
|
||||
|
||||
const config = parseConfigFromInstallLink(antivirusBase64Config);
|
||||
|
||||
const expected = {
|
||||
name: 'clamav',
|
||||
image: 'docker.io/nhost/clamav:0.1.1',
|
||||
command: [],
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: 1000,
|
||||
memory: 2048,
|
||||
},
|
||||
storage: [],
|
||||
replicas: 1,
|
||||
},
|
||||
environment: [],
|
||||
ports: [
|
||||
{
|
||||
port: '3310',
|
||||
type: 'tcp',
|
||||
publish: false,
|
||||
},
|
||||
],
|
||||
autoscaler: null,
|
||||
compute: {
|
||||
cpu: 1000,
|
||||
memory: 2048,
|
||||
},
|
||||
replicas: 1,
|
||||
storage: [],
|
||||
};
|
||||
expect(config).toEqual(expected);
|
||||
});
|
||||
|
||||
it('invalid config should throw an error', () => {
|
||||
const invalidBase64Config = 'invalid';
|
||||
|
||||
expect(() => parseConfigFromInstallLink(invalidBase64Config)).toThrow();
|
||||
});
|
||||
|
||||
it('pghero config with autoscaler should be formatted correctly', () => {
|
||||
const pgheroWithAutoscalerBase64 =
|
||||
'eyJuYW1lIjoicGdoZXJvIiwiaW1hZ2UiOnsiaW1hZ2UiOiJkb2NrZXIuaW8vYW5rYW5lL3BnaGVybzpsYXRlc3QifSwiY29tbWFuZCI6W10sInJlc291cmNlcyI6eyJjb21wdXRlIjp7ImNwdSI6MTI1LCJtZW1vcnkiOjI1Nn0sInN0b3JhZ2UiOltdLCJyZXBsaWNhcyI6MSwiYXV0b3NjYWxlciI6eyJtYXhSZXBsaWNhcyI6MTF9fSwiZW52aXJvbm1lbnQiOlt7Im5hbWUiOiJEQVRBQkFTRV9VUkwiLCJ2YWx1ZSI6InBvc3RncmVzOi8vcG9zdGdyZXM6W1BBU1NXT1JEXUBwb3N0Z3Jlcy1zZXJ2aWNlOjU0MzIvW1NVQkRPTUFJTl0/c3NsbW9kZT1kaXNhYmxlIn0seyJuYW1lIjoiUEdIRVJPX1VTRVJOQU1FIiwidmFsdWUiOiJbVVNFUl0ifSx7Im5hbWUiOiJQR0hFUk9fUEFTU1dPUkQiLCJ2YWx1ZSI6IltQQVNTV09SRF0ifV0sInBvcnRzIjpbeyJwb3J0Ijo4MDgwLCJ0eXBlIjoiaHR0cCIsInB1Ymxpc2giOnRydWUsInJhdGVMaW1pdCI6bnVsbH1dLCJoZWFsdGhDaGVjayI6bnVsbH0=';
|
||||
|
||||
const config = parseConfigFromInstallLink(pgheroWithAutoscalerBase64);
|
||||
|
||||
const expected = {
|
||||
name: 'pghero',
|
||||
image: 'docker.io/ankane/pghero:latest',
|
||||
command: [],
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: 125,
|
||||
memory: 256,
|
||||
},
|
||||
storage: [],
|
||||
replicas: 1,
|
||||
autoscaler: {
|
||||
maxReplicas: 11,
|
||||
},
|
||||
},
|
||||
environment: [
|
||||
{
|
||||
name: 'DATABASE_URL',
|
||||
value:
|
||||
'postgres://postgres:[PASSWORD]@postgres-service:5432/[SUBDOMAIN]?sslmode=disable',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_USERNAME',
|
||||
value: '[USER]',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_PASSWORD',
|
||||
value: '[PASSWORD]',
|
||||
},
|
||||
],
|
||||
ports: [
|
||||
{
|
||||
port: 8080,
|
||||
type: 'http',
|
||||
publish: true,
|
||||
},
|
||||
],
|
||||
autoscaler: {
|
||||
maxReplicas: 11,
|
||||
},
|
||||
compute: {
|
||||
cpu: 125,
|
||||
memory: 256,
|
||||
},
|
||||
replicas: 1,
|
||||
storage: [],
|
||||
};
|
||||
|
||||
expect(config).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { type RunServiceConfig } from '@/features/orgs/projects/common/hooks/useRunServices';
|
||||
import { type PortTypes } from '@/features/orgs/projects/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
|
||||
import {
|
||||
defaultServiceFormValues,
|
||||
type ServiceFormInitialData,
|
||||
} from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
|
||||
|
||||
export default function parseConfigFromInstallLink(
|
||||
base64Config: string,
|
||||
): ServiceFormInitialData {
|
||||
const decodedConfig = atob(base64Config);
|
||||
const parsedConfig: RunServiceConfig = JSON.parse(decodedConfig);
|
||||
const initialData = {
|
||||
...parsedConfig,
|
||||
autoscaler:
|
||||
parsedConfig?.resources?.autoscaler ??
|
||||
defaultServiceFormValues.autoscaler,
|
||||
compute:
|
||||
parsedConfig?.resources?.compute ?? defaultServiceFormValues.compute,
|
||||
image: parsedConfig?.image?.image,
|
||||
command: parsedConfig?.command?.map((arg) => ({
|
||||
argument: arg,
|
||||
})),
|
||||
environment:
|
||||
parsedConfig?.environment?.map((env) => ({
|
||||
name: env.name,
|
||||
value: env.value,
|
||||
})) ?? undefined,
|
||||
healthCheck: parsedConfig?.healthCheck
|
||||
? {
|
||||
port: parsedConfig.healthCheck.port ?? 3000,
|
||||
initialDelaySeconds:
|
||||
parsedConfig.healthCheck.initialDelaySeconds ?? 30,
|
||||
probePeriodSeconds: parsedConfig.healthCheck.probePeriodSeconds ?? 60,
|
||||
}
|
||||
: undefined,
|
||||
ports:
|
||||
parsedConfig?.ports?.map((item) => ({
|
||||
port: item.port ?? 3000,
|
||||
type: item.type as PortTypes,
|
||||
publish: Boolean(item.publish),
|
||||
})) ?? [],
|
||||
replicas: parsedConfig?.resources?.replicas,
|
||||
storage: parsedConfig?.resources?.storage ?? undefined,
|
||||
};
|
||||
|
||||
return initialData;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import { useDataGridCustomizerOpenStateContext } from '@/features/orgs/projects/common/components/DataGridCustomizerControls/DataGridCustomizerOpenStateProvider';
|
||||
import { DataBrowserEmptyState } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserEmptyState';
|
||||
|
||||
function Description() {
|
||||
const { setOpen } = useDataGridCustomizerOpenStateContext();
|
||||
|
||||
function handleOnClick() {
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="link" onClick={handleOnClick} className="pr-2">
|
||||
Open Customize Table View
|
||||
</Button>
|
||||
to choose which columns to display.
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AllColumnsHiddenMessage() {
|
||||
return (
|
||||
<DataBrowserEmptyState
|
||||
title="All columns are hidden"
|
||||
description={<Description />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default AllColumnsHiddenMessage;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { render, screen } from '@/tests/testUtils';
|
||||
import type { Column } from 'react-table';
|
||||
import { expect, test } from 'vitest';
|
||||
import { expect, it } from 'vitest';
|
||||
import DataGrid from './DataGrid';
|
||||
|
||||
interface MockDataDetails {
|
||||
@@ -18,50 +18,54 @@ const mockData: MockDataDetails[] = [
|
||||
{ id: 2, name: 'bar' },
|
||||
];
|
||||
|
||||
test('should render an empty state if columns are not available', () => {
|
||||
render(<DataGrid columns={[]} data={[]} />);
|
||||
describe('DataGrid', () => {
|
||||
it('should render an empty state if columns are not available', () => {
|
||||
render(<DataGrid columns={[]} data={[]} />);
|
||||
|
||||
expect(screen.getByText(/columns not found/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render columns and empty state message if data is unavailable', () => {
|
||||
render(<DataGrid columns={mockColumns} data={[]} />);
|
||||
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('columnheader', { name: /id/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('columnheader', { name: /name/i }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/no data is available/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render custom empty state message if data is unavailable', () => {
|
||||
const customEmptyStateMessage = 'custom empty state message';
|
||||
|
||||
render(
|
||||
<DataGrid
|
||||
columns={mockColumns}
|
||||
data={[]}
|
||||
emptyStateMessage={customEmptyStateMessage}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(customEmptyStateMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should display a loading indicator', async () => {
|
||||
render(<DataGrid columns={mockColumns} data={[]} loading />);
|
||||
|
||||
// Activity indicator is not immediately displayed, so we need to wait
|
||||
expect(await screen.findByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render data if provided', () => {
|
||||
render(<DataGrid columns={mockColumns} data={mockData} />);
|
||||
|
||||
expect(screen.getAllByRole('row')).toHaveLength(2);
|
||||
expect(screen.getByRole('cell', { name: /1/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('cell', { name: /foo/i })).toBeInTheDocument();
|
||||
expect(screen.getByText(/columns not found/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render columns and empty state message if data is unavailable', () => {
|
||||
render(<DataGrid columns={mockColumns} data={[]} />);
|
||||
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole('columnheader', { name: /id/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('columnheader', { name: /name/i }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/no data is available/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render custom empty state message if data is unavailable', () => {
|
||||
const customEmptyStateMessage = 'custom empty state message';
|
||||
|
||||
render(
|
||||
<DataGrid
|
||||
columns={mockColumns}
|
||||
data={[]}
|
||||
emptyStateMessage={customEmptyStateMessage}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(customEmptyStateMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display a loading indicator', async () => {
|
||||
render(<DataGrid columns={mockColumns} data={[]} loading />);
|
||||
|
||||
// Activity indicator is not immediately displayed, so we need to wait
|
||||
expect(await screen.findByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render data if provided', () => {
|
||||
render(<DataGrid columns={mockColumns} data={mockData} />);
|
||||
|
||||
expect(screen.getAllByRole('row')).toHaveLength(2);
|
||||
expect(screen.getByRole('cell', { name: /1/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('cell', { name: /foo/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Spinner } from '@/components/ui/v3/spinner';
|
||||
import DataGridCustomizerOpenStateProvider from '@/features/orgs/projects/common/components/DataGridCustomizerControls/DataGridCustomizerOpenStateProvider';
|
||||
import { DataBrowserEmptyState } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserEmptyState';
|
||||
import type { UseDataGridOptions } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid/useDataGrid';
|
||||
import { DataGridBody } from '@/features/orgs/projects/storage/dataGrid/components/DataGridBody';
|
||||
@@ -7,11 +7,13 @@ import { DataGridConfigProvider } from '@/features/orgs/projects/storage/dataGri
|
||||
import { DataGridFrame } from '@/features/orgs/projects/storage/dataGrid/components/DataGridFrame';
|
||||
import type { DataGridHeaderProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridHeader';
|
||||
import { DataGridHeader } from '@/features/orgs/projects/storage/dataGrid/components/DataGridHeader';
|
||||
import { DataTableDesignProvider } from '@/features/orgs/projects/storage/dataGrid/providers/DataTableDesignProvider';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef, useEffect, useRef } from 'react';
|
||||
import { mergeRefs } from 'react-merge-refs';
|
||||
import type { Column, Row, SortingRule, TableOptions } from 'react-table';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import AllColumnsHiddenMessage from './AllColumnsHiddenMessage';
|
||||
import useDataGrid from './useDataGrid';
|
||||
|
||||
export interface DataGridProps<TColumnData extends object>
|
||||
@@ -65,6 +67,10 @@ export interface DataGridProps<TColumnData extends object>
|
||||
* Props to be passed to the `DataGridHeader` component.
|
||||
*/
|
||||
headerProps?: DataGridHeaderProps;
|
||||
/**
|
||||
* Determines whether the Grid is used for displaying files.
|
||||
*/
|
||||
isFileDataGrid?: boolean;
|
||||
}
|
||||
|
||||
function DataGrid<TColumnData extends object>(
|
||||
@@ -82,6 +88,7 @@ function DataGrid<TColumnData extends object>(
|
||||
onSort,
|
||||
loading,
|
||||
className,
|
||||
isFileDataGrid,
|
||||
}: DataGridProps<TColumnData>,
|
||||
ref: ForwardedRef<HTMLDivElement>,
|
||||
) {
|
||||
@@ -112,6 +119,8 @@ function DataGrid<TColumnData extends object>(
|
||||
}
|
||||
}, [allowSort, dataGridProps.state.sortBy, onSort, toggleAllRowsSelected]);
|
||||
|
||||
const allColumnsHidden =
|
||||
dataGridProps.allColumns.filter(({ isVisible }) => isVisible).length === 1;
|
||||
return (
|
||||
<DataGridConfigProvider
|
||||
toggleAllRowsSelected={toggleAllRowsSelected}
|
||||
@@ -119,38 +128,43 @@ function DataGrid<TColumnData extends object>(
|
||||
tableRef={tableRef}
|
||||
{...dataGridProps}
|
||||
>
|
||||
<>
|
||||
{controls}
|
||||
{columns.length === 0 && !loading && (
|
||||
<DataBrowserEmptyState
|
||||
title="Columns not found"
|
||||
description="Please create a column before adding data to the table."
|
||||
/>
|
||||
)}
|
||||
|
||||
{columns.length > 0 && (
|
||||
<Box
|
||||
ref={mergeRefs([ref, tableRef])}
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
className={twMerge(
|
||||
'overflow-x-auto',
|
||||
!loading && 'h-full',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<DataGridFrame>
|
||||
<DataGridHeader {...headerProps} />
|
||||
|
||||
<DataGridBody
|
||||
emptyStateMessage={emptyStateMessage}
|
||||
loading={loading}
|
||||
<DataTableDesignProvider>
|
||||
<DataGridCustomizerOpenStateProvider>
|
||||
<>
|
||||
{controls}
|
||||
{columns.length === 0 && !loading && (
|
||||
<DataBrowserEmptyState
|
||||
title="Columns not found"
|
||||
description="Please create a column before adding data to the table."
|
||||
/>
|
||||
</DataGridFrame>
|
||||
</Box>
|
||||
)}
|
||||
)}
|
||||
{columns.length > 0 && allColumnsHidden && (
|
||||
<AllColumnsHiddenMessage />
|
||||
)}
|
||||
{columns.length > 0 && !allColumnsHidden && (
|
||||
<div
|
||||
ref={mergeRefs([ref, tableRef])}
|
||||
className={cn(
|
||||
'box overflow-x-auto bg-background',
|
||||
{ 'h-[calc(100%-1px)]': !loading }, // need to set height like this to remove vertical scrollbar
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<DataGridFrame>
|
||||
<DataGridHeader {...headerProps} />
|
||||
<DataGridBody
|
||||
isFileDataGrid={isFileDataGrid}
|
||||
emptyStateMessage={emptyStateMessage}
|
||||
loading={loading}
|
||||
/>
|
||||
</DataGridFrame>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && <ActivityIndicator delay={1000} className="my-4" />}
|
||||
</>
|
||||
{loading && <Spinner className="my-4" />}
|
||||
</>
|
||||
</DataGridCustomizerOpenStateProvider>
|
||||
</DataTableDesignProvider>
|
||||
</DataGridConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Checkbox } from '@/components/ui/v2/Checkbox';
|
||||
import { Checkbox } from '@/components/ui/v3/checkbox';
|
||||
import { useTablePath } from '@/features/orgs/projects/database/common/hooks/useTablePath';
|
||||
import PersistenColumnConfigurationStorage from '@/features/orgs/projects/storage/dataGrid/utils/PersistenDataTableConfigurationStorage';
|
||||
import type { MutableRefObject } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { PluginHook, TableInstance, TableOptions } from 'react-table';
|
||||
import {
|
||||
useBlockLayout,
|
||||
useColumnOrder,
|
||||
useResizeColumns,
|
||||
useRowSelect,
|
||||
useSortBy,
|
||||
@@ -57,17 +60,26 @@ export default function useDataGrid<T extends object>(
|
||||
[],
|
||||
);
|
||||
|
||||
const tablePath = useTablePath();
|
||||
|
||||
const pluginHooks = [
|
||||
useBlockLayout,
|
||||
useResizeColumns,
|
||||
useSortBy,
|
||||
useRowSelect,
|
||||
useColumnOrder,
|
||||
];
|
||||
|
||||
const tableData = useTable<T>(
|
||||
{
|
||||
defaultColumn,
|
||||
...options,
|
||||
initialState: {
|
||||
hiddenColumns:
|
||||
PersistenColumnConfigurationStorage.getHiddenColumns(tablePath),
|
||||
columnOrder:
|
||||
PersistenColumnConfigurationStorage.getColumnOrder(tablePath),
|
||||
},
|
||||
},
|
||||
...pluginHooks,
|
||||
...plugins,
|
||||
@@ -76,25 +88,43 @@ export default function useDataGrid<T extends object>(
|
||||
? hooks.visibleColumns.push((columns) => [
|
||||
{
|
||||
id: 'selection-column',
|
||||
Header: ({ rows, getToggleAllRowsSelectedProps }: any) => (
|
||||
<Checkbox
|
||||
disabled={rows.length === 0}
|
||||
{...getToggleAllRowsSelectedProps({ style: null })}
|
||||
style={{
|
||||
...getToggleAllRowsSelectedProps().style,
|
||||
cursor: rows.length === 0 ? 'default' : 'pointer',
|
||||
}}
|
||||
/>
|
||||
),
|
||||
Header: ({ rows, getToggleAllRowsSelectedProps }: any) => {
|
||||
const { indeterminate, style, onChange, ...props } =
|
||||
getToggleAllRowsSelectedProps();
|
||||
|
||||
function handleCheckedChange(newCheckedState: boolean) {
|
||||
onChange({ target: { checked: newCheckedState } });
|
||||
}
|
||||
return (
|
||||
<Checkbox
|
||||
className="border-[#21324b] data-[state=checked]:!border-transparent dark:border-[#dfecf5]"
|
||||
disabled={rows.length === 0}
|
||||
{...props}
|
||||
style={{
|
||||
...style,
|
||||
cursor: rows.length === 0 ? 'default' : 'pointer',
|
||||
}}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Cell: ({ row }: any) => {
|
||||
const originalValue = row.original as any;
|
||||
|
||||
const { indeterminate, onChange, ...props } =
|
||||
row.getToggleRowSelectedProps();
|
||||
|
||||
function handleCheckedChange(newCheckedState: boolean) {
|
||||
onChange({ target: { checked: newCheckedState } });
|
||||
}
|
||||
return (
|
||||
<Checkbox
|
||||
{...row.getToggleRowSelectedProps()}
|
||||
className="border-[#21324b] data-[state=checked]:!border-transparent dark:border-[#dfecf5]"
|
||||
{...props}
|
||||
// disable selection if row is just a upload preview
|
||||
checked={originalValue.uploading ? false : row.isSelected}
|
||||
disabled={originalValue.uploading}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import type { DataGridProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid/DataGrid';
|
||||
import { DataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
||||
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
|
||||
import { isNotEmptyValue } from '@/lib/utils';
|
||||
import { useDataTableDesignContext } from '@/features/orgs/projects/storage/dataGrid/providers/DataTableDesignProvider';
|
||||
import { cn, isNotEmptyValue } from '@/lib/utils';
|
||||
import type { DetailedHTMLProps, HTMLProps, KeyboardEvent } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import type { Row } from 'react-table';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface DataGridBodyProps<T extends object>
|
||||
extends Omit<
|
||||
@@ -15,10 +14,7 @@ export interface DataGridBodyProps<T extends object>
|
||||
'children'
|
||||
>,
|
||||
Pick<DataGridProps<T>, 'emptyStateMessage' | 'loading'> {
|
||||
/**
|
||||
* Determines whether column insertion is allowed.
|
||||
*/
|
||||
allowInsertColumn?: boolean;
|
||||
isFileDataGrid?: boolean;
|
||||
}
|
||||
|
||||
// TODO: Get rid of Data Browser related code from here. This component should
|
||||
@@ -26,11 +22,12 @@ export interface DataGridBodyProps<T extends object>
|
||||
export default function DataGridBody<T extends object>({
|
||||
emptyStateMessage = 'No data is available',
|
||||
loading,
|
||||
allowInsertColumn,
|
||||
isFileDataGrid,
|
||||
...props
|
||||
}: DataGridBodyProps<T>) {
|
||||
const { getTableBodyProps, totalColumnsWidth, rows, prepareRow } =
|
||||
useDataGridConfig<T>();
|
||||
const context = useDataTableDesignContext();
|
||||
|
||||
const bodyRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -137,35 +134,18 @@ export default function DataGridBody<T extends object>({
|
||||
}
|
||||
}
|
||||
|
||||
const getBackgroundCellColor = (
|
||||
row: Row<T>,
|
||||
column: DataBrowserGridColumn<T>,
|
||||
) => {
|
||||
// Grey out files not uploaded
|
||||
if (!row.values.isUploaded) {
|
||||
return 'grey.200';
|
||||
}
|
||||
|
||||
if (column.isDisabled) {
|
||||
return 'grey.100';
|
||||
}
|
||||
|
||||
return 'background.paper';
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...getTableBodyProps()} ref={bodyRef} {...props}>
|
||||
{rows.length === 0 && !loading && (
|
||||
<div className="flex flex-nowrap">
|
||||
<Box
|
||||
className="inline-flex h-12 items-center border-b-1 border-r-1 px-2 py-1.5 text-xs"
|
||||
sx={{ color: 'text.secondary' }}
|
||||
<div
|
||||
className="box inline-flex h-12 items-center border-b-1 border-r-1 px-2 py-1.5 text-xs dark:!text-[#a2b3be]"
|
||||
style={{
|
||||
width: totalColumnsWidth,
|
||||
}}
|
||||
>
|
||||
{emptyStateMessage}
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -174,6 +154,7 @@ export default function DataGridBody<T extends object>({
|
||||
|
||||
const rowProps = row.getRowProps({
|
||||
style: {
|
||||
height: context.rowDensity === 'comfortable' ? '3rem' : '2rem',
|
||||
width: totalColumnsWidth,
|
||||
},
|
||||
});
|
||||
@@ -182,7 +163,12 @@ export default function DataGridBody<T extends object>({
|
||||
<div
|
||||
{...rowProps}
|
||||
id={row.id}
|
||||
className="flex scroll-mt-10"
|
||||
className={cn(
|
||||
'flex scroll-mt-10 border-b-1 border-b-transparent last:border-b-data-table-border-color',
|
||||
isFileDataGrid && !row.values.isUploaded
|
||||
? 'bg-disabled'
|
||||
: 'odd:bg-data-cell-bg-odd even:bg-data-cell-bg hover:bg-data-cell-bg-hover',
|
||||
)}
|
||||
role="row"
|
||||
onKeyDown={(event) => handleKeyDown(event, row)}
|
||||
tabIndex={-1}
|
||||
@@ -198,23 +184,16 @@ export default function DataGridBody<T extends object>({
|
||||
|
||||
return (
|
||||
<DataGridCell
|
||||
{...cell.getCellProps({
|
||||
style: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
})}
|
||||
{...cell.getCellProps()}
|
||||
cell={cell}
|
||||
sx={{
|
||||
backgroundColor: getBackgroundCellColor(row, column),
|
||||
color: isCellDisabled ? 'text.secondary' : 'text.primary',
|
||||
}}
|
||||
className={twMerge(
|
||||
'h-12 font-display text-xs motion-safe:transition-colors',
|
||||
'border-b-1 border-r-1',
|
||||
className={cn(
|
||||
'group !inline-flex items-center bg-inherit font-display text-xs',
|
||||
'border-b-0 border-r-1',
|
||||
'scroll-ml-8 scroll-mt-[57px]',
|
||||
'border-r-transparent last:border-r-data-table-border-color',
|
||||
column.id === 'selection-column' &&
|
||||
'sticky left-0 z-20 justify-center px-0',
|
||||
isCellDisabled ? 'text-secondary' : 'text-primary-text',
|
||||
)}
|
||||
isEditable={!column.isDisabled && column.isEditable}
|
||||
id={cellIndex.toString()}
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
import { ReadOnlyToggle } from '@/components/presentational/ReadOnlyToggle';
|
||||
import { Dropdown } from '@/components/ui/v2/Dropdown';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/v3/dropdown-menu';
|
||||
import type { CommonDataGridCellProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
||||
import { useDataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
||||
import type { MouseEvent, KeyboardEvent as ReactKeyboardEvent } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import {
|
||||
useState,
|
||||
type MouseEvent,
|
||||
type KeyboardEvent as ReactKeyboardEvent,
|
||||
} from 'react';
|
||||
|
||||
export type DataGridBooleanCellProps<TData extends object> =
|
||||
CommonDataGridCellProps<TData, boolean | null | undefined>;
|
||||
export interface DataGridBooleanCellProps<TData extends object>
|
||||
extends CommonDataGridCellProps<TData, boolean | null | undefined> {
|
||||
rowId?: string;
|
||||
}
|
||||
|
||||
export default function DataGridBooleanCell<TData extends object>({
|
||||
onSave,
|
||||
optimisticValue,
|
||||
temporaryValue,
|
||||
onTemporaryValueChange,
|
||||
rowId,
|
||||
cell: {
|
||||
column: { isNullable },
|
||||
column: { isNullable, getHeaderProps },
|
||||
},
|
||||
}: DataGridBooleanCellProps<TData>) {
|
||||
const {
|
||||
inputRef,
|
||||
isEditing,
|
||||
focusCell,
|
||||
editCell,
|
||||
cancelEditCell,
|
||||
isSelected,
|
||||
} = useDataGridCell<HTMLButtonElement>();
|
||||
const { inputRef, focusCell, cancelEditCell, focusNextCell, editCell } =
|
||||
useDataGridCell<HTMLButtonElement>();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
async function handleMenuClick(
|
||||
event: MouseEvent<HTMLLIElement> | ReactKeyboardEvent<HTMLLIElement>,
|
||||
event: MouseEvent<HTMLDivElement> | ReactKeyboardEvent<HTMLDivElement>,
|
||||
value: boolean | null,
|
||||
) {
|
||||
event.stopPropagation();
|
||||
@@ -48,74 +53,83 @@ export default function DataGridBooleanCell<TData extends object>({
|
||||
// We need to restore the temporary value, because editing was cancelled
|
||||
if (event.key === 'Escape' && onTemporaryValueChange) {
|
||||
event.stopPropagation();
|
||||
|
||||
onTemporaryValueChange(optimisticValue);
|
||||
cancelEditCell();
|
||||
focusCell();
|
||||
}
|
||||
|
||||
if (event.key === 'Tab' && onSave) {
|
||||
await onSave(temporaryValue);
|
||||
cancelEditCell();
|
||||
setOpen(false);
|
||||
focusNextCell();
|
||||
}
|
||||
}
|
||||
|
||||
function handleTemporaryValueChange(value: boolean | null) {
|
||||
if (onTemporaryValueChange) {
|
||||
function handleTemporaryValueChange(
|
||||
event: ReactKeyboardEvent<HTMLDivElement>,
|
||||
value: boolean | null,
|
||||
) {
|
||||
if (onTemporaryValueChange && event.key === 'Enter') {
|
||||
onTemporaryValueChange(value);
|
||||
}
|
||||
}
|
||||
// needed to open with enter
|
||||
function handleTriggerClick(event: MouseEvent) {
|
||||
if (!open && !event.nativeEvent.isTrusted) {
|
||||
setOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
return isSelected ? (
|
||||
<Dropdown.Root id="boolean-data-editor" className="h-full w-full">
|
||||
<Dropdown.Trigger
|
||||
id="boolean-trigger"
|
||||
className={twMerge(
|
||||
'h-full w-full border-none p-0 outline-none',
|
||||
isEditing && 'p-1.5',
|
||||
)}
|
||||
function handleOpenChange(newOpenState: boolean) {
|
||||
if (newOpenState) {
|
||||
editCell();
|
||||
} else {
|
||||
cancelEditCell();
|
||||
}
|
||||
setOpen(newOpenState);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={handleOpenChange} modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
ref={inputRef}
|
||||
onClick={editCell}
|
||||
autoFocus={false}
|
||||
sx={{ '&:hover': { backgroundColor: 'transparent !important' } }}
|
||||
className="h-full w-full focus-visible:border-transparent focus-visible:outline-none"
|
||||
onClick={handleTriggerClick}
|
||||
>
|
||||
<ReadOnlyToggle checked={optimisticValue} />
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content
|
||||
menu
|
||||
disablePortal
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
id={rowId}
|
||||
style={{ width: getHeaderProps().style?.width }}
|
||||
onKeyDown={handleMenuKeyDown}
|
||||
PaperProps={{ className: 'w-[200px]' }}
|
||||
TransitionProps={{ onExited: focusCell }}
|
||||
className="rounded-none border-2 !border-t-0 border-[#0052cd] bg-data-cell-bg"
|
||||
>
|
||||
<Dropdown.Item
|
||||
selected={optimisticValue === true}
|
||||
onKeyUp={() => handleTemporaryValueChange(true)}
|
||||
<DropdownMenuCheckboxItem
|
||||
className="hover:!bg-data-cell-bg-hover"
|
||||
checked={optimisticValue === true}
|
||||
onKeyUp={(event) => handleTemporaryValueChange(event, true)}
|
||||
onClick={(event) => handleMenuClick(event, true)}
|
||||
>
|
||||
<ReadOnlyToggle checked />
|
||||
</Dropdown.Item>
|
||||
|
||||
<Dropdown.Item
|
||||
selected={optimisticValue === false}
|
||||
onKeyUp={() => handleTemporaryValueChange(false)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
className="hover:!bg-data-cell-bg-hover"
|
||||
checked={optimisticValue === false}
|
||||
onKeyUp={(event) => handleTemporaryValueChange(event, false)}
|
||||
onClick={(event) => handleMenuClick(event, false)}
|
||||
>
|
||||
<ReadOnlyToggle checked={false} />
|
||||
</Dropdown.Item>
|
||||
|
||||
</DropdownMenuCheckboxItem>
|
||||
{isNullable && (
|
||||
<Dropdown.Item
|
||||
selected={optimisticValue === null}
|
||||
onKeyUp={() => handleTemporaryValueChange(null)}
|
||||
<DropdownMenuCheckboxItem
|
||||
className="hover:!bg-data-cell-bg-hover"
|
||||
checked={optimisticValue === null}
|
||||
onKeyUp={(event) => handleTemporaryValueChange(event, null)}
|
||||
onClick={(event) => handleMenuClick(event, null)}
|
||||
>
|
||||
<ReadOnlyToggle checked={null} />
|
||||
</Dropdown.Item>
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
) : (
|
||||
<ReadOnlyToggle checked={optimisticValue} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Tooltip, useTooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { useTooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import type {
|
||||
ColumnType,
|
||||
DataBrowserGridCell,
|
||||
DataBrowserGridCellProps,
|
||||
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import { cn, isNotEmptyValue } from '@/lib/utils';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { Copy } from 'lucide-react';
|
||||
import type {
|
||||
CSSProperties,
|
||||
FocusEvent,
|
||||
JSXElementConstructor,
|
||||
KeyboardEvent,
|
||||
MouseEvent,
|
||||
PropsWithChildren,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
ReactPortal,
|
||||
@@ -24,10 +28,15 @@ import {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import DataGridCellProvider from './DataGridCellProvider';
|
||||
import useDataGridCell from './useDataGridCell';
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/v3/tooltip';
|
||||
|
||||
export interface CommonDataGridCellProps<TData extends object, TValue = any>
|
||||
extends DataBrowserGridCellProps<TData, TValue> {
|
||||
/**
|
||||
@@ -54,7 +63,7 @@ export interface CommonDataGridCellProps<TData extends object, TValue = any>
|
||||
onTemporaryValueChange?: (value: TValue) => void;
|
||||
}
|
||||
|
||||
export interface DataGridCellProps<TData extends object> extends BoxProps {
|
||||
export interface DataGridCellProps<TData extends object> {
|
||||
/**
|
||||
* Current cell's props.
|
||||
*/
|
||||
@@ -67,6 +76,9 @@ export interface DataGridCellProps<TData extends object> extends BoxProps {
|
||||
* Determines the column's type.
|
||||
*/
|
||||
columnType?: ColumnType;
|
||||
className?: string;
|
||||
id?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
function DataGridCellContent<TData extends object = {}>({
|
||||
@@ -79,7 +91,7 @@ function DataGridCellContent<TData extends object = {}>({
|
||||
row,
|
||||
},
|
||||
...props
|
||||
}: DataGridCellProps<TData>) {
|
||||
}: PropsWithChildren<DataGridCellProps<TData>>) {
|
||||
const { openAlertDialog } = useDialog();
|
||||
|
||||
const {
|
||||
@@ -194,6 +206,8 @@ function DataGridCellContent<TData extends object = {}>({
|
||||
},
|
||||
});
|
||||
|
||||
focusCell();
|
||||
cancelEditCell();
|
||||
// Syncing optimistic value with server-side value
|
||||
setTemporaryValue(data.original[id.toString()]);
|
||||
setOptimisticValue(data.original[id.toString()]);
|
||||
@@ -203,17 +217,31 @@ function DataGridCellContent<TData extends object = {}>({
|
||||
// Resetting values
|
||||
setTemporaryValue(latestOptimisticValue);
|
||||
setOptimisticValue(latestOptimisticValue);
|
||||
activateInput();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBlur(event: FocusEvent<HTMLDivElement>) {
|
||||
// We are deselecting cell only if focus target is not a descendant of it.
|
||||
if (!isEditable || event.currentTarget.contains(event.relatedTarget)) {
|
||||
const { id: currentRowId } = row.original as { id: string };
|
||||
const isTargetDropdownMenu =
|
||||
event.relatedTarget?.id === currentRowId ||
|
||||
event.relatedTarget?.parentElement?.id === currentRowId;
|
||||
|
||||
if (
|
||||
!isEditable ||
|
||||
event.currentTarget.contains(event.relatedTarget) ||
|
||||
(isEditing && type === 'boolean' && isTargetDropdownMenu)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await handleSave(temporaryValue);
|
||||
closeTooltip();
|
||||
if (type !== 'boolean') {
|
||||
await handleSave(temporaryValue);
|
||||
}
|
||||
if (tooltipOpen) {
|
||||
closeTooltip();
|
||||
}
|
||||
deselectCell();
|
||||
}
|
||||
|
||||
@@ -227,8 +255,7 @@ function DataGridCellContent<TData extends object = {}>({
|
||||
if (!isNullable) {
|
||||
openTooltip(
|
||||
<span>
|
||||
<strong>{id}</strong>
|
||||
is non-nullable.
|
||||
<strong>{id}</strong> is non-nullable.
|
||||
</span>,
|
||||
);
|
||||
|
||||
@@ -308,14 +335,14 @@ function DataGridCellContent<TData extends object = {}>({
|
||||
}
|
||||
|
||||
const content = (
|
||||
<Box
|
||||
<div
|
||||
ref={cellRef}
|
||||
className={twMerge(
|
||||
'relative grid h-full w-full cursor-default grid-flow-col items-center gap-1',
|
||||
className={cn(
|
||||
'relative grid w-full cursor-default grid-flow-col items-center gap-1 border-divider px-2 py-1.5 text-primary-text',
|
||||
isEditable &&
|
||||
'focus-within:outline-none focus-within:ring-0 focus:ring-0',
|
||||
isSelected && 'shadow-outline',
|
||||
isEditing ? 'p-0.5 shadow-outline-dark' : 'px-2 py-1.5',
|
||||
isEditing && 'shadow-outline-dark',
|
||||
className,
|
||||
)}
|
||||
onFocus={handleFocus}
|
||||
@@ -324,7 +351,6 @@ function DataGridCellContent<TData extends object = {}>({
|
||||
tabIndex={isEditable ? 0 : undefined}
|
||||
onClick={handleClick}
|
||||
role="textbox"
|
||||
sx={{ backgroundColor: 'transparent' }}
|
||||
{...props}
|
||||
>
|
||||
{Children.map(
|
||||
@@ -338,30 +364,62 @@ function DataGridCellContent<TData extends object = {}>({
|
||||
if (!isValidElement(child)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cloneElement(child, {
|
||||
const { id: rowId } = row.original as { id: string };
|
||||
const clonedChild = cloneElement(child, {
|
||||
...child.props,
|
||||
onSave: handleSave,
|
||||
optimisticValue,
|
||||
onOptimisticValueChange: setOptimisticValue,
|
||||
temporaryValue,
|
||||
onTemporaryValueChange: setTemporaryValue,
|
||||
rowId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{clonedChild}
|
||||
{id !== 'preview-column' &&
|
||||
type !== 'boolean' &&
|
||||
isNotEmptyValue(optimisticValue) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
const copiableValue =
|
||||
typeof optimisticValue === 'object'
|
||||
? JSON.stringify(optimisticValue)
|
||||
: String(optimisticValue).replace(/\\n/gi, '\n');
|
||||
|
||||
copy(copiableValue, 'Value');
|
||||
}}
|
||||
className="-ml-px h-6 w-6 border-transparent bg-transparent p-1 text-disabled opacity-0 hover:bg-transparent group-hover:opacity-100"
|
||||
aria-label="Copy value"
|
||||
>
|
||||
<Copy width={16} height={16} />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
|
||||
// TODO: https://github.com/nhost/nhost/issues/3677
|
||||
if (isEditable) {
|
||||
return (
|
||||
<Tooltip
|
||||
disableHoverListener
|
||||
disableFocusListener
|
||||
delayDuration={100}
|
||||
open={tooltipOpen}
|
||||
title={tooltipTitle || ''}
|
||||
TransitionProps={{ onExited: resetTooltipTitle }}
|
||||
onOpenChange={(newState) => {
|
||||
if (!newState) {
|
||||
resetTooltipTitle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
||||
<TooltipContent>{tooltipTitle}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -370,7 +428,7 @@ function DataGridCellContent<TData extends object = {}>({
|
||||
}
|
||||
|
||||
export default function DataGridCell<TData extends object>(
|
||||
props: DataGridCellProps<TData>,
|
||||
props: PropsWithChildren<DataGridCellProps<TData>>,
|
||||
) {
|
||||
return (
|
||||
<DataGridCellProvider>
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
import { Input, inputClasses } from '@/components/ui/v2/Input';
|
||||
import type { TextProps } from '@/components/ui/v2/Text';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Input } from '@/components/ui/v3/input';
|
||||
import type { CommonDataGridCellProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
||||
import { useDataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getDateComponents } from '@/utils/getDateComponents';
|
||||
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { ChangeEvent, KeyboardEvent, Ref } from 'react';
|
||||
|
||||
export interface DataGridDateCellProps<TData extends object>
|
||||
extends CommonDataGridCellProps<TData, string> {
|
||||
/**
|
||||
* Props to be passed to date display.
|
||||
*/
|
||||
dateProps?: TextProps;
|
||||
/**
|
||||
* Props to be passed to time display.
|
||||
*/
|
||||
timeProps?: TextProps;
|
||||
}
|
||||
extends CommonDataGridCellProps<TData, string> {}
|
||||
|
||||
export default function DataGridDateCell<TData extends object>({
|
||||
onSave,
|
||||
@@ -27,13 +16,8 @@ export default function DataGridDateCell<TData extends object>({
|
||||
cell: {
|
||||
column: { specificType },
|
||||
},
|
||||
dateProps,
|
||||
timeProps,
|
||||
className,
|
||||
}: DataGridDateCellProps<TData>) {
|
||||
const { className: dateClassName, ...restDateProps } = dateProps || {};
|
||||
const { className: timeClassName, ...restTimeProps } = timeProps || {};
|
||||
|
||||
// Note: No date (year-month-day) is saved for time / timetz columns, so we
|
||||
// need to add it manually.
|
||||
const date =
|
||||
@@ -51,8 +35,7 @@ export default function DataGridDateCell<TData extends object>({
|
||||
),
|
||||
});
|
||||
|
||||
const { inputRef, focusCell, isEditing, cancelEditCell } =
|
||||
useDataGridCell<HTMLInputElement>();
|
||||
const { inputRef, isEditing } = useDataGridCell<HTMLInputElement>();
|
||||
|
||||
async function handleSave() {
|
||||
if (onSave) {
|
||||
@@ -77,8 +60,6 @@ export default function DataGridDateCell<TData extends object>({
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
await handleSave();
|
||||
await focusCell();
|
||||
cancelEditCell();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,78 +72,38 @@ export default function DataGridDateCell<TData extends object>({
|
||||
if (isEditing) {
|
||||
return (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
ref={inputRef as Ref<HTMLInputElement>}
|
||||
value={
|
||||
temporaryValue !== null && typeof temporaryValue !== 'undefined'
|
||||
? temporaryValue
|
||||
: ''
|
||||
}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch"
|
||||
sx={{
|
||||
[`&.${inputClasses.focused}`]: {
|
||||
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
|
||||
borderColor: 'transparent !important',
|
||||
borderRadius: 0,
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? `${theme.palette.secondary[100]} !important`
|
||||
: `${theme.palette.common.white} !important`,
|
||||
},
|
||||
[`& .${inputClasses.input}`]: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
slotProps={{
|
||||
inputWrapper: { className: 'h-full' },
|
||||
input: { className: 'h-full' },
|
||||
inputRoot: {
|
||||
className:
|
||||
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
|
||||
},
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
wrapperClassName="absolute top-0 z-10 w-full top-0 left-0 h-full"
|
||||
className="h-full w-full resize-none rounded-none border-none px-2 py-1.5 !text-xs outline-none focus-within:rounded-none focus-within:border-transparent focus-within:bg-white focus-within:shadow-[inset_0_0_0_1.5px_rgba(0,82,205,1)] focus:outline-none focus:ring-0 dark:focus-within:bg-theme-grey-200"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!optimisticValue) {
|
||||
return (
|
||||
<Text className="truncate text-xs" color="secondary">
|
||||
null
|
||||
</Text>
|
||||
);
|
||||
return <p className="truncate text-xs text-secondary">null</p>;
|
||||
}
|
||||
|
||||
if (specificType === 'interval') {
|
||||
return <Text className="truncate text-xs">{optimisticValue}</Text>;
|
||||
return <p className="truncate text-xs">{optimisticValue}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={twMerge('grid grid-flow-row', className)}>
|
||||
{specificType !== 'time' && specificType !== 'timetz' && (
|
||||
<Text
|
||||
className={twMerge('truncate text-xs', dateClassName)}
|
||||
{...restDateProps}
|
||||
>
|
||||
{[year, month, day].filter(Boolean).join('-')}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{specificType !== 'date' && (
|
||||
<Text
|
||||
className={twMerge('truncate text-xs', timeClassName)}
|
||||
color={
|
||||
specificType === 'time' || specificType === 'timetz'
|
||||
? 'primary'
|
||||
: 'secondary'
|
||||
}
|
||||
{...restTimeProps}
|
||||
>
|
||||
{[hour, minute, second].filter(Boolean).join(':')}
|
||||
</Text>
|
||||
)}
|
||||
<div className={cn('grid grid-flow-row', className)}>
|
||||
<p className="truncate text-xs">
|
||||
{specificType !== 'time' && specificType !== 'timetz' && (
|
||||
<span>{[year, month, day].filter(Boolean).join('-')}</span>
|
||||
)}{' '}
|
||||
{specificType !== 'date' && (
|
||||
<span>{[hour, minute, second].filter(Boolean).join(':')}</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import { Input, inputClasses } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import type { CommonDataGridCellProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
||||
import { useDataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
||||
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||
|
||||
export type DataGridDecimalCellProps<TData extends object> =
|
||||
CommonDataGridCellProps<TData, number | string | null>;
|
||||
|
||||
export default function DataGridDecimalCell<TData extends object>({
|
||||
onSave,
|
||||
optimisticValue,
|
||||
temporaryValue,
|
||||
onTemporaryValueChange,
|
||||
}: DataGridDecimalCellProps<TData>) {
|
||||
const { inputRef, focusCell, isEditing, cancelEditCell } =
|
||||
useDataGridCell<HTMLInputElement>();
|
||||
|
||||
async function handleSave() {
|
||||
if (onSave) {
|
||||
if (typeof temporaryValue === 'string') {
|
||||
await onSave(parseFloat(temporaryValue));
|
||||
} else if (typeof temporaryValue === 'number') {
|
||||
await onSave(temporaryValue);
|
||||
} else {
|
||||
await onSave(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
||||
if (
|
||||
event.key === 'ArrowLeft' ||
|
||||
event.key === 'ArrowRight' ||
|
||||
event.key === 'ArrowUp' ||
|
||||
event.key === 'ArrowDown' ||
|
||||
event.key === 'Backspace'
|
||||
) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (event.key === 'Tab') {
|
||||
await handleSave();
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
await handleSave();
|
||||
await focusCell();
|
||||
cancelEditCell();
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
if (onTemporaryValueChange) {
|
||||
onTemporaryValueChange(event.target.value ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
ref={inputRef}
|
||||
value={
|
||||
temporaryValue !== null && typeof temporaryValue !== 'undefined'
|
||||
? temporaryValue
|
||||
: ''
|
||||
}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch"
|
||||
sx={{
|
||||
[`&.${inputClasses.focused}`]: {
|
||||
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
|
||||
borderColor: 'transparent !important',
|
||||
borderRadius: 0,
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? `${theme.palette.secondary[100]} !important`
|
||||
: `${theme.palette.common.white} !important`,
|
||||
},
|
||||
[`& .${inputClasses.input}`]: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
slotProps={{
|
||||
inputWrapper: { className: 'h-full' },
|
||||
input: { className: 'h-full' },
|
||||
inputRoot: {
|
||||
className:
|
||||
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (optimisticValue === null || typeof optimisticValue === 'undefined') {
|
||||
return (
|
||||
<Text className="truncate !text-xs" color="disabled">
|
||||
null
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return <Text className="truncate !text-xs">{optimisticValue}</Text>;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './DataGridDecimalCell';
|
||||
export { default as DataGridDecimalCell } from './DataGridDecimalCell';
|
||||
@@ -2,9 +2,7 @@ import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/da
|
||||
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
|
||||
import { DataGridHeaderButton } from '@/features/orgs/projects/storage/dataGrid/components/DataGridHeaderButton';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTheme } from '@mui/material';
|
||||
import type { DetailedHTMLProps, HTMLProps } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface HeaderActionProps
|
||||
extends DetailedHTMLProps<HTMLProps<HTMLElement>, HTMLElement> {}
|
||||
@@ -22,55 +20,54 @@ export default function DataGridHeader({
|
||||
...props
|
||||
}: DataGridHeaderProps) {
|
||||
const { flatHeaders } = useDataGridConfig();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
className={cn(
|
||||
'sticky top-0 z-30 inline-flex w-full items-center',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{flatHeaders.map((column: DataBrowserGridColumn) => {
|
||||
const sortByProps = column.getSortByToggleProps();
|
||||
const headerProps = column.getHeaderProps({
|
||||
style: { display: 'inline-flex' },
|
||||
...sortByProps,
|
||||
});
|
||||
{flatHeaders
|
||||
.filter(({ isVisible }) => isVisible)
|
||||
.map((column: DataBrowserGridColumn) => {
|
||||
const sortByProps = column.getSortByToggleProps();
|
||||
const headerProps = column.getHeaderProps({
|
||||
style: { display: 'inline-flex' },
|
||||
...sortByProps,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group relative inline-flex self-stretch overflow-hidden font-display text-xs font-bold focus:outline-none focus-visible:outline-none',
|
||||
'border-b-1 border-r-1',
|
||||
{ 'sticky left-0 max-w-2': column.id === 'selection-column' },
|
||||
'dark:text-[#dfecf5]',
|
||||
)}
|
||||
style={{
|
||||
...headerProps.style,
|
||||
backgroundColor: column.isDisabled
|
||||
? theme.palette.background.default
|
||||
: theme.palette.background.paper,
|
||||
maxWidth:
|
||||
column.id === 'selection-column'
|
||||
? 32
|
||||
: headerProps.style?.maxWidth,
|
||||
width:
|
||||
column.id === 'selection-column'
|
||||
? '100%'
|
||||
: headerProps.style?.width,
|
||||
zIndex:
|
||||
column.id === 'selection-column'
|
||||
? 10
|
||||
: headerProps.style?.zIndex,
|
||||
position: undefined,
|
||||
}}
|
||||
key={column.id}
|
||||
>
|
||||
<DataGridHeaderButton column={column} headerProps={headerProps} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group relative inline-flex self-stretch overflow-hidden font-display text-xs font-bold focus:outline-none focus-visible:outline-none',
|
||||
'border-b-1 border-r-1',
|
||||
'bg-paper',
|
||||
{ 'sticky left-0 max-w-2': column.id === 'selection-column' },
|
||||
)}
|
||||
style={{
|
||||
...headerProps.style,
|
||||
maxWidth:
|
||||
column.id === 'selection-column'
|
||||
? 32
|
||||
: headerProps.style?.maxWidth,
|
||||
width:
|
||||
column.id === 'selection-column'
|
||||
? '100%'
|
||||
: headerProps.style?.width,
|
||||
zIndex:
|
||||
column.id === 'selection-column'
|
||||
? 10
|
||||
: headerProps.style?.zIndex,
|
||||
position: undefined,
|
||||
}}
|
||||
key={column.id}
|
||||
>
|
||||
<DataGridHeaderButton column={column} headerProps={headerProps} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ArrowDown, ArrowUp } from 'lucide-react';
|
||||
import type { TableHeaderProps } from 'react-table';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
interface DataGridHeaderButtonProps<T extends object> {
|
||||
column: DataBrowserGridColumn<T>;
|
||||
@@ -38,7 +38,7 @@ export default function DataGridHeaderButton<T extends object>({
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={twMerge(
|
||||
className={cn(
|
||||
'h-fit p-0 text-xs focus:outline-none motion-safe:transition-colors dark:hover:bg-[#21262d]',
|
||||
)}
|
||||
disabled={column.isDisabled || column.disableSortBy}
|
||||
@@ -65,7 +65,7 @@ export default function DataGridHeaderButton<T extends object>({
|
||||
{...column.getResizerProps({
|
||||
onClick: (event: Event) => event.stopPropagation(),
|
||||
})}
|
||||
className="absolute -right-0.5 bottom-0 top-0 z-10 h-full w-1.5 group-hover:bg-slate-900 group-hover:bg-opacity-20 group-active:bg-slate-900 group-active:bg-opacity-20 motion-safe:transition-colors"
|
||||
className="absolute -right-0.5 bottom-0 top-0 z-10 h-full w-2 group-hover:bg-slate-900 group-hover:bg-opacity-20 group-active:bg-slate-900 group-active:bg-opacity-20 motion-safe:transition-colors"
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Input, inputClasses } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import {
|
||||
useDataGridCell,
|
||||
type CommonDataGridCellProps,
|
||||
} from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
||||
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||
|
||||
export type DataGridIntegerCellProps<TData extends object> =
|
||||
CommonDataGridCellProps<TData, number | null>;
|
||||
|
||||
export default function DataGridIntegerCell<TData extends object>({
|
||||
onSave,
|
||||
optimisticValue,
|
||||
temporaryValue,
|
||||
onTemporaryValueChange,
|
||||
}: DataGridIntegerCellProps<TData>) {
|
||||
const { inputRef, focusCell, isEditing, cancelEditCell } =
|
||||
useDataGridCell<HTMLInputElement>();
|
||||
|
||||
async function handleSave() {
|
||||
if (onSave) {
|
||||
if (typeof temporaryValue === 'number') {
|
||||
await onSave(temporaryValue);
|
||||
} else {
|
||||
await onSave(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
||||
if (
|
||||
event.key === 'ArrowLeft' ||
|
||||
event.key === 'ArrowRight' ||
|
||||
event.key === 'ArrowUp' ||
|
||||
event.key === 'ArrowDown' ||
|
||||
event.key === 'Backspace'
|
||||
) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (event.key === 'Tab') {
|
||||
await handleSave();
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
await handleSave();
|
||||
await focusCell();
|
||||
cancelEditCell();
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
if (onTemporaryValueChange) {
|
||||
if (event.target.value) {
|
||||
onTemporaryValueChange(parseInt(event.target.value, 10));
|
||||
} else {
|
||||
onTemporaryValueChange(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
ref={inputRef}
|
||||
value={
|
||||
temporaryValue !== null && typeof temporaryValue !== 'undefined'
|
||||
? temporaryValue
|
||||
: ''
|
||||
}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch"
|
||||
sx={{
|
||||
[`&.${inputClasses.focused}`]: {
|
||||
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
|
||||
borderColor: 'transparent !important',
|
||||
borderRadius: 0,
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? `${theme.palette.secondary[100]} !important`
|
||||
: `${theme.palette.common.white} !important`,
|
||||
},
|
||||
[`& .${inputClasses.input}`]: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
slotProps={{
|
||||
inputWrapper: { className: 'h-full' },
|
||||
input: { className: 'h-full' },
|
||||
inputRoot: {
|
||||
className:
|
||||
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (optimisticValue === null || typeof optimisticValue === 'undefined') {
|
||||
return (
|
||||
<Text className="truncate !text-xs" color="disabled">
|
||||
null
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return <Text className="truncate !text-xs">{optimisticValue}</Text>;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './DataGridIntegerCell';
|
||||
export { default as DataGridIntegerCell } from './DataGridIntegerCell';
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Input } from '@/components/ui/v3/input';
|
||||
import type { CommonDataGridCellProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
||||
import { useDataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
||||
import { isNotEmptyValue } from '@/lib/utils';
|
||||
import {
|
||||
useEffect,
|
||||
type ChangeEvent,
|
||||
type KeyboardEvent,
|
||||
type Ref,
|
||||
} from 'react';
|
||||
|
||||
export type DataGridNumericCellProps<TData extends object> =
|
||||
CommonDataGridCellProps<TData, number | null>;
|
||||
|
||||
export default function DataGridNumericCell<TData extends object>({
|
||||
onSave,
|
||||
optimisticValue,
|
||||
temporaryValue,
|
||||
onTemporaryValueChange,
|
||||
cell: {
|
||||
column: { dataType },
|
||||
},
|
||||
}: DataGridNumericCellProps<TData>) {
|
||||
const { inputRef, isEditing } = useDataGridCell<HTMLInputElement>();
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
function preventWheelInNumberInput(event: WheelEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (inputRef.current) {
|
||||
inputRef.current.addEventListener('wheel', preventWheelInNumberInput, {
|
||||
passive: false,
|
||||
signal: controller.signal,
|
||||
});
|
||||
}
|
||||
return () => controller.abort();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inputRef.current]);
|
||||
|
||||
async function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
||||
if (
|
||||
event.key === 'ArrowLeft' ||
|
||||
event.key === 'ArrowRight' ||
|
||||
event.key === 'ArrowUp' ||
|
||||
event.key === 'ArrowDown' ||
|
||||
event.key === 'Backspace'
|
||||
) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (isNotEmptyValue(onSave) && typeof temporaryValue !== 'undefined') {
|
||||
if (event.key === 'Tab') {
|
||||
await onSave?.(temporaryValue);
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
await onSave?.(temporaryValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
if (onTemporaryValueChange) {
|
||||
const newValue = isNotEmptyValue(event.target.value)
|
||||
? +event.target.value
|
||||
: null;
|
||||
onTemporaryValueChange(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
const step = dataType === 'integer' ? 1 : 0.1;
|
||||
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
ref={inputRef as Ref<HTMLInputElement>}
|
||||
value={
|
||||
temporaryValue !== null && typeof temporaryValue !== 'undefined'
|
||||
? temporaryValue
|
||||
: ''
|
||||
}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
wrapperClassName="absolute top-0 z-10 w-full top-0 left-0 h-full"
|
||||
className="h-full w-full resize-none rounded-none border-none px-2 py-1.5 !text-xs outline-none [appearance:textfield] focus-within:rounded-none focus-within:border-transparent focus-within:bg-white focus-within:shadow-[inset_0_0_0_1.5px_rgba(0,82,205,1)] focus:outline-none focus:ring-0 dark:focus-within:bg-theme-grey-200 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
step={step}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (optimisticValue === null || typeof optimisticValue === 'undefined') {
|
||||
return <p className="truncate !text-xs text-disabled">null</p>;
|
||||
}
|
||||
|
||||
return <p className="truncate !text-xs">{optimisticValue}</p>;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './DataGridNumericCell';
|
||||
export { default as DataGridNumericCell } from './DataGridNumericCell';
|
||||
@@ -1,13 +1,8 @@
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import type { IconButtonProps } from '@/components/ui/v2/IconButton';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { ChevronLeftIcon } from '@/components/ui/v2/icons/ChevronLeftIcon';
|
||||
import { ChevronRightIcon } from '@/components/ui/v2/icons/ChevronRightIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import clsx from 'clsx';
|
||||
import { Button, type ButtonProps } from '@/components/ui/v3/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
export interface DataGridPaginationProps extends BoxProps {
|
||||
export interface DataGridPaginationProps {
|
||||
/**
|
||||
* Number of pages.
|
||||
*/
|
||||
@@ -27,11 +22,12 @@ export interface DataGridPaginationProps extends BoxProps {
|
||||
/**
|
||||
* Props to be passed to the next button component.
|
||||
*/
|
||||
nextButtonProps?: IconButtonProps;
|
||||
nextButtonProps?: ButtonProps;
|
||||
/**
|
||||
* Props to be passed to the previous button component.
|
||||
*/
|
||||
prevButtonProps?: IconButtonProps;
|
||||
prevButtonProps?: ButtonProps;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function DataGridPagination({
|
||||
@@ -42,50 +38,48 @@ export default function DataGridPagination({
|
||||
onOpenNextPage,
|
||||
nextButtonProps,
|
||||
prevButtonProps,
|
||||
...props
|
||||
}: DataGridPaginationProps) {
|
||||
return (
|
||||
<Box
|
||||
className={clsx(
|
||||
'grid grid-flow-col items-center justify-around rounded-md border-1',
|
||||
<div
|
||||
className={cn(
|
||||
'grid grid-flow-col items-center justify-around rounded-md border-1 px-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === 1}
|
||||
onClick={onOpenPrevPage}
|
||||
aria-label="Previous page"
|
||||
className="h-max w-max border-none bg-transparent dark:hover:bg-[#2f363d]"
|
||||
{...prevButtonProps}
|
||||
>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<span
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'mx-1 inline-block font-display font-medium',
|
||||
currentPage > 99 ? 'text-xs' : 'text-sm+',
|
||||
)}
|
||||
>
|
||||
{currentPage}
|
||||
<Text component="span" className="mx-1 inline-block" color="disabled">
|
||||
/
|
||||
</Text>
|
||||
<span className="mx-1 inline-block text-disabled">/</span>
|
||||
{totalPages}
|
||||
</span>
|
||||
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={onOpenNextPage}
|
||||
aria-label="Next page"
|
||||
className="h-max w-max border-none bg-transparent dark:hover:bg-[#2f363d]"
|
||||
{...nextButtonProps}
|
||||
>
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { Modal } from '@/components/ui/v1/Modal';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { AudioPreviewIcon } from '@/components/ui/v2/icons/AudioPreviewIcon';
|
||||
import { FilePreviewIcon } from '@/components/ui/v2/icons/FilePreviewIcon';
|
||||
import { PDFPreviewIcon } from '@/components/ui/v2/icons/PDFPreviewIcon';
|
||||
import { VideoPreviewIcon } from '@/components/ui/v2/icons/VideoPreviewIcon';
|
||||
import { XIcon } from '@/components/ui/v2/icons/XIcon';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/v3/dialog';
|
||||
import { Spinner } from '@/components/ui/v3/spinner';
|
||||
import { useAppClient } from '@/features/orgs/projects/hooks/useAppClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { usePreviewToggle } from '@/features/orgs/projects/storage/dataGrid/hooks/usePreviewToggle';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import clsx from 'clsx';
|
||||
import { FileText } from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, useReducer, useState } from 'react';
|
||||
import type { CellProps } from 'react-table';
|
||||
@@ -246,124 +249,119 @@ export default function DataGridPreviewCell<TData extends object>({
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose(openState: boolean) {
|
||||
if (!openState) {
|
||||
setShowModal(false);
|
||||
dispatch({ type: 'CLEAR_PREVIEW' });
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <ActivityIndicator delay={500} className="mx-auto" />;
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<Spinner className="mx-auto h-4 w-4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box
|
||||
className="grid w-full grid-flow-col items-center justify-center gap-1 text-center"
|
||||
sx={{ color: 'error.main' }}
|
||||
>
|
||||
<FilePreviewIcon error /> Error
|
||||
</Box>
|
||||
<div className="box grid w-full grid-flow-col items-center justify-center gap-1 text-center !text-error-main">
|
||||
<FileText className="text-error-main" /> Error
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
wrapperClassName="items-center"
|
||||
showModal={showModal}
|
||||
close={() => setShowModal(false)}
|
||||
afterLeave={() => dispatch({ type: 'CLEAR_PREVIEW' })}
|
||||
className={clsx(
|
||||
previewableImages.includes(mimeType) || isVideo || isAudio
|
||||
? 'mx-12 flex h-screen items-center justify-center'
|
||||
: 'mt-4 inline-block h-near-screen w-full px-12',
|
||||
)}
|
||||
>
|
||||
<Box
|
||||
className={clsx(
|
||||
!isJson && 'bg-checker-pattern',
|
||||
'relative mx-auto flex overflow-hidden rounded-md',
|
||||
)}
|
||||
sx={{
|
||||
backgroundColor: isJson ? 'background.default' : undefined,
|
||||
color: 'text.primary',
|
||||
}}
|
||||
<Dialog open={showModal} onOpenChange={handleClose}>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={alt}
|
||||
onClick={handleOpenPreview}
|
||||
className={cn('flex h-full w-full items-center justify-center')}
|
||||
>
|
||||
{!previewLoading && (
|
||||
<IconButton
|
||||
aria-label="Close"
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="absolute right-2 top-2 z-50 p-2"
|
||||
sx={{
|
||||
[`&:hover, &:active, &:focus`]: {
|
||||
backgroundColor: (theme) => {
|
||||
if (isAudio || isVideo || isJson) {
|
||||
return 'common.black';
|
||||
}
|
||||
|
||||
return theme.palette.mode === 'dark'
|
||||
? 'grey.800'
|
||||
: 'grey.200';
|
||||
},
|
||||
},
|
||||
}}
|
||||
onClick={() => setShowModal(false)}
|
||||
>
|
||||
<XIcon
|
||||
className="h-5 w-5"
|
||||
sx={{
|
||||
color: (theme) => {
|
||||
if (isAudio || isVideo || isJson) {
|
||||
return 'common.white';
|
||||
}
|
||||
|
||||
return theme.palette.mode === 'dark'
|
||||
? 'grey.100'
|
||||
: 'grey.700';
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{previewLoading && !previewUrl && (
|
||||
<ActivityIndicator
|
||||
delay={500}
|
||||
className="mx-auto"
|
||||
label="Loading preview..."
|
||||
/>
|
||||
)}
|
||||
|
||||
{previewError && (
|
||||
<Box
|
||||
className="px-6 py-3.5 pr-12 text-start font-medium"
|
||||
sx={{ color: 'error.main' }}
|
||||
>
|
||||
<p>Error: Preview can't be loaded.</p>
|
||||
|
||||
<p>{previewError.message}</p>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{previewUrl && isImage && (
|
||||
<picture className="h-auto max-h-near-screen min-h-38 min-w-38">
|
||||
<source srcSet={previewUrl} type={mimeType} />
|
||||
{previewEnabled &&
|
||||
previewableImages.includes(mimeType) &&
|
||||
objectUrl ? (
|
||||
<picture className="h-full w-20">
|
||||
<source srcSet={objectUrl} type={mimeType} />
|
||||
<img
|
||||
src={previewUrl}
|
||||
src={objectUrl}
|
||||
alt={alt}
|
||||
className="h-full w-full object-scale-down"
|
||||
/>
|
||||
</picture>
|
||||
)}
|
||||
) : (
|
||||
<>
|
||||
{isVideo && <VideoPreviewIcon className="h-5 w-5" />}
|
||||
|
||||
{isAudio && <AudioPreviewIcon className="h-5 w-5" />}
|
||||
|
||||
{mimeType === 'application/pdf' && (
|
||||
<PDFPreviewIcon className="h-5 w-5" />
|
||||
)}
|
||||
|
||||
{!isVideo &&
|
||||
!isAudio &&
|
||||
mimeType !== 'application/pdf' &&
|
||||
fallbackPreview}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
closeButtonClassName={cn({ 'text-white': isVideo || isAudio })}
|
||||
className={cn(
|
||||
{ 'bg-checker-pattern': !isJson && !previewError },
|
||||
{ 'p-0': isVideo || isAudio },
|
||||
isAudio ? '!w-auto' : 'h-[90vh] min-w-[96vw]',
|
||||
'flex items-center justify-center overflow-hidden rounded-md',
|
||||
)}
|
||||
>
|
||||
<>
|
||||
<DialogTitle className="hidden">{alt}</DialogTitle>
|
||||
<DialogDescription className="hidden">{alt}</DialogDescription>
|
||||
{previewLoading && !previewUrl && (
|
||||
<Spinner
|
||||
className={cn('h-5 w-5', {
|
||||
'!stroke-[#1e324b]': !isJson,
|
||||
})}
|
||||
wrapperClassName={cn('flex-row gap-1 text-xs', {
|
||||
'text-disabled': isJson,
|
||||
'text-gray-600': !isJson,
|
||||
})}
|
||||
>
|
||||
Loading preview...
|
||||
</Spinner>
|
||||
)}
|
||||
{previewError && (
|
||||
<div className="px-6 py-3.5 pr-12 text-start font-medium !text-error-main">
|
||||
<p>Error: Preview can't be loaded.</p>
|
||||
|
||||
<p>{previewError?.message}</p>
|
||||
</div>
|
||||
)}
|
||||
{previewUrl && isImage && (
|
||||
<picture className="flex h-full max-h-full items-center justify-center">
|
||||
<source srcSet={previewUrl} type={mimeType} />
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt={alt}
|
||||
className="h-full max-w-full object-contain"
|
||||
/>
|
||||
</picture>
|
||||
)}
|
||||
{previewUrl && isVideo && (
|
||||
<video
|
||||
autoPlay
|
||||
controls
|
||||
className="h-auto max-h-near-screen w-full bg-black"
|
||||
className="h-full w-full rounded-sm bg-black"
|
||||
>
|
||||
<track kind="captions" />
|
||||
<source src={previewUrl} type={mimeType} />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
)}
|
||||
|
||||
{previewUrl && isAudio && (
|
||||
<audio autoPlay controls className="h-28 bg-black">
|
||||
<track kind="captions" />
|
||||
@@ -371,7 +369,6 @@ export default function DataGridPreviewCell<TData extends object>({
|
||||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
)}
|
||||
|
||||
{!previewLoading &&
|
||||
previewUrl &&
|
||||
!previewableImages.includes(mimeType) &&
|
||||
@@ -383,52 +380,8 @@ export default function DataGridPreviewCell<TData extends object>({
|
||||
title="File preview"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Modal>
|
||||
|
||||
<div className="flex h-full w-full justify-center">
|
||||
{previewEnabled && previewableImages.includes(mimeType) && objectUrl ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={alt}
|
||||
onClick={handleOpenPreview}
|
||||
className="mx-auto h-full"
|
||||
>
|
||||
<picture className="h-full w-20">
|
||||
<source srcSet={objectUrl} type={mimeType} />
|
||||
<img
|
||||
src={objectUrl}
|
||||
alt={alt}
|
||||
className="h-full w-full object-scale-down"
|
||||
/>
|
||||
</picture>
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{(!previewableImages.includes(mimeType) ||
|
||||
!objectUrl ||
|
||||
!previewEnabled) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenPreview}
|
||||
aria-label={alt}
|
||||
className="grid h-full w-full items-center justify-center self-center"
|
||||
>
|
||||
{isVideo && <VideoPreviewIcon className="h-5 w-5" />}
|
||||
|
||||
{isAudio && <AudioPreviewIcon className="h-5 w-5" />}
|
||||
|
||||
{mimeType === 'application/pdf' && (
|
||||
<PDFPreviewIcon className="h-5 w-5" />
|
||||
)}
|
||||
|
||||
{!isVideo &&
|
||||
!isAudio &&
|
||||
mimeType !== 'application/pdf' &&
|
||||
fallbackPreview}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { Input, inputClasses } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Input } from '@/components/ui/v3/input';
|
||||
import { Textarea } from '@/components/ui/v3/textarea';
|
||||
import {
|
||||
useDataGridCell,
|
||||
type CommonDataGridCellProps,
|
||||
} from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
|
||||
import { copy } from '@/utils/copy';
|
||||
|
||||
import type { ChangeEvent, KeyboardEvent, Ref } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
@@ -19,7 +17,7 @@ export default function DataGridTextCell<TData extends object>({
|
||||
temporaryValue,
|
||||
onTemporaryValueChange,
|
||||
cell: {
|
||||
column: { isCopiable, specificType },
|
||||
column: { specificType },
|
||||
},
|
||||
}: DataGridTextCellProps<TData>) {
|
||||
const isMultiline =
|
||||
@@ -74,8 +72,6 @@ export default function DataGridTextCell<TData extends object>({
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
await handleSave();
|
||||
await focusCell();
|
||||
cancelEditCell();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,35 +118,12 @@ export default function DataGridTextCell<TData extends object>({
|
||||
|
||||
if (isEditing && isMultiline) {
|
||||
return (
|
||||
<Input
|
||||
multiline
|
||||
ref={inputRef as Ref<HTMLInputElement>}
|
||||
<Textarea
|
||||
ref={inputRef as Ref<HTMLTextAreaElement>}
|
||||
value={(normalizedTemporaryValue || '').replace(/\\n/gi, `\n`)}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleTextAreaKeyDown}
|
||||
fullWidth
|
||||
className="absolute top-0 z-10 -mx-0.5 h-full min-h-38"
|
||||
rows={5}
|
||||
sx={{
|
||||
[`&.${inputClasses.focused}`]: {
|
||||
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
|
||||
borderColor: 'transparent !important',
|
||||
borderRadius: 0,
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? `${theme.palette.secondary[100]} !important`
|
||||
: `${theme.palette.common.white} !important`,
|
||||
},
|
||||
[`& .${inputClasses.input}`]: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
slotProps={{
|
||||
inputRoot: {
|
||||
className:
|
||||
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
|
||||
},
|
||||
}}
|
||||
className="absolute left-0 top-0 z-10 h-25 min-h-25 w-full resize-none rounded-none !text-xs outline-none focus-within:rounded-none focus-within:border-transparent focus-within:bg-white focus-within:shadow-[inset_0_0_0_1.5px_rgba(0,82,205,1)] focus:outline-none focus:ring-0 dark:focus-within:bg-theme-grey-200"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -162,84 +135,25 @@ export default function DataGridTextCell<TData extends object>({
|
||||
value={(normalizedTemporaryValue || '').replace(/\\n/gi, `\n`)}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
fullWidth
|
||||
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch"
|
||||
sx={{
|
||||
[`&.${inputClasses.focused}`]: {
|
||||
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
|
||||
borderColor: 'transparent !important',
|
||||
borderRadius: 0,
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? `${theme.palette.secondary[100]} !important`
|
||||
: `${theme.palette.common.white} !important`,
|
||||
},
|
||||
[`& .${inputClasses.input}`]: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
slotProps={{
|
||||
inputWrapper: { className: 'h-full' },
|
||||
input: { className: 'h-full' },
|
||||
inputRoot: {
|
||||
className:
|
||||
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
|
||||
},
|
||||
}}
|
||||
wrapperClassName="absolute top-0 z-10 w-full top-0 left-0 h-full"
|
||||
className="h-full w-full resize-none rounded-none border-none px-2 py-1.5 !text-xs outline-none focus-within:rounded-none focus-within:border-transparent focus-within:bg-white focus-within:shadow-[inset_0_0_0_1.5px_rgba(0,82,205,1)] focus:outline-none focus:ring-0 dark:focus-within:bg-theme-grey-200"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!optimisticValue) {
|
||||
return (
|
||||
<Text className="truncate !text-xs" color="secondary">
|
||||
<p className="truncate !text-xs text-[#7d8ca3]">
|
||||
{optimisticValue === '' ? 'empty' : 'null'}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (isCopiable) {
|
||||
return (
|
||||
<div className="grid grid-flow-col items-center justify-start gap-1">
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
const copiableValue =
|
||||
typeof optimisticValue === 'object'
|
||||
? JSON.stringify(optimisticValue)
|
||||
: String(optimisticValue).replace(/\\n/gi, '\n');
|
||||
|
||||
copy(copiableValue, 'Value');
|
||||
}}
|
||||
className="-ml-px min-w-0 p-0"
|
||||
aria-label="Copy value"
|
||||
sx={{
|
||||
color: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? 'text.secondary'
|
||||
: 'text.disabled',
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Text className="truncate text-xs">
|
||||
{typeof normalizedOptimisticValue === 'object'
|
||||
? JSON.stringify(normalizedOptimisticValue)
|
||||
: normalizedOptimisticValue}
|
||||
</Text>
|
||||
</div>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text className="truncate text-xs">
|
||||
<p className="truncate text-xs">
|
||||
{typeof normalizedOptimisticValue === 'object'
|
||||
? JSON.stringify(normalizedOptimisticValue)
|
||||
: normalizedOptimisticValue}
|
||||
</Text>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user