@aws/nx-plugin 0.70.0 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-THIRD-PARTY +96 -36
- package/package.json +7 -7
- package/src/infra/app/__snapshots__/generator.spec.ts.snap +2 -2
- package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +1 -1
- package/src/py/strands-agent/__snapshots__/generator.spec.ts.snap +3 -3
- package/src/ts/nx-plugin/__snapshots__/generator.spec.ts.snap +1 -1
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +4120 -4120
- package/src/ts/react-website/cognito-auth/__snapshots__/generator.spec.ts.snap +0 -592
- package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +259 -0
- package/src/ts/react-website/cognito-auth/__snapshots__/generator.ux-provider.spec.ts.snap +335 -0
- package/src/utils/files/common/shadcn/src/components/ui/breadcrumb.tsx.template +2 -2
- package/src/utils/files/common/shadcn/src/components/ui/button.tsx.template +4 -2
- package/src/utils/files/common/shadcn/src/components/ui/separator.tsx.template +1 -1
- package/src/utils/files/common/shadcn/src/components/ui/sheet.tsx.template +9 -5
- package/src/utils/files/common/shadcn/src/components/ui/sidebar.tsx.template +6 -6
- package/src/utils/files/common/shadcn/src/components/ui/tooltip.tsx.template +2 -6
- package/src/utils/shared-shadcn.js +1 -6
- package/src/utils/shared-shadcn.js.map +1 -1
- package/src/utils/versions.d.ts +21 -26
- package/src/utils/versions.js +20 -25
- package/src/utils/versions.js.map +1 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`cognito-auth generator terraform iacProvider > should generate terraform files for cognito auth and snapshot them > terraform-cognito-auth-files 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"add-callback-url.tf": "terraform {
|
|
6
|
+
required_providers {
|
|
7
|
+
aws = {
|
|
8
|
+
source = "hashicorp/aws"
|
|
9
|
+
version = "~> 6.0"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Variables
|
|
15
|
+
variable "callback_url" {
|
|
16
|
+
description = "Callback URL to add (e.g., https://d123456789.cloudfront.net)"
|
|
17
|
+
type = string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Read runtime config to get user pool client details
|
|
21
|
+
module "runtime_config_reader" {
|
|
22
|
+
source = "../../runtime-config/read"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Extract cognito details from runtime config
|
|
26
|
+
locals {
|
|
27
|
+
cognito_props = try(module.runtime_config_reader.config.cognitoProps, null)
|
|
28
|
+
|
|
29
|
+
user_pool_id = local.cognito_props != null ? local.cognito_props.userPoolId : null
|
|
30
|
+
user_pool_client_id = local.cognito_props != null ? local.cognito_props.userPoolWebClientId : null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Validation: Ensure cognito props exist in runtime config
|
|
34
|
+
resource "terraform_data" "validate_cognito_props" {
|
|
35
|
+
lifecycle {
|
|
36
|
+
precondition {
|
|
37
|
+
condition = local.cognito_props != null
|
|
38
|
+
error_message = "ERROR: cognitoProps not found in runtime config. Ensure user-identity module has been deployed first and has added cognitoProps to the runtime configuration."
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
precondition {
|
|
42
|
+
condition = local.user_pool_id != null && local.user_pool_id != ""
|
|
43
|
+
error_message = "ERROR: cognitoProps.userPoolId is missing or empty in runtime config. Check that user-identity module completed successfully."
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
precondition {
|
|
47
|
+
condition = local.user_pool_client_id != null && local.user_pool_client_id != ""
|
|
48
|
+
error_message = "ERROR: cognitoProps.userPoolWebClientId is missing or empty in runtime config. Check that user-identity module completed successfully."
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Update the user pool client with additional callback URL
|
|
55
|
+
resource "null_resource" "add_callback_url" {
|
|
56
|
+
triggers = {
|
|
57
|
+
callback_url = var.callback_url
|
|
58
|
+
user_pool_id = local.user_pool_id
|
|
59
|
+
user_pool_client_id = local.user_pool_client_id
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
provisioner "local-exec" {
|
|
63
|
+
command = <<-EOT
|
|
64
|
+
uv run --with boto3 python -c "
|
|
65
|
+
import boto3
|
|
66
|
+
import sys
|
|
67
|
+
|
|
68
|
+
# Configuration
|
|
69
|
+
user_pool_id = '\${local.user_pool_id}'
|
|
70
|
+
client_id = '\${local.user_pool_client_id}'
|
|
71
|
+
new_callback_url = '\${var.callback_url}'
|
|
72
|
+
|
|
73
|
+
# Initialize Cognito client
|
|
74
|
+
cognito = boto3.client('cognito-idp')
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# Get current user pool client configuration
|
|
78
|
+
response = cognito.describe_user_pool_client(
|
|
79
|
+
UserPoolId=user_pool_id,
|
|
80
|
+
ClientId=client_id
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
client_config = response['UserPoolClient']
|
|
84
|
+
current_callback_urls = client_config.get('CallbackURLs', [])
|
|
85
|
+
current_logout_urls = client_config.get('LogoutURLs', [])
|
|
86
|
+
|
|
87
|
+
# Check if URL already exists
|
|
88
|
+
if new_callback_url in current_callback_urls:
|
|
89
|
+
print(f'Callback URL {new_callback_url} already exists')
|
|
90
|
+
else:
|
|
91
|
+
# Add new URL to both callback and logout URLs
|
|
92
|
+
updated_callback_urls = current_callback_urls + [new_callback_url]
|
|
93
|
+
updated_logout_urls = current_logout_urls + [new_callback_url]
|
|
94
|
+
|
|
95
|
+
# Update the user pool client
|
|
96
|
+
# Only include valid update parameters (exclude read-only fields and ones we're setting)
|
|
97
|
+
valid_update_params = [
|
|
98
|
+
'ClientName', 'RefreshTokenValidity', 'AccessTokenValidity', 'IdTokenValidity',
|
|
99
|
+
'TokenValidityUnits', 'ReadAttributes', 'WriteAttributes', 'ExplicitAuthFlows',
|
|
100
|
+
'SupportedIdentityProviders', 'DefaultRedirectURI', 'AllowedOAuthFlows',
|
|
101
|
+
'AllowedOAuthScopes', 'AllowedOAuthFlowsUserPoolClient', 'AnalyticsConfiguration',
|
|
102
|
+
'PreventUserExistenceErrors', 'EnableTokenRevocation',
|
|
103
|
+
'EnablePropagateAdditionalUserContextData', 'AuthSessionValidity', 'RefreshTokenRotation'
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
update_config = {k: v for k, v in client_config.items() if k in valid_update_params}
|
|
107
|
+
|
|
108
|
+
cognito.update_user_pool_client(
|
|
109
|
+
UserPoolId=user_pool_id,
|
|
110
|
+
ClientId=client_id,
|
|
111
|
+
CallbackURLs=updated_callback_urls,
|
|
112
|
+
LogoutURLs=updated_logout_urls,
|
|
113
|
+
**update_config
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
print(f'Successfully added callback URL: {new_callback_url}')
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
print(f'Error updating callback URLs: {e}')
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
"
|
|
122
|
+
EOT
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
depends_on = [terraform_data.validate_cognito_props]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
",
|
|
129
|
+
"user-identity.tf": "module "identity" {
|
|
130
|
+
source = "./identity"
|
|
131
|
+
|
|
132
|
+
user_pool_domain_prefix = "test"
|
|
133
|
+
allow_signup = true
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Outputs
|
|
137
|
+
output "region" {
|
|
138
|
+
description = "AWS region"
|
|
139
|
+
value = module.identity.region
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
output "user_pool_id" {
|
|
143
|
+
description = "ID of the Cognito User Pool"
|
|
144
|
+
value = module.identity.user_pool_id
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
output "user_pool_arn" {
|
|
148
|
+
description = "ARN of the Cognito User Pool"
|
|
149
|
+
value = module.identity.user_pool_arn
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
output "user_pool_client_id" {
|
|
153
|
+
description = "ID of the Cognito User Pool Client"
|
|
154
|
+
value = module.identity.user_pool_client_id
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
output "identity_pool_id" {
|
|
158
|
+
description = "ID of the Cognito Identity Pool"
|
|
159
|
+
value = module.identity.identity_pool_id
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
output "authenticated_role_name" {
|
|
163
|
+
description = "Name of the authenticated IAM role"
|
|
164
|
+
value = module.identity.authenticated_role_name
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
output "authenticated_role_arn" {
|
|
168
|
+
description = "ARN of the authenticated IAM role"
|
|
169
|
+
value = module.identity.authenticated_role_arn
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
output "user_pool_domain" {
|
|
173
|
+
description = "Domain of the Cognito User Pool"
|
|
174
|
+
value = module.identity.user_pool_domain
|
|
175
|
+
}
|
|
176
|
+
",
|
|
177
|
+
}
|
|
178
|
+
`;
|
|
179
|
+
|
|
180
|
+
exports[`cognito-auth generator terraform iacProvider > should place add-callback-url module directly after cloudfront distribution resource > complex-static-website-with-callback-url 1`] = `
|
|
181
|
+
"
|
|
182
|
+
# Some initial resources
|
|
183
|
+
resource "aws_s3_bucket" "website" {
|
|
184
|
+
bucket = "test-bucket"
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
resource "aws_cloudfront_distribution" "website" {
|
|
188
|
+
origin {
|
|
189
|
+
domain_name = aws_s3_bucket.website.bucket_regional_domain_name
|
|
190
|
+
origin_id = "S3-\${aws_s3_bucket.website.id}"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
enabled = true
|
|
194
|
+
is_ipv6_enabled = true
|
|
195
|
+
default_root_object = "index.html"
|
|
196
|
+
|
|
197
|
+
default_cache_behavior {
|
|
198
|
+
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
|
|
199
|
+
cached_methods = ["GET", "HEAD"]
|
|
200
|
+
target_origin_id = "S3-\${aws_s3_bucket.website.id}"
|
|
201
|
+
compress = true
|
|
202
|
+
viewer_protocol_policy = "redirect-to-https"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
restrictions {
|
|
206
|
+
geo_restriction {
|
|
207
|
+
restriction_type = "none"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
viewer_certificate {
|
|
212
|
+
cloudfront_default_certificate = true
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Add CloudFront domain to user pool client callback URLs
|
|
217
|
+
module "add_callback_url" {
|
|
218
|
+
source = "../user-identity/add-callback-url"
|
|
219
|
+
|
|
220
|
+
callback_url = "https://\${aws_cloudfront_distribution.website.domain_name}"
|
|
221
|
+
|
|
222
|
+
depends_on = [aws_cloudfront_distribution.website]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# S3 Bucket Policy for CloudFront OAC
|
|
226
|
+
resource "aws_s3_bucket_policy" "website_cloudfront_policy" {
|
|
227
|
+
bucket = aws_s3_bucket.website.id
|
|
228
|
+
policy = jsonencode({
|
|
229
|
+
Version = "2012-10-17"
|
|
230
|
+
Statement = [
|
|
231
|
+
{
|
|
232
|
+
Effect = "Allow"
|
|
233
|
+
Principal = {
|
|
234
|
+
Service = "cloudfront.amazonaws.com"
|
|
235
|
+
}
|
|
236
|
+
Action = "s3:GetObject"
|
|
237
|
+
Resource = "\${aws_s3_bucket.website.arn}/*"
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# Some outputs
|
|
244
|
+
output "website_bucket_name" {
|
|
245
|
+
value = aws_s3_bucket.website.bucket
|
|
246
|
+
}
|
|
247
|
+
"
|
|
248
|
+
`;
|
|
249
|
+
|
|
250
|
+
exports[`cognito-auth generator terraform iacProvider > should update static-website.tf with add-callback-url module > static-website-with-callback-url 1`] = `
|
|
251
|
+
"
|
|
252
|
+
module "static_website" {
|
|
253
|
+
source = "../../../core/static-website"
|
|
254
|
+
|
|
255
|
+
website_name = "test-project"
|
|
256
|
+
build_path = "dist/packages/test-project"
|
|
257
|
+
}
|
|
258
|
+
"
|
|
259
|
+
`;
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`cognito-auth generator uxProvider tests > Cloudscape > should update AppLayout > app-layout-with-auth 1`] = `
|
|
4
|
+
"import { useAuth } from 'react-oidc-context';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { createContext, useCallback, useEffect, useState } from 'react';
|
|
7
|
+
import { NavItems } from './navitems';
|
|
8
|
+
import Config from '../../config';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
BreadcrumbGroup,
|
|
12
|
+
BreadcrumbGroupProps,
|
|
13
|
+
SideNavigation,
|
|
14
|
+
TopNavigation,
|
|
15
|
+
} from '@cloudscape-design/components';
|
|
16
|
+
|
|
17
|
+
import CloudscapeAppLayout, {
|
|
18
|
+
AppLayoutProps,
|
|
19
|
+
} from '@cloudscape-design/components/app-layout';
|
|
20
|
+
|
|
21
|
+
import { matchByPath, useLocation, useNavigate } from '@tanstack/react-router';
|
|
22
|
+
import { Outlet } from '@tanstack/react-router';
|
|
23
|
+
|
|
24
|
+
const getBreadcrumbs = (
|
|
25
|
+
pathName: string,
|
|
26
|
+
search: string,
|
|
27
|
+
defaultBreadcrumb: string,
|
|
28
|
+
availableRoutes?: string[],
|
|
29
|
+
) => {
|
|
30
|
+
const segments = [
|
|
31
|
+
defaultBreadcrumb,
|
|
32
|
+
...pathName.split('/').filter((segment) => segment !== ''),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
return segments.map((segment, i) => {
|
|
36
|
+
const href =
|
|
37
|
+
i === 0
|
|
38
|
+
? '/'
|
|
39
|
+
: \`/\${segments
|
|
40
|
+
.slice(1, i + 1)
|
|
41
|
+
.join('/')
|
|
42
|
+
.replace('//', '/')}\`;
|
|
43
|
+
|
|
44
|
+
const matched =
|
|
45
|
+
!availableRoutes || availableRoutes.find((r) => matchByPath(r, href, {}));
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
href: matched ? \`\${href}\${search}\` : '#',
|
|
49
|
+
text: segment,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export interface AppLayoutContext {
|
|
55
|
+
appLayoutProps: AppLayoutProps;
|
|
56
|
+
setAppLayoutProps: (props: AppLayoutProps) => void;
|
|
57
|
+
displayHelpPanel: (helpContent: React.ReactNode) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Context for updating/retrieving the AppLayout.
|
|
62
|
+
*/
|
|
63
|
+
export const AppLayoutContext = createContext({
|
|
64
|
+
appLayoutProps: {},
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
66
|
+
setAppLayoutProps: (_: AppLayoutProps) => {},
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
68
|
+
displayHelpPanel: (_: React.ReactNode) => {},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Defines the App layout and contains logic for routing.
|
|
73
|
+
*/
|
|
74
|
+
const AppLayout: React.FC = () => {
|
|
75
|
+
const { user, removeUser, signoutRedirect, clearStaleState } = useAuth();
|
|
76
|
+
const navigate = useNavigate();
|
|
77
|
+
const appLayout = React.useRef<AppLayoutProps.Ref>(null);
|
|
78
|
+
const [activeBreadcrumbs, setActiveBreadcrumbs] = useState<
|
|
79
|
+
BreadcrumbGroupProps.Item[]
|
|
80
|
+
>([{ text: '/', href: '/' }]);
|
|
81
|
+
const [appLayoutProps, setAppLayoutProps] = useState<AppLayoutProps>({});
|
|
82
|
+
const { pathname, search } = useLocation();
|
|
83
|
+
const setAppLayoutPropsSafe = useCallback(
|
|
84
|
+
(props: AppLayoutProps) => {
|
|
85
|
+
JSON.stringify(appLayoutProps) !== JSON.stringify(props) &&
|
|
86
|
+
setAppLayoutProps(props);
|
|
87
|
+
},
|
|
88
|
+
[appLayoutProps],
|
|
89
|
+
);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const breadcrumbs = getBreadcrumbs(
|
|
92
|
+
pathname,
|
|
93
|
+
Object.entries(search).reduce((p, [k, v]) => p + \`\${k}=\${v}\`, ''),
|
|
94
|
+
'/',
|
|
95
|
+
);
|
|
96
|
+
setActiveBreadcrumbs(breadcrumbs);
|
|
97
|
+
}, [pathname, search]);
|
|
98
|
+
const onNavigate = useCallback(
|
|
99
|
+
(
|
|
100
|
+
e: CustomEvent<{
|
|
101
|
+
href: string;
|
|
102
|
+
external?: boolean;
|
|
103
|
+
}>,
|
|
104
|
+
) => {
|
|
105
|
+
if (!e.detail.external) {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
setAppLayoutPropsSafe({
|
|
108
|
+
contentType: undefined,
|
|
109
|
+
});
|
|
110
|
+
navigate({ to: e.detail.href });
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
[navigate, setAppLayoutPropsSafe],
|
|
114
|
+
);
|
|
115
|
+
return (
|
|
116
|
+
<AppLayoutContext.Provider
|
|
117
|
+
value={{
|
|
118
|
+
appLayoutProps,
|
|
119
|
+
setAppLayoutProps: setAppLayoutPropsSafe,
|
|
120
|
+
displayHelpPanel: (helpContent: React.ReactNode) => {
|
|
121
|
+
setAppLayoutPropsSafe({ tools: helpContent, toolsHide: false });
|
|
122
|
+
appLayout.current?.openTools();
|
|
123
|
+
},
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
<TopNavigation
|
|
127
|
+
identity={{
|
|
128
|
+
href: '/',
|
|
129
|
+
title: Config.applicationName,
|
|
130
|
+
logo: {
|
|
131
|
+
src: Config.logo,
|
|
132
|
+
},
|
|
133
|
+
}}
|
|
134
|
+
utilities={[
|
|
135
|
+
{
|
|
136
|
+
type: 'menu-dropdown',
|
|
137
|
+
text: \`\${user?.profile?.['cognito:username']}\`,
|
|
138
|
+
iconName: 'user-profile-active',
|
|
139
|
+
onItemClick: (e) => {
|
|
140
|
+
if (e.detail.id === 'signout') {
|
|
141
|
+
removeUser();
|
|
142
|
+
signoutRedirect({
|
|
143
|
+
post_logout_redirect_uri: window.location.origin,
|
|
144
|
+
extraQueryParams: {
|
|
145
|
+
redirect_uri: window.location.origin,
|
|
146
|
+
response_type: 'code',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
clearStaleState();
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
items: [{ id: 'signout', text: 'Sign out' }],
|
|
153
|
+
},
|
|
154
|
+
]}
|
|
155
|
+
/>
|
|
156
|
+
<CloudscapeAppLayout
|
|
157
|
+
ref={appLayout}
|
|
158
|
+
breadcrumbs={
|
|
159
|
+
<BreadcrumbGroup onFollow={onNavigate} items={activeBreadcrumbs} />
|
|
160
|
+
}
|
|
161
|
+
toolsHide
|
|
162
|
+
navigation={
|
|
163
|
+
<SideNavigation
|
|
164
|
+
header={{ text: Config.applicationName, href: '/' }}
|
|
165
|
+
activeHref={pathname}
|
|
166
|
+
onFollow={onNavigate}
|
|
167
|
+
items={NavItems}
|
|
168
|
+
/>
|
|
169
|
+
}
|
|
170
|
+
content={<Outlet />}
|
|
171
|
+
{...appLayoutProps}
|
|
172
|
+
/>
|
|
173
|
+
</AppLayoutContext.Provider>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export default AppLayout;
|
|
178
|
+
"
|
|
179
|
+
`;
|
|
180
|
+
|
|
181
|
+
exports[`cognito-auth generator uxProvider tests > None > should update AppLayout > app-layout-with-auth 1`] = `
|
|
182
|
+
"import { useAuth } from 'react-oidc-context';
|
|
183
|
+
import * as React from 'react';
|
|
184
|
+
|
|
185
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
186
|
+
|
|
187
|
+
import Config from '../../config';
|
|
188
|
+
import { Link, useLocation, useMatchRoute } from '@tanstack/react-router';
|
|
189
|
+
|
|
190
|
+
const getBreadcrumbs = (
|
|
191
|
+
matchRoute: ReturnType<typeof useMatchRoute>,
|
|
192
|
+
pathName: string,
|
|
193
|
+
search: string,
|
|
194
|
+
defaultBreadcrumb: string,
|
|
195
|
+
availableRoutes?: string[],
|
|
196
|
+
) => {
|
|
197
|
+
const segments = [
|
|
198
|
+
defaultBreadcrumb,
|
|
199
|
+
...pathName.split('/').filter((segment) => segment !== ''),
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
return segments.map((segment, i) => {
|
|
203
|
+
const href =
|
|
204
|
+
i === 0
|
|
205
|
+
? '/'
|
|
206
|
+
: \`/\${segments
|
|
207
|
+
.slice(1, i + 1)
|
|
208
|
+
.join('/')
|
|
209
|
+
.replace('//', '/')}\`;
|
|
210
|
+
|
|
211
|
+
const matched =
|
|
212
|
+
!availableRoutes || availableRoutes.find((r) => matchRoute({ to: href }));
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
href: matched ? \`\${href}\${search}\` : '#',
|
|
216
|
+
text: segment,
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Defines the App layout and contains logic for routing.
|
|
223
|
+
*/
|
|
224
|
+
const AppLayout: React.FC<React.PropsWithChildren> = ({ children }) => {
|
|
225
|
+
const { user, removeUser, signoutRedirect, clearStaleState } = useAuth();
|
|
226
|
+
const { user, removeUser, signoutRedirect, clearStaleState } = useAuth();
|
|
227
|
+
const [activeBreadcrumbs, setActiveBreadcrumbs] = useState<
|
|
228
|
+
{
|
|
229
|
+
href: string;
|
|
230
|
+
text: string;
|
|
231
|
+
}[]
|
|
232
|
+
>([{ text: '/', href: '/' }]);
|
|
233
|
+
const matchRoute = useMatchRoute();
|
|
234
|
+
const { pathname, search } = useLocation();
|
|
235
|
+
const navItems = useMemo(() => [{ to: '/', label: 'Home' }], []);
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
const breadcrumbs = getBreadcrumbs(
|
|
238
|
+
matchRoute,
|
|
239
|
+
pathname,
|
|
240
|
+
Object.entries(search).reduce((p, [k, v]) => p + \`\${k}=\${v}\`, ''),
|
|
241
|
+
'Home',
|
|
242
|
+
);
|
|
243
|
+
setActiveBreadcrumbs(breadcrumbs);
|
|
244
|
+
}, [matchRoute, pathname, search]);
|
|
245
|
+
return (
|
|
246
|
+
<div className="app-shell">
|
|
247
|
+
<header className="app-header">
|
|
248
|
+
<div className="app-header-inner">
|
|
249
|
+
<div className="brand">
|
|
250
|
+
<a href="/">
|
|
251
|
+
<img
|
|
252
|
+
src={Config.logo}
|
|
253
|
+
alt={\`\${Config.applicationName} logo\`}
|
|
254
|
+
className="brand-logo"
|
|
255
|
+
/>
|
|
256
|
+
<span className="brand-name">{Config.applicationName}</span>
|
|
257
|
+
</a>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<nav className="app-nav">
|
|
261
|
+
{navItems.map((item) => (
|
|
262
|
+
<Link
|
|
263
|
+
key={item.to}
|
|
264
|
+
to={item.to}
|
|
265
|
+
className={pathname === item.to ? 'active' : undefined}
|
|
266
|
+
>
|
|
267
|
+
{item.label}
|
|
268
|
+
</Link>
|
|
269
|
+
))}
|
|
270
|
+
</nav>
|
|
271
|
+
<div className="user-greeting">
|
|
272
|
+
<span>Hi, {\`\${user?.profile?.['cognito:username']}\`}</span>
|
|
273
|
+
<button
|
|
274
|
+
type="button"
|
|
275
|
+
className="signout-link"
|
|
276
|
+
onClick={() => {
|
|
277
|
+
removeUser();
|
|
278
|
+
signoutRedirect({
|
|
279
|
+
post_logout_redirect_uri: window.location.origin,
|
|
280
|
+
extraQueryParams: {
|
|
281
|
+
redirect_uri: window.location.origin,
|
|
282
|
+
response_type: 'code',
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
clearStaleState();
|
|
286
|
+
}}
|
|
287
|
+
>
|
|
288
|
+
Sign out
|
|
289
|
+
</button>
|
|
290
|
+
</div>
|
|
291
|
+
<div className="user-greeting">
|
|
292
|
+
<span>Hi, {\`\${user?.profile?.['cognito:username']}\`}</span>
|
|
293
|
+
<button
|
|
294
|
+
type="button"
|
|
295
|
+
className="signout-link"
|
|
296
|
+
onClick={() => {
|
|
297
|
+
removeUser();
|
|
298
|
+
signoutRedirect({
|
|
299
|
+
post_logout_redirect_uri: window.location.origin,
|
|
300
|
+
extraQueryParams: {
|
|
301
|
+
redirect_uri: window.location.origin,
|
|
302
|
+
response_type: 'code',
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
clearStaleState();
|
|
306
|
+
}}
|
|
307
|
+
>
|
|
308
|
+
Sign out
|
|
309
|
+
</button>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
</header>
|
|
313
|
+
<main className="app-main">
|
|
314
|
+
<nav className="breadcrumbs" aria-label="Breadcrumb">
|
|
315
|
+
{activeBreadcrumbs.map((crumb, index) => (
|
|
316
|
+
<span className="breadcrumb-segment" key={crumb.href || index}>
|
|
317
|
+
{index > 0 && <span className="breadcrumb-separator">/</span>}
|
|
318
|
+
{index === activeBreadcrumbs.length - 1 ? (
|
|
319
|
+
<span className="breadcrumb-current">{crumb.text}</span>
|
|
320
|
+
) : (
|
|
321
|
+
<Link to={crumb.href}>{crumb.text}</Link>
|
|
322
|
+
)}
|
|
323
|
+
</span>
|
|
324
|
+
))}
|
|
325
|
+
</nav>
|
|
326
|
+
|
|
327
|
+
<section className="card">{children}</section>
|
|
328
|
+
</main>
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
export default AppLayout;
|
|
334
|
+
"
|
|
335
|
+
`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
-
import { Slot } from "@radix-ui/react-slot"
|
|
3
2
|
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
|
3
|
+
import { Slot } from "radix-ui"
|
|
4
4
|
|
|
5
5
|
import { cn } from "<%= scopeAlias %>common-shadcn/lib/utils"
|
|
6
6
|
|
|
@@ -38,7 +38,7 @@ function BreadcrumbLink({
|
|
|
38
38
|
}: React.ComponentProps<"a"> & {
|
|
39
39
|
asChild?: boolean
|
|
40
40
|
}) {
|
|
41
|
-
const Comp = asChild ? Slot : "a"
|
|
41
|
+
const Comp = asChild ? Slot.Root : "a"
|
|
42
42
|
|
|
43
43
|
return (
|
|
44
44
|
<Comp
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
-
import { Slot } from "@radix-ui/react-slot"
|
|
3
2
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { Slot } from "radix-ui"
|
|
4
4
|
|
|
5
5
|
import { cn } from "<%= scopeAlias %>common-shadcn/lib/utils"
|
|
6
6
|
|
|
@@ -22,9 +22,11 @@ const buttonVariants = cva(
|
|
|
22
22
|
},
|
|
23
23
|
size: {
|
|
24
24
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
25
|
+
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
25
26
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
26
27
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
27
28
|
icon: "size-9",
|
|
29
|
+
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
|
28
30
|
"icon-sm": "size-8",
|
|
29
31
|
"icon-lg": "size-10",
|
|
30
32
|
},
|
|
@@ -46,7 +48,7 @@ function Button({
|
|
|
46
48
|
VariantProps<typeof buttonVariants> & {
|
|
47
49
|
asChild?: boolean
|
|
48
50
|
}) {
|
|
49
|
-
const Comp = asChild ? Slot : "button"
|
|
51
|
+
const Comp = asChild ? Slot.Root : "button"
|
|
50
52
|
|
|
51
53
|
return (
|
|
52
54
|
<Comp
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
|
-
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
|
5
4
|
import { XIcon } from "lucide-react"
|
|
5
|
+
import { Dialog as SheetPrimitive } from "radix-ui"
|
|
6
6
|
|
|
7
7
|
import { cn } from "<%= scopeAlias %>common-shadcn/lib/utils"
|
|
8
8
|
|
|
@@ -48,9 +48,11 @@ function SheetContent({
|
|
|
48
48
|
className,
|
|
49
49
|
children,
|
|
50
50
|
side = "right",
|
|
51
|
+
showCloseButton = true,
|
|
51
52
|
...props
|
|
52
53
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
|
53
54
|
side?: "top" | "right" | "bottom" | "left"
|
|
55
|
+
showCloseButton?: boolean
|
|
54
56
|
}) {
|
|
55
57
|
return (
|
|
56
58
|
<SheetPortal>
|
|
@@ -72,10 +74,12 @@ function SheetContent({
|
|
|
72
74
|
{...props}
|
|
73
75
|
>
|
|
74
76
|
{children}
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
{showCloseButton && (
|
|
78
|
+
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
|
79
|
+
<XIcon className="size-4" />
|
|
80
|
+
<span className="sr-only">Close</span>
|
|
81
|
+
</SheetPrimitive.Close>
|
|
82
|
+
)}
|
|
79
83
|
</SheetPrimitive.Content>
|
|
80
84
|
</SheetPortal>
|
|
81
85
|
)
|