From 22c733d8daa72edb6cd7f7cf926e589ae869a5bd Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Thu, 28 Nov 2024 07:18:53 +0100 Subject: [PATCH 001/170] kaas --- infrastructure/app.bicep | 164 +++++++++++++-------------- infrastructure/bicepconfig.json | 7 ++ infrastructure/main.bicep | 21 ++-- infrastructure/monitoring.bicep | 18 ++- infrastructure/params/prd.bicepparam | 1 - infrastructure/registry.bicep | 35 +++--- infrastructure/types.bicep | 12 -- 7 files changed, 133 insertions(+), 125 deletions(-) delete mode 100644 infrastructure/types.bicep diff --git a/infrastructure/app.bicep b/infrastructure/app.bicep index 714568a..7510894 100644 --- a/infrastructure/app.bicep +++ b/infrastructure/app.bicep @@ -1,4 +1,6 @@ -import { Context } from 'types.bicep' +import { Context } from 'br/Tricep:types:latest' +import { with_name } from 'br/Tricep:common/context:latest' +import { container_app_environment, container_app, container, with_app_logs, with_auto_scaling, with_environment } from 'br/Tricep:recommended/app/container-app:latest' targetScope = 'resourceGroup' @@ -6,96 +8,92 @@ param context Context param version string @secure() param registryUrl string +param customerId string +param sharedKey string var appName = 'app' -resource environment 'Microsoft.App/managedEnvironments@2024-03-01' = { - name: 'cea-${context.locationAbbreviation}-${context.environment}-${context.projectName}' - location: context.location - properties: { - appLogsConfiguration: { - destination: 'azure-monitor' - } - peerAuthentication: { - mtls: { - enabled: false +var environmentConfig = container_app_environment(with_name(context, 'app'), [ + with_app_logs(customerId, sharedKey) + { + properties: { + appLogsConfiguration: { + destination: 'azure-monitor' } - } - peerTrafficConfiguration: { - encryption: { - enabled: false + peerAuthentication: { + mtls: { + enabled: false + } + } + peerTrafficConfiguration: { + encryption: { + enabled: false + } } } } +]) +var appConfig = container_app( + with_name(context, 'app'), + [ + container('${context.project}-${appName}', '${registryUrl}/${context.project}-${appName}:${version}') + ], + [ + with_environment(environment.id) + with_auto_scaling(0, 1, { + ruleName: { + concurrentRequests: '10' + } + }) + { + properties: { + configuration: { + activeRevisionsMode: 'Single' + + ingress: { + external: true + targetPort: 3000 + transport: 'auto' + allowInsecure: false + traffic: [ + { + weight: 100 + latestRevision: true + } + ] + corsPolicy: { + allowedOrigins: [ + // 'https://localhost:3000' + '*' + ] + allowCredentials: true + allowedHeaders: ['*'] + allowedMethods: ['Get, POST'] + maxAge: 0 + } + } + + registries: [ + { + identity: 'system' + server: registryUrl + } + ] + } + } + } + ] +) + +resource environment 'Microsoft.App/managedEnvironments@2024-03-01' = { + name: environmentConfig.name + location: environmentConfig.location + properties: environmentConfig.properties } resource app 'Microsoft.App/containerApps@2024-03-01' = { - name: 'ca-${context.locationAbbreviation}-${context.environment}-${context.projectName}-app' - location: context.location - identity: { - type: 'SystemAssigned' - } - properties: { - environmentId: environment.id - - configuration: { - activeRevisionsMode: 'Single' - - ingress: { - external: true - targetPort: 3000 - transport: 'auto' - allowInsecure: false - traffic: [ - { - weight: 100 - latestRevision: true - } - ] - corsPolicy: { - allowedOrigins: [ - // 'https://localhost:3000' - '*' - ] - allowCredentials: true - allowedHeaders: ['*'] - allowedMethods: ['Get, POST'] - maxAge: 0 - } - } - registries: [ - { - identity: 'system' - server: registryUrl - } - ] - } - - template: { - containers: [ - { - image: '${registryUrl}/${context.projectName}-${appName}:${version}' - name: '${context.projectName}-${appName}' - resources: { - cpu: json('0.25') - memory: '0.5Gi' - } - } - ] - scale: { - minReplicas: 1 - maxReplicas: 2 - rules: [ - { - name: 'http-rule' - http: { - metadata: { - concurrentRequests: '50' - } - } - } - ] - } - } - } + name: appConfig.name + location: appConfig.location + identity: appConfig.identity + properties: appConfig.properties } diff --git a/infrastructure/bicepconfig.json b/infrastructure/bicepconfig.json index 09945bd..622d0de 100644 --- a/infrastructure/bicepconfig.json +++ b/infrastructure/bicepconfig.json @@ -7,5 +7,12 @@ "resourceTypedParamsAndOutputs": true, "sourceMapping": true, "symbolicNameCodegen": true + }, + "moduleAliases": { + "br": { + "Tricep": { + "registry": "acreuwprdtricep.azurecr.io" + } + } } } \ No newline at end of file diff --git a/infrastructure/main.bicep b/infrastructure/main.bicep index 6e207c5..5130d1b 100644 --- a/infrastructure/main.bicep +++ b/infrastructure/main.bicep @@ -1,8 +1,8 @@ -import { Context } from 'types.bicep' +import { create_context } from 'br/Tricep:common/context:latest' +import { resource_group } from 'br/Tricep:recommended/resources/resource-group:latest' targetScope = 'subscription' -param locationAbbreviation string param location string param environment string param projectName string @@ -11,17 +11,22 @@ param version string param registryUrl string param deployedAt string = utcNow('yyyyMMdd') -var context = { - locationAbbreviation: locationAbbreviation +var context = create_context({ + name: '' + project: projectName + nameConventionTemplate: '$type-$env-$loc-$project-$name' location: location environment: environment - projectName: projectName deployedAt: deployedAt -} + tenant: tenant() + tags: {} +}) + +var resourceGroupConfig = resource_group(context, []) resource calqueResourceGroup 'Microsoft.Resources/resourceGroups@2024-07-01' = { - name: 'rg-${locationAbbreviation}-${environment}-${projectName}' - location: location + name: resourceGroupConfig.name + location: resourceGroupConfig.location } module monitoring 'monitoring.bicep' = { diff --git a/infrastructure/monitoring.bicep b/infrastructure/monitoring.bicep index f057cd0..9740965 100644 --- a/infrastructure/monitoring.bicep +++ b/infrastructure/monitoring.bicep @@ -1,11 +1,17 @@ -import { Context } from 'types.bicep' +import { Context } from 'br/Tricep:types:latest' +import { with_managed_identity } from 'br/Tricep:common/identity:latest' +import { log_analytics } from 'br/Tricep:recommended/operational-insights/log-analytics:latest' targetScope = 'resourceGroup' param context Context -// resource monitoring 'Microsoft.___/___@___' = { -// name: '___-${context.locationAbbreviation}-${context.environment}-${context.projectName}' -// location: context.location -// properties: {} -// } +var logAnalyticsConfig = log_analytics(context, [ + with_managed_identity() +]) + +resource monitoring 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: logAnalyticsConfig.name + location: logAnalyticsConfig.location + properties: logAnalyticsConfig.properties +} diff --git a/infrastructure/params/prd.bicepparam b/infrastructure/params/prd.bicepparam index 5d93d34..06404bf 100644 --- a/infrastructure/params/prd.bicepparam +++ b/infrastructure/params/prd.bicepparam @@ -1,6 +1,5 @@ using '../main.bicep' -param locationAbbreviation = 'euw' param location = 'westeurope' param environment = 'prd' param projectName = 'calque' diff --git a/infrastructure/registry.bicep b/infrastructure/registry.bicep index a13abbf..dddded3 100644 --- a/infrastructure/registry.bicep +++ b/infrastructure/registry.bicep @@ -1,23 +1,28 @@ -import { Context } from 'types.bicep' +import { Context } from 'br/Tricep:types:latest' +import { with_managed_identity } from 'br/Tricep:common/identity:latest' +import { container_registry } from 'br/Tricep:recommended/container-registry/container-registry:latest' targetScope = 'resourceGroup' param context Context -resource registry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { - name: 'acr${context.locationAbbreviation}${context.environment}${context.projectName}' - location: context.location - sku: { - name: 'Basic' - } - identity: { - type: 'SystemAssigned' - } - properties: { - adminUserEnabled: true - dataEndpointEnabled: false - encryption: { - status: 'disabled' +var registryConfig = container_registry(context, [ + with_managed_identity() + { + properties: { + adminUserEnabled: true + dataEndpointEnabled: false + encryption: { + status: 'disabled' + } } } +]) + +resource registry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { + name: registryConfig.name + location: registryConfig.location + sku: registryConfig.sku + identity: registryConfig.identity + properties: registryConfig.properties } diff --git a/infrastructure/types.bicep b/infrastructure/types.bicep deleted file mode 100644 index 768ff80..0000000 --- a/infrastructure/types.bicep +++ /dev/null @@ -1,12 +0,0 @@ -@export() -type Context = { - @minLength(2) - locationAbbreviation: string - @minLength(2) - location: string - @minLength(3) - environment: string - @minLength(2) - projectName: string - deployedAt: string -} From e917ab12ed1afd24f00009a8f778dfb002f2b611 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Thu, 28 Nov 2024 07:18:53 +0100 Subject: [PATCH 002/170] kaas --- infrastructure/app.bicep | 164 +++++++++++++-------------- infrastructure/bicepconfig.json | 7 ++ infrastructure/main.bicep | 21 ++-- infrastructure/monitoring.bicep | 18 ++- infrastructure/params/prd.bicepparam | 1 - infrastructure/registry.bicep | 35 +++--- infrastructure/types.bicep | 12 -- 7 files changed, 133 insertions(+), 125 deletions(-) delete mode 100644 infrastructure/types.bicep diff --git a/infrastructure/app.bicep b/infrastructure/app.bicep index 714568a..7510894 100644 --- a/infrastructure/app.bicep +++ b/infrastructure/app.bicep @@ -1,4 +1,6 @@ -import { Context } from 'types.bicep' +import { Context } from 'br/Tricep:types:latest' +import { with_name } from 'br/Tricep:common/context:latest' +import { container_app_environment, container_app, container, with_app_logs, with_auto_scaling, with_environment } from 'br/Tricep:recommended/app/container-app:latest' targetScope = 'resourceGroup' @@ -6,96 +8,92 @@ param context Context param version string @secure() param registryUrl string +param customerId string +param sharedKey string var appName = 'app' -resource environment 'Microsoft.App/managedEnvironments@2024-03-01' = { - name: 'cea-${context.locationAbbreviation}-${context.environment}-${context.projectName}' - location: context.location - properties: { - appLogsConfiguration: { - destination: 'azure-monitor' - } - peerAuthentication: { - mtls: { - enabled: false +var environmentConfig = container_app_environment(with_name(context, 'app'), [ + with_app_logs(customerId, sharedKey) + { + properties: { + appLogsConfiguration: { + destination: 'azure-monitor' } - } - peerTrafficConfiguration: { - encryption: { - enabled: false + peerAuthentication: { + mtls: { + enabled: false + } + } + peerTrafficConfiguration: { + encryption: { + enabled: false + } } } } +]) +var appConfig = container_app( + with_name(context, 'app'), + [ + container('${context.project}-${appName}', '${registryUrl}/${context.project}-${appName}:${version}') + ], + [ + with_environment(environment.id) + with_auto_scaling(0, 1, { + ruleName: { + concurrentRequests: '10' + } + }) + { + properties: { + configuration: { + activeRevisionsMode: 'Single' + + ingress: { + external: true + targetPort: 3000 + transport: 'auto' + allowInsecure: false + traffic: [ + { + weight: 100 + latestRevision: true + } + ] + corsPolicy: { + allowedOrigins: [ + // 'https://localhost:3000' + '*' + ] + allowCredentials: true + allowedHeaders: ['*'] + allowedMethods: ['Get, POST'] + maxAge: 0 + } + } + + registries: [ + { + identity: 'system' + server: registryUrl + } + ] + } + } + } + ] +) + +resource environment 'Microsoft.App/managedEnvironments@2024-03-01' = { + name: environmentConfig.name + location: environmentConfig.location + properties: environmentConfig.properties } resource app 'Microsoft.App/containerApps@2024-03-01' = { - name: 'ca-${context.locationAbbreviation}-${context.environment}-${context.projectName}-app' - location: context.location - identity: { - type: 'SystemAssigned' - } - properties: { - environmentId: environment.id - - configuration: { - activeRevisionsMode: 'Single' - - ingress: { - external: true - targetPort: 3000 - transport: 'auto' - allowInsecure: false - traffic: [ - { - weight: 100 - latestRevision: true - } - ] - corsPolicy: { - allowedOrigins: [ - // 'https://localhost:3000' - '*' - ] - allowCredentials: true - allowedHeaders: ['*'] - allowedMethods: ['Get, POST'] - maxAge: 0 - } - } - registries: [ - { - identity: 'system' - server: registryUrl - } - ] - } - - template: { - containers: [ - { - image: '${registryUrl}/${context.projectName}-${appName}:${version}' - name: '${context.projectName}-${appName}' - resources: { - cpu: json('0.25') - memory: '0.5Gi' - } - } - ] - scale: { - minReplicas: 1 - maxReplicas: 2 - rules: [ - { - name: 'http-rule' - http: { - metadata: { - concurrentRequests: '50' - } - } - } - ] - } - } - } + name: appConfig.name + location: appConfig.location + identity: appConfig.identity + properties: appConfig.properties } diff --git a/infrastructure/bicepconfig.json b/infrastructure/bicepconfig.json index 09945bd..622d0de 100644 --- a/infrastructure/bicepconfig.json +++ b/infrastructure/bicepconfig.json @@ -7,5 +7,12 @@ "resourceTypedParamsAndOutputs": true, "sourceMapping": true, "symbolicNameCodegen": true + }, + "moduleAliases": { + "br": { + "Tricep": { + "registry": "acreuwprdtricep.azurecr.io" + } + } } } \ No newline at end of file diff --git a/infrastructure/main.bicep b/infrastructure/main.bicep index 6e207c5..5130d1b 100644 --- a/infrastructure/main.bicep +++ b/infrastructure/main.bicep @@ -1,8 +1,8 @@ -import { Context } from 'types.bicep' +import { create_context } from 'br/Tricep:common/context:latest' +import { resource_group } from 'br/Tricep:recommended/resources/resource-group:latest' targetScope = 'subscription' -param locationAbbreviation string param location string param environment string param projectName string @@ -11,17 +11,22 @@ param version string param registryUrl string param deployedAt string = utcNow('yyyyMMdd') -var context = { - locationAbbreviation: locationAbbreviation +var context = create_context({ + name: '' + project: projectName + nameConventionTemplate: '$type-$env-$loc-$project-$name' location: location environment: environment - projectName: projectName deployedAt: deployedAt -} + tenant: tenant() + tags: {} +}) + +var resourceGroupConfig = resource_group(context, []) resource calqueResourceGroup 'Microsoft.Resources/resourceGroups@2024-07-01' = { - name: 'rg-${locationAbbreviation}-${environment}-${projectName}' - location: location + name: resourceGroupConfig.name + location: resourceGroupConfig.location } module monitoring 'monitoring.bicep' = { diff --git a/infrastructure/monitoring.bicep b/infrastructure/monitoring.bicep index f057cd0..9740965 100644 --- a/infrastructure/monitoring.bicep +++ b/infrastructure/monitoring.bicep @@ -1,11 +1,17 @@ -import { Context } from 'types.bicep' +import { Context } from 'br/Tricep:types:latest' +import { with_managed_identity } from 'br/Tricep:common/identity:latest' +import { log_analytics } from 'br/Tricep:recommended/operational-insights/log-analytics:latest' targetScope = 'resourceGroup' param context Context -// resource monitoring 'Microsoft.___/___@___' = { -// name: '___-${context.locationAbbreviation}-${context.environment}-${context.projectName}' -// location: context.location -// properties: {} -// } +var logAnalyticsConfig = log_analytics(context, [ + with_managed_identity() +]) + +resource monitoring 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: logAnalyticsConfig.name + location: logAnalyticsConfig.location + properties: logAnalyticsConfig.properties +} diff --git a/infrastructure/params/prd.bicepparam b/infrastructure/params/prd.bicepparam index 5d93d34..06404bf 100644 --- a/infrastructure/params/prd.bicepparam +++ b/infrastructure/params/prd.bicepparam @@ -1,6 +1,5 @@ using '../main.bicep' -param locationAbbreviation = 'euw' param location = 'westeurope' param environment = 'prd' param projectName = 'calque' diff --git a/infrastructure/registry.bicep b/infrastructure/registry.bicep index a13abbf..dddded3 100644 --- a/infrastructure/registry.bicep +++ b/infrastructure/registry.bicep @@ -1,23 +1,28 @@ -import { Context } from 'types.bicep' +import { Context } from 'br/Tricep:types:latest' +import { with_managed_identity } from 'br/Tricep:common/identity:latest' +import { container_registry } from 'br/Tricep:recommended/container-registry/container-registry:latest' targetScope = 'resourceGroup' param context Context -resource registry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { - name: 'acr${context.locationAbbreviation}${context.environment}${context.projectName}' - location: context.location - sku: { - name: 'Basic' - } - identity: { - type: 'SystemAssigned' - } - properties: { - adminUserEnabled: true - dataEndpointEnabled: false - encryption: { - status: 'disabled' +var registryConfig = container_registry(context, [ + with_managed_identity() + { + properties: { + adminUserEnabled: true + dataEndpointEnabled: false + encryption: { + status: 'disabled' + } } } +]) + +resource registry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { + name: registryConfig.name + location: registryConfig.location + sku: registryConfig.sku + identity: registryConfig.identity + properties: registryConfig.properties } diff --git a/infrastructure/types.bicep b/infrastructure/types.bicep deleted file mode 100644 index 768ff80..0000000 --- a/infrastructure/types.bicep +++ /dev/null @@ -1,12 +0,0 @@ -@export() -type Context = { - @minLength(2) - locationAbbreviation: string - @minLength(2) - location: string - @minLength(3) - environment: string - @minLength(2) - projectName: string - deployedAt: string -} From 8faa5c7d551f7d7fcc270b0d0340a4361b81657d Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Thu, 28 Nov 2024 10:40:39 +0100 Subject: [PATCH 003/170] getting an exception locally. seems there is a bug in the tricep output??? --- .github/workflows/app.yml | 3 ++ infrastructure/app.bicep | 82 +++++++++++++-------------------- infrastructure/bicepconfig.json | 8 +--- infrastructure/main.bicep | 3 +- infrastructure/monitoring.bicep | 1 + infrastructure/registry.bicep | 1 + 6 files changed, 39 insertions(+), 59 deletions(-) diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml index dbbd9e1..9971ab8 100644 --- a/.github/workflows/app.yml +++ b/.github/workflows/app.yml @@ -76,6 +76,9 @@ jobs: sparse-checkout: | infrastructure + - name: Test bicep + run: az bicep build ./infrastructure/main.bicep + - name: Az CLI login uses: azure/login@v2 with: diff --git a/infrastructure/app.bicep b/infrastructure/app.bicep index 7510894..1e4ca7c 100644 --- a/infrastructure/app.bicep +++ b/infrastructure/app.bicep @@ -1,6 +1,15 @@ import { Context } from 'br/Tricep:types:latest' import { with_name } from 'br/Tricep:common/context:latest' -import { container_app_environment, container_app, container, with_app_logs, with_auto_scaling, with_environment } from 'br/Tricep:recommended/app/container-app:latest' +import { with_managed_identity } from 'br/Tricep:common/identity:latest' +import { + container_app_environment + container_app + container + with_public_access + with_app_logs + with_auto_scaling + with_environment +} from 'br/Tricep:recommended/app/container-app:latest' targetScope = 'resourceGroup' @@ -8,71 +17,42 @@ param context Context param version string @secure() param registryUrl string -param customerId string -param sharedKey string var appName = 'app' -var environmentConfig = container_app_environment(with_name(context, 'app'), [ - with_app_logs(customerId, sharedKey) - { - properties: { - appLogsConfiguration: { - destination: 'azure-monitor' - } - peerAuthentication: { - mtls: { - enabled: false - } - } - peerTrafficConfiguration: { - encryption: { - enabled: false - } - } - } - } -]) +var environmentConfig = container_app_environment(with_name(context, appName), []) var appConfig = container_app( - with_name(context, 'app'), + context, [ - container('${context.project}-${appName}', '${registryUrl}/${context.project}-${appName}:${version}') + container({ + name: '${context.project}-${appName}' + image: '${registryUrl}/${context.project}-${appName}:${version}' + }) ], [ + with_managed_identity() with_environment(environment.id) with_auto_scaling(0, 1, { ruleName: { concurrentRequests: '10' } }) + with_public_access({ + port: 3000 + cors: { + allowedOrigins: [ + // 'https://localhost:3000' + '*' + ] + allowCredentials: true + allowedHeaders: ['*'] + allowedMethods: ['Get, POST'] + maxAge: 0 + } + }) { properties: { configuration: { - activeRevisionsMode: 'Single' - - ingress: { - external: true - targetPort: 3000 - transport: 'auto' - allowInsecure: false - traffic: [ - { - weight: 100 - latestRevision: true - } - ] - corsPolicy: { - allowedOrigins: [ - // 'https://localhost:3000' - '*' - ] - allowCredentials: true - allowedHeaders: ['*'] - allowedMethods: ['Get, POST'] - maxAge: 0 - } - } - registries: [ { identity: 'system' @@ -88,12 +68,14 @@ var appConfig = container_app( resource environment 'Microsoft.App/managedEnvironments@2024-03-01' = { name: environmentConfig.name location: environmentConfig.location + tags: environmentConfig.tags properties: environmentConfig.properties } resource app 'Microsoft.App/containerApps@2024-03-01' = { name: appConfig.name location: appConfig.location + tags: appConfig.tags identity: appConfig.identity properties: appConfig.properties } diff --git a/infrastructure/bicepconfig.json b/infrastructure/bicepconfig.json index 622d0de..dc847c0 100644 --- a/infrastructure/bicepconfig.json +++ b/infrastructure/bicepconfig.json @@ -1,12 +1,6 @@ { "experimentalFeaturesEnabled": { - "assertions": true, - "testFramework": true, - "extensibility": true, - "resourceDerivedTypes": true, - "resourceTypedParamsAndOutputs": true, - "sourceMapping": true, - "symbolicNameCodegen": true + "resourceTypedParamsAndOutputs": true }, "moduleAliases": { "br": { diff --git a/infrastructure/main.bicep b/infrastructure/main.bicep index 5130d1b..31d0d79 100644 --- a/infrastructure/main.bicep +++ b/infrastructure/main.bicep @@ -12,9 +12,8 @@ param registryUrl string param deployedAt string = utcNow('yyyyMMdd') var context = create_context({ - name: '' project: projectName - nameConventionTemplate: '$type-$env-$loc-$project-$name' + nameConventionTemplate: '$type-$environment-$location-$project' location: location environment: environment deployedAt: deployedAt diff --git a/infrastructure/monitoring.bicep b/infrastructure/monitoring.bicep index 9740965..202f16c 100644 --- a/infrastructure/monitoring.bicep +++ b/infrastructure/monitoring.bicep @@ -13,5 +13,6 @@ var logAnalyticsConfig = log_analytics(context, [ resource monitoring 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { name: logAnalyticsConfig.name location: logAnalyticsConfig.location + tags: logAnalyticsConfig.tags properties: logAnalyticsConfig.properties } diff --git a/infrastructure/registry.bicep b/infrastructure/registry.bicep index dddded3..1b45c5d 100644 --- a/infrastructure/registry.bicep +++ b/infrastructure/registry.bicep @@ -22,6 +22,7 @@ var registryConfig = container_registry(context, [ resource registry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { name: registryConfig.name location: registryConfig.location + tags: registryConfig.tags sku: registryConfig.sku identity: registryConfig.identity properties: registryConfig.properties From 687f1e0a440f01cf7fdaaa9d0b339e88156f80a6 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Thu, 28 Nov 2024 10:48:14 +0100 Subject: [PATCH 004/170] right --- .github/workflows/app.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml index 9971ab8..fa0a6e7 100644 --- a/.github/workflows/app.yml +++ b/.github/workflows/app.yml @@ -1,6 +1,7 @@ name: Deploy App on: + workflow_dispatch: push: branches: - main From 569e7a4cef15e2dc587037c0f9e71a3e491f6531 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Thu, 28 Nov 2024 10:53:37 +0100 Subject: [PATCH 005/170] attempt 2 --- .github/workflows/app.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml index fa0a6e7..0e8d9fa 100644 --- a/.github/workflows/app.yml +++ b/.github/workflows/app.yml @@ -47,6 +47,9 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Test bicep + run: az bicep build --file ./infrastructure/main.bicep --stdout + - name: Build container images run: | docker build . --file Dockerfile --tag ${{ secrets.ACR_LOGIN_SERVER }}/$IMAGE_NAME:${{needs.versionize.outputs.semver}} @@ -77,9 +80,6 @@ jobs: sparse-checkout: | infrastructure - - name: Test bicep - run: az bicep build ./infrastructure/main.bicep - - name: Az CLI login uses: azure/login@v2 with: From 007b812d7a2ffbbfa03ca5ec54cc2361ef93eecf Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Thu, 28 Nov 2024 11:22:56 +0100 Subject: [PATCH 006/170] tricep should be public... --- .github/workflows/app.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml index 0e8d9fa..4cb64d9 100644 --- a/.github/workflows/app.yml +++ b/.github/workflows/app.yml @@ -47,6 +47,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Az CLI login + uses: azure/login@v2 + with: + client-id: ${{ secrets.CALQUE_PRD_CLIENT_ID }} + tenant-id: ${{ secrets.CALQUE_PRD_TENANT_ID }} + subscription-id: ${{ secrets.CALQUE_PRD_SUBSCRIPTION_ID }} + - name: Test bicep run: az bicep build --file ./infrastructure/main.bicep --stdout From fa86609db9b34d60a20007a875d36ab596bb28a1 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Thu, 28 Nov 2024 11:31:07 +0100 Subject: [PATCH 007/170] next try --- .github/workflows/app.yml | 13 ++++--------- infrastructure/bicepconfig.json | 6 ++++++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml index 4cb64d9..46c7739 100644 --- a/.github/workflows/app.yml +++ b/.github/workflows/app.yml @@ -47,15 +47,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Az CLI login - uses: azure/login@v2 - with: - client-id: ${{ secrets.CALQUE_PRD_CLIENT_ID }} - tenant-id: ${{ secrets.CALQUE_PRD_TENANT_ID }} - subscription-id: ${{ secrets.CALQUE_PRD_SUBSCRIPTION_ID }} - - name: Test bicep - run: az bicep build --file ./infrastructure/main.bicep --stdout + uses: Azure/cli@v2 + with: + inlineScript: | + az bicep build --file ./infrastructure/main.bicep --stdout - name: Build container images run: | @@ -97,7 +93,6 @@ jobs: - name: Deploy bicep uses: Azure/cli@v2 with: - azcliversion: 2.66.0 inlineScript: | az deployment sub create \ --location westeurope \ diff --git a/infrastructure/bicepconfig.json b/infrastructure/bicepconfig.json index dc847c0..9812ce7 100644 --- a/infrastructure/bicepconfig.json +++ b/infrastructure/bicepconfig.json @@ -8,5 +8,11 @@ "registry": "acreuwprdtricep.azurecr.io" } } + }, + "cloud": { + "currentProfile": "AzureCloud", + "credentialPrecedence": [ + "AzureCLI" + ] } } \ No newline at end of file From 4e98849e07661da91856159ccebe18787afaa0ef Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Mon, 2 Dec 2024 16:26:00 +0100 Subject: [PATCH 008/170] implemented feature TODOs: - extract logic to feature file to simplify component - add unit test - add end-to-end tests --- src/features/file/grid.tsx | 27 +++++++++-- src/routes/(editor)/edit.tsx | 86 ++++++++++++++++++++++++++++++------ src/utilities.ts | 8 ++-- 3 files changed, 101 insertions(+), 20 deletions(-) diff --git a/src/features/file/grid.tsx b/src/features/file/grid.tsx index 7baf4f0..d3c381c 100644 --- a/src/features/file/grid.tsx +++ b/src/features/file/grid.tsx @@ -19,6 +19,7 @@ export interface GridContextType { mutate(prop: string, lang: string, value: string): void; remove(props: string[]): void; insert(prop: string): void; + addColumn(name: string): void; } export interface GridApi { @@ -29,6 +30,7 @@ export interface GridApi { clear(): void; remove(keys: string[]): void; insert(prop: string): void; + addColumn(name: string): void; } const GridContext = createContext(); @@ -38,8 +40,9 @@ const useGrid = () => useContext(GridContext)!; export const Grid: Component<{ class?: string, columns: string[], rows: Rows, api?: (api: GridApi) => any }> = (props) => { const [selection, setSelection] = createSignal([]); - const [state, setState] = createStore<{ rows: Record>, snapshot: Rows, numberOfRows: number }>({ + const [state, setState] = createStore<{ rows: Record>, columns: string[], snapshot: Rows, numberOfRows: number }>({ rows: {}, + columns: [], snapshot: new Map, numberOfRows: 0, }); @@ -51,12 +54,17 @@ export const Grid: Component<{ class?: string, columns: string[], rows: Rows, ap return deepDiff(state.snapshot, state.rows).toArray(); }); const rows = createMemo(() => Object.fromEntries(Object.entries(state.rows).map(([key, row]) => [key, unwrap(row)] as const))); + const columns = createMemo(() => state.columns); createEffect(() => { setState('rows', Object.fromEntries(deepCopy(props.rows).entries())); setState('snapshot', props.rows); }); + createEffect(() => { + setState('columns', [...props.columns]); + }); + createEffect(() => { setState('numberOfRows', Object.keys(state.rows).length); }); @@ -82,18 +90,27 @@ export const Grid: Component<{ class?: string, columns: string[], rows: Rows, ap insert(prop: string) { setState('rows', produce(rows => { - rows[prop] = Object.fromEntries(props.columns.slice(1).map(lang => [lang, ''])); + rows[prop] = Object.fromEntries(state.columns.slice(1).map(lang => [lang, ''])); return rows })) }, + + addColumn(name: string): void { + setState(produce(state => { + state.columns.push(name); + state.rows = Object.fromEntries(Object.entries(state.rows).map(([key, row]) => [key, { ...row, [name]: '' }])); + + return state; + })) + }, }; return - <_Grid class={props.class} columns={props.columns} rows={rows()} /> + <_Grid class={props.class} columns={columns()} rows={rows()} /> ; }; @@ -154,6 +171,10 @@ const Api: Component<{ api: undefined | ((api: GridApi) => any) }> = (props) => insert(prop: string) { gridContext.insert(prop); }, + + addColumn(name: string): void { + gridContext.addColumn(name); + }, }; createEffect(() => { diff --git a/src/routes/(editor)/edit.tsx b/src/routes/(editor)/edit.tsx index 37f7fc7..9d50b1e 100644 --- a/src/routes/(editor)/edit.tsx +++ b/src/routes/(editor)/edit.tsx @@ -1,5 +1,5 @@ import { Component, createEffect, createMemo, createSignal, For, onMount, ParentProps, Setter, Show } from "solid-js"; -import { filter, MutarionKind, Mutation, splitAt } from "~/utilities"; +import { Created, filter, MutarionKind, Mutation, splitAt } from "~/utilities"; import { Sidebar } from "~/components/sidebar"; import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree } from "~/components/filetree"; import { Menu } from "~/features/menu"; @@ -91,7 +91,8 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { const [active, setActive] = createSignal(); const [contents, setContents] = createSignal>>(new Map()); const [tree, setFiles] = createSignal(emptyFolder); - const [prompt, setPrompt] = createSignal(); + const [newKeyPrompt, setNewKeyPrompt] = createSignal(); + const [newLanguagePrompt, setNewLanguagePrompt] = createSignal(); const tab = createMemo(() => { const name = active(); @@ -99,7 +100,7 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { return tabs().find(t => t.handle.name === name); }); const api = createMemo(() => tab()?.api()); - const mutations = createMemo<(Mutation & { file?: { value: string, handle: FileSystemFileHandle, id: string } })[]>(() => tabs().flatMap(tab => { + const mutations = createMemo<(Mutation & { lang: string, file?: { value: string, handle: FileSystemFileHandle, id: string } })[]>(() => tabs().flatMap(tab => { const entries = tab.entries(); const files = tab.files(); const mutations = tab.api()?.mutations() ?? []; @@ -109,11 +110,19 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { case MutarionKind.Update: { const [key, lang] = splitAt(m.key, m.key.lastIndexOf('.')); - return { kind: MutarionKind.Update, key, file: entries.get(key)?.[lang] }; + return { kind: MutarionKind.Update, key, lang, file: entries.get(key)?.[lang] }; } case MutarionKind.Create: { - return Object.entries(m.value).map(([lang, value]) => ({ kind: MutarionKind.Create, key: m.key, file: files.get(lang)!, value })); + if (typeof m.value === 'object') { + return Object.entries(m.value).map(([lang, value]) => { + return ({ kind: MutarionKind.Create, key: m.key, lang, file: files.get(lang)!, value }); + }); + } + + const [key, lang] = splitAt(m.key, m.key.lastIndexOf('.')); + + return { kind: MutarionKind.Create, key, lang, file: undefined, value: m.value }; } case MutarionKind.Delete: { @@ -137,8 +146,35 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { } const groupedByFileId = Object.groupBy(muts, m => m.file?.id ?? 'undefined'); + const newFiles = Object.entries(Object.groupBy((groupedByFileId['undefined'] ?? []) as (Created & { lang: string, file: undefined })[], m => m.lang)).map(([lang, mutations]) => { + const data = mutations!.reduce((aggregate, { key, value }) => { + let obj = aggregate; + const i = key.lastIndexOf('.'); - return entries.map(({ id, handle }) => { + if (i !== -1) { + const [k, lastPart] = splitAt(key, i); + + for (const part of k.split('.')) { + if (!Object.hasOwn(obj, part)) { + obj[part] = {}; + } + + obj = obj[part]; + } + + obj[lastPart] = value; + } + else { + obj[key] = value; + } + + return aggregate; + }, {} as Record); + + return [{ existing: false, name: lang }, data] as const; + }) + + const existingFiles = entries.map(({ id, handle }) => { const existing = new Map(files.get(id)!); const mutations = groupedByFileId[id]!; @@ -158,7 +194,7 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { } return [ - handle, + { existing: true, handle }, existing.entries().reduce((aggregate, [key, value]) => { let obj = aggregate; const i = key.lastIndexOf('.'); @@ -183,9 +219,15 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { return aggregate; }, {} as Record) ] as const; - }).toArray(); + }).toArray() as (readonly [({ existing: true, handle: FileSystemFileHandle } | { existing: false, name: string }), Record])[]; + + return existingFiles.concat(newFiles); }); + // createEffect(() => { + // console.log(mutatedData()); + // }); + createEffect(() => { const directory = props.root; @@ -208,7 +250,10 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { filesContext.remove(id); }, { key: 'w', modifier: Modifier.Control | (isInstalledPWA ? Modifier.None : Modifier.Alt) }), save: createCommand('save', async () => { - await Promise.allSettled(mutatedData().map(async ([handle, data]) => { + await Promise.allSettled(mutatedData().map(async ([file, data]) => { + // TODO :: add the newly created file to the known files list to that the save file picker is not shown again on subsequent saves + const handle = file.existing ? file.handle : await window.showSaveFilePicker({ suggestedName: file.name, excludeAcceptAllOption: true, types: [{ description: 'JSON file', accept: { 'application/json': ['.json'] } }] }); + const stream = await handle.createWritable({ keepExistingData: false }); stream.write(JSON.stringify(data, null, 4)); @@ -246,7 +291,7 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { remove(Object.keys(selection())); }, { key: 'delete', modifier: Modifier.None }), inserNewKey: createCommand('insert new key', async () => { - const formData = await prompt()?.showModal(); + const formData = await newKeyPrompt()?.showModal(); const key = formData?.get('key')?.toString(); if (!key) { @@ -255,7 +300,18 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { api()?.insert(key); }), - inserNewLanguage: noop.withLabel('insert new language'), + inserNewLanguage: createCommand('insert new language', async () => { + const formData = await newLanguagePrompt()?.showModal(); + const language = formData?.get('locale')?.toString(); + + if (!language) { + return; + } + + console.log(language); + + api()?.addColumn(language); + }), } as const; return
@@ -295,8 +351,12 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { - hint: use . to denote nested keys,
i.e. this.is.some.key would be a key that is four levels deep}> - + hint: use . to denote nested keys,
i.e. this.is.some.key would be a key that is four levels deep}> + +
+ + + diff --git a/src/utilities.ts b/src/utilities.ts index 8408a4e..c47a58b 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -54,10 +54,10 @@ export enum MutarionKind { Update = 'updated', Delete = 'deleted', } -type Created = { kind: MutarionKind.Create, value: any }; -type Updated = { kind: MutarionKind.Update, value: any, original: any }; -type Deleted = { kind: MutarionKind.Delete }; -export type Mutation = { key: string } & (Created | Updated | Deleted); +export type Created = { kind: MutarionKind.Create, key: string, value: any }; +export type Updated = { kind: MutarionKind.Update, key: string, value: any, original: any }; +export type Deleted = { kind: MutarionKind.Delete, key: string }; +export type Mutation = Created | Updated | Deleted; export function* deepDiff(a: T1, b: T2, path: string[] = []): Generator { if (!isIterable(a) || !isIterable(b)) { From f301f384d7d333f84c066be6a0c5fd1c41154fe1 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Tue, 3 Dec 2024 08:44:51 +0100 Subject: [PATCH 009/170] bug repro --- infrastructure/repro.bicep | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 infrastructure/repro.bicep diff --git a/infrastructure/repro.bicep b/infrastructure/repro.bicep new file mode 100644 index 0000000..965826c --- /dev/null +++ b/infrastructure/repro.bicep @@ -0,0 +1,12 @@ +import { + container + resources_xxs +} from 'br/Tricep:recommended/app/container-app:latest' + +targetScope = 'resourceGroup' + +var container1 = container({ + name: 'name' + image: 'registry/project-app:latest' + resources: resources_xxs +}) From 17e49c23d879bee4ebf2286ee506ada9b5b4cf7a Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Tue, 3 Dec 2024 16:01:14 +0100 Subject: [PATCH 010/170] started splitting of into components --- src/components/grid.module.css | 128 +++++++++ src/components/grid.tsx | 280 ++++++++++++++++++++ src/components/table.module.css | 108 ++++++++ src/components/table.tsx | 114 ++++++++ src/routes/(editor)/edit.tsx | 4 - src/routes/(editor)/experimental.css | 27 -- src/routes/(editor)/experimental.module.css | 26 ++ src/routes/(editor)/experimental.tsx | 157 +++-------- 8 files changed, 686 insertions(+), 158 deletions(-) create mode 100644 src/components/grid.module.css create mode 100644 src/components/grid.tsx create mode 100644 src/components/table.module.css create mode 100644 src/components/table.tsx delete mode 100644 src/routes/(editor)/experimental.css create mode 100644 src/routes/(editor)/experimental.module.css diff --git a/src/components/grid.module.css b/src/components/grid.module.css new file mode 100644 index 0000000..05c99e3 --- /dev/null +++ b/src/components/grid.module.css @@ -0,0 +1,128 @@ +.table { + position: relative; + display: grid; + grid-template-columns: 2em minmax(10em, max-content) repeat(var(--columns), auto); + align-content: start; + padding-inline: 1px; + margin-inline: -1px; + + block-size: 100%; + overflow: clip auto; + + background-color: var(--surface-600); + + & input[type="checkbox"] { + margin: .1em; + } + + & textarea { + resize: vertical; + min-block-size: max(2em, 100%); + max-block-size: 50em; + + background-color: var(--surface-600); + color: var(--text-1); + border-color: var(--text-2); + border-radius: var(--radii-s); + + &:has(::spelling-error, ::grammar-error) { + border-color: var(--fail); + } + + & ::spelling-error { + outline: 1px solid var(--fail); + text-decoration: yellow underline; + } + } + + & .cell { + display: grid; + padding: .5em; + border: 1px solid transparent; + border-radius: var(--radii-m); + + &:has(textarea:focus) { + border-color: var(--info); + } + + & > span { + align-self: center; + } + } + + & :is(.header, .main, .footer) { + grid-column: span calc(2 + var(--columns)); + display: grid; + grid-template-columns: subgrid; + } + + & .header { + position: sticky; + inset-block-start: 0; + background-color: var(--surface-600); + border-block-end: 1px solid var(--surface-300); + } + + & .row { + --bg: var(--text); + --alpha: 0; + grid-column: span calc(2 + var(--columns)); + display: grid; + grid-template-columns: subgrid; + border: 1px solid transparent; + background-color: color(from var(--bg) srgb r g b / var(--alpha)); + + &:has(> .cell > :checked) { + --bg: var(--info); + --alpha: .1; + border-color: var(--bg); + + & span { + font-variation-settings: 'GRAD' 1000; + } + + & + :has(> .cell> :checked) { + border-block-start-color: transparent; + } + + &:has(+ .row > .cell > :checked) { + border-block-end-color: transparent; + } + } + + &:hover { + --alpha: .2 !important; + } + } + + & details { + display: contents; + + &::details-content { + grid-column: span calc(2 + var(--columns)); + display: grid; + grid-template-columns: subgrid; + } + + &:not([open])::details-content { + display: none; + } + + & > summary { + grid-column: 2 / span calc(1 + var(--columns)); + padding: .5em; + padding-inline-start: calc(var(--depth) * 1em + .5em); + + } + + & > .row > .cell > span { + padding-inline-start: calc(var(--depth) * 1em); + } + } +} + +@property --depth { + syntax: ""; + inherits: false; + initial-value: 0; +} \ No newline at end of file diff --git a/src/components/grid.tsx b/src/components/grid.tsx new file mode 100644 index 0000000..c0e0c2a --- /dev/null +++ b/src/components/grid.tsx @@ -0,0 +1,280 @@ +import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, ParentComponent, Show, useContext } from "solid-js"; +import { createStore, produce, unwrap } from "solid-js/store"; +import { SelectionProvider, useSelection, selectable } from "../features/selectable"; +import { debounce, deepCopy, deepDiff, Mutation } from "~/utilities"; +import css from './grid.module.css'; + +selectable // prevents removal of import + +type Rows = Map>; +type SelectionItem = { key: string, value: Accessor>, element: WeakRef }; + +type Insertion = { kind: 'row', key: string } | { kind: 'column', value: string }; + +export interface GridContextType { + readonly rows: Accessor>>; + readonly mutations: Accessor; + readonly selection: Accessor; + mutate(prop: string, lang: string, value: string): void; + remove(props: string[]): void; + insert(insertion: Insertion): void; +} + +export interface GridApi { + readonly selection: Accessor>>; + readonly rows: Accessor>>; + readonly mutations: Accessor; + selectAll(): void; + clear(): void; + remove(keys: string[]): void; + insert(insertion: Insertion): void; +} + +const GridContext = createContext(); + +const useGrid = () => useContext(GridContext)!; + +export const Grid: Component<{ class?: string, columns: string[], rows: Rows, api?: (api: GridApi) => any }> = (props) => { + const [selection, setSelection] = createSignal([]); + const [state, setState] = createStore<{ rows: Record>, columns: string[], snapshot: Rows, numberOfRows: number }>({ + rows: {}, + columns: [], + snapshot: new Map, + numberOfRows: 0, + }); + + const mutations = createMemo(() => { + // enumerate all values to make sure the memo is recalculated on any change + Object.values(state.rows).map(entry => Object.values(entry)); + + return deepDiff(state.snapshot, state.rows).toArray(); + }); + const rows = createMemo(() => Object.fromEntries(Object.entries(state.rows).map(([key, row]) => [key, unwrap(row)] as const))); + const columns = createMemo(() => state.columns); + + createEffect(() => { + setState('rows', Object.fromEntries(deepCopy(props.rows).entries())); + setState('snapshot', props.rows); + }); + + createEffect(() => { + setState('columns', [...props.columns]); + }); + + createEffect(() => { + setState('numberOfRows', Object.keys(state.rows).length); + }); + + const ctx: GridContextType = { + rows, + mutations, + selection, + + mutate(prop: string, lang: string, value: string) { + setState('rows', prop, lang, value); + }, + + remove(props: string[]) { + setState('rows', produce(rows => { + for (const prop of props) { + delete rows[prop]; + } + + return rows; + })); + }, + + insert(prop: string) { + setState('rows', produce(rows => { + rows[prop] = Object.fromEntries(state.columns.slice(1).map(lang => [lang, ''])); + + return rows + })) + }, + + addColumn(name: string): void { + setState(produce(state => { + state.columns.push(name); + state.rows = Object.fromEntries(Object.entries(state.rows).map(([key, row]) => [key, { ...row, [name]: '' }])); + + return state; + })) + }, + }; + + return + + + + <_Grid class={props.class} columns={columns()} rows={rows()} /> + + ; +}; + +const _Grid: Component<{ class?: string, columns: string[], rows: Record> }> = (props) => { + const columnCount = createMemo(() => props.columns.length - 1); + const root = createMemo(() => Object.entries(props.rows) + .reduce((aggregate, [key, value]) => { + let obj: any = aggregate; + const parts = key.split('.'); + + for (const [i, part] of parts.entries()) { + if (Object.hasOwn(obj, part) === false) { + obj[part] = {}; + } + + if (i === (parts.length - 1)) { + obj[part] = value; + } + else { + obj = obj[part]; + } + } + + return aggregate; + }, {})); + + return
+ + +
+ +
+
+}; + +const Api: Component<{ api: undefined | ((api: GridApi) => any) }> = (props) => { + const gridContext = useGrid(); + const selectionContext = useSelection<{ key: string, value: Accessor>, element: WeakRef }>(); + + const api: GridApi = { + selection: createMemo(() => { + const selection = selectionContext.selection(); + + return Object.fromEntries(selection.map(({ key, value }) => [key, value()] as const)); + }), + rows: gridContext.rows, + mutations: gridContext.mutations, + selectAll() { + selectionContext.selectAll(); + }, + clear() { + selectionContext.clear(); + }, + remove(props: string[]) { + gridContext.remove(props); + }, + insert(prop: string) { + gridContext.insert(prop); + }, + + addColumn(name: string): void { + gridContext.addColumn(name); + }, + }; + + createEffect(() => { + props.api?.(api); + }); + + return null; +}; + +const Head: Component<{ headers: string[] }> = (props) => { + const context = useSelection(); + + return
+
+ 0 && context.selection().length === context.length()} + indeterminate={context.selection().length !== 0 && context.selection().length !== context.length()} + on:input={(e: InputEvent) => e.target.checked ? context.selectAll() : context.clear()} + /> +
+ + { + header => {header} + } +
; +}; + +const Row: Component<{ entry: Entry, path?: string[] }> = (props) => { + const grid = useGrid(); + + return { + ([key, value]) => { + const values = Object.entries(value); + const path = [...(props.path ?? []), key]; + const k = path.join('.'); + const context = useSelection(); + + const isSelected = context.isSelected(k); + + return }> +
+
+ context.select([k])} on:pointerdown={e => e.stopPropagation()} /> +
+ +
+ {key} +
+ + { + ([lang, value]) =>
+ + -