Merge branch 'main' into iain/dockerfile-node18-node22
This commit is contained in:
+102
-14
@@ -16,16 +16,23 @@ workflows:
|
||||
- main
|
||||
- hotfix*
|
||||
|
||||
- test-server:
|
||||
- test-server: &test-server-job-definition
|
||||
context:
|
||||
- speckle-server-licensing
|
||||
- stripe-integration
|
||||
filters: &filters-allow-all
|
||||
tags:
|
||||
# run tests for any commit on any branch, including any tags
|
||||
only: /.*/
|
||||
requires:
|
||||
- docker-publish-postgres-container
|
||||
|
||||
- test-server-no-ff:
|
||||
filters: *filters-allow-all
|
||||
requires:
|
||||
- docker-publish-postgres-container
|
||||
|
||||
- test-server-multiregion: *test-server-job-definition
|
||||
|
||||
- test-frontend-2:
|
||||
filters: *filters-allow-all
|
||||
@@ -145,6 +152,12 @@ workflows:
|
||||
requires:
|
||||
- get-version
|
||||
|
||||
- docker-build-postgres-container:
|
||||
context: *build-context
|
||||
filters: *filters-build
|
||||
requires:
|
||||
- get-version
|
||||
|
||||
- docker-build-monitor-container:
|
||||
context: *build-context
|
||||
filters: *filters-build
|
||||
@@ -179,6 +192,7 @@ workflows:
|
||||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-frontend:
|
||||
@@ -194,6 +208,7 @@ workflows:
|
||||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-frontend-2:
|
||||
@@ -209,6 +224,7 @@ workflows:
|
||||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-webhooks:
|
||||
@@ -224,6 +240,7 @@ workflows:
|
||||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-file-imports:
|
||||
@@ -239,6 +256,7 @@ workflows:
|
||||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-previews:
|
||||
@@ -254,6 +272,7 @@ workflows:
|
||||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-test-container:
|
||||
@@ -269,8 +288,15 @@ workflows:
|
||||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-postgres-container:
|
||||
context: *docker-hub-context
|
||||
filters: *filters-publish
|
||||
requires:
|
||||
- docker-build-postgres-container
|
||||
|
||||
- docker-publish-monitor-container:
|
||||
context: *docker-hub-context
|
||||
filters: *filters-publish
|
||||
@@ -284,6 +310,7 @@ workflows:
|
||||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- docker-publish-docker-compose-ingress:
|
||||
@@ -299,6 +326,7 @@ workflows:
|
||||
- test-objectsender
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-preview-service
|
||||
|
||||
- publish-helm-chart:
|
||||
@@ -339,6 +367,7 @@ workflows:
|
||||
- get-version
|
||||
- test-server
|
||||
- test-server-no-ff
|
||||
- test-server-multiregion
|
||||
- test-ui-components
|
||||
- test-frontend-2
|
||||
- test-viewer
|
||||
@@ -438,11 +467,12 @@ jobs:
|
||||
docker:
|
||||
- image: cimg/node:22.6.0
|
||||
- image: cimg/redis:7.2.4
|
||||
- image: cimg/postgres:14.11
|
||||
- image: 'speckle/speckle-postgres'
|
||||
environment:
|
||||
POSTGRES_DB: speckle2_test
|
||||
POSTGRES_PASSWORD: speckle
|
||||
POSTGRES_USER: speckle
|
||||
command: -c 'max_connections=1000'
|
||||
- image: 'minio/minio'
|
||||
command: server /data --console-address ":9001"
|
||||
# environment:
|
||||
@@ -452,6 +482,7 @@ jobs:
|
||||
NODE_ENV: test
|
||||
DATABASE_URL: 'postgres://speckle:speckle@127.0.0.1:5432/speckle2_test'
|
||||
PGDATABASE: speckle2_test
|
||||
POSTGRES_MAX_CONNECTIONS_SERVER: 20
|
||||
PGUSER: speckle
|
||||
SESSION_SECRET: 'keyboard cat'
|
||||
STRATEGY_LOCAL: 'true'
|
||||
@@ -463,7 +494,9 @@ jobs:
|
||||
S3_CREATE_BUCKET: 'true'
|
||||
REDIS_URL: 'redis://127.0.0.1:6379'
|
||||
S3_REGION: '' # optional, defaults to 'us-east-1'
|
||||
AUTOMATE_ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json'
|
||||
ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json'
|
||||
FF_BILLING_INTEGRATION_ENABLED: 'true'
|
||||
RATELIMITER_ENABLED: 'false'
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
@@ -511,8 +544,8 @@ jobs:
|
||||
working_directory: 'packages/server'
|
||||
|
||||
- run:
|
||||
name: Checking for GQL schema breakages against speckle.xyz
|
||||
command: 'yarn rover graph check Speckle-Server@speckle-xyz --schema ./introspected-schema.graphql'
|
||||
name: Checking for GQL schema breakages against app.speckle.systems
|
||||
command: 'yarn rover graph check Speckle-Server@app-speckle-systems --schema ./introspected-schema.graphql'
|
||||
working_directory: 'packages/server'
|
||||
|
||||
- run:
|
||||
@@ -544,12 +577,56 @@ jobs:
|
||||
S3_CREATE_BUCKET: 'true'
|
||||
REDIS_URL: 'redis://127.0.0.1:6379'
|
||||
S3_REGION: '' # optional, defaults to 'us-east-1'
|
||||
AUTOMATE_ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json'
|
||||
FF_AUTOMATE_MODULE_ENABLED: 'false' # Disable all FFs
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'false'
|
||||
FF_WORKSPACES_SSO_ENABLED: 'false'
|
||||
FF_MULTIPLE_EMAILS_MODULE_ENABLED: 'false'
|
||||
FF_GENDOAI_MODULE_ENABLED: 'false'
|
||||
ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json'
|
||||
DISABLE_ALL_FFS: 'true'
|
||||
RATELIMITER_ENABLED: 'false'
|
||||
|
||||
test-server-multiregion:
|
||||
<<: *test-server-job
|
||||
docker:
|
||||
- image: cimg/node:18.19.0
|
||||
- image: cimg/redis:7.2.4
|
||||
- image: 'speckle/speckle-postgres'
|
||||
environment:
|
||||
POSTGRES_DB: speckle2_test
|
||||
POSTGRES_PASSWORD: speckle
|
||||
POSTGRES_USER: speckle
|
||||
command: -c 'max_connections=1000' -c 'wal_level=logical'
|
||||
- image: 'speckle/speckle-postgres'
|
||||
environment:
|
||||
POSTGRES_DB: speckle2_test
|
||||
POSTGRES_PASSWORD: speckle
|
||||
POSTGRES_USER: speckle
|
||||
command: -c 'max_connections=1000' -c 'port=5433' -c 'wal_level=logical'
|
||||
- image: 'minio/minio'
|
||||
command: server /data --console-address ":9001" --address "0.0.0.0:9000"
|
||||
- image: 'minio/minio'
|
||||
command: server /data --console-address ":9021" --address "0.0.0.0:9020"
|
||||
environment:
|
||||
# Same as test-server:
|
||||
NODE_ENV: test
|
||||
DATABASE_URL: 'postgres://speckle:speckle@127.0.0.1:5432/speckle2_test'
|
||||
PGDATABASE: speckle2_test
|
||||
POSTGRES_MAX_CONNECTIONS_SERVER: 20
|
||||
PGUSER: speckle
|
||||
SESSION_SECRET: 'keyboard cat'
|
||||
STRATEGY_LOCAL: 'true'
|
||||
CANONICAL_URL: 'http://127.0.0.1:3000'
|
||||
S3_ENDPOINT: 'http://127.0.0.1:9000'
|
||||
S3_ACCESS_KEY: 'minioadmin'
|
||||
S3_SECRET_KEY: 'minioadmin'
|
||||
S3_BUCKET: 'speckle-server'
|
||||
S3_CREATE_BUCKET: 'true'
|
||||
REDIS_URL: 'redis://127.0.0.1:6379'
|
||||
S3_REGION: '' # optional, defaults to 'us-east-1'
|
||||
ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json'
|
||||
FF_BILLING_INTEGRATION_ENABLED: 'true'
|
||||
# These are the only different env keys:
|
||||
MULTI_REGION_CONFIG_PATH: '../../.circleci/multiregion.test-ci.json'
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
FF_WORKSPACES_MULTI_REGION_ENABLED: 'true'
|
||||
RUN_TESTS_IN_MULTIREGION_MODE: true
|
||||
RATELIMITER_ENABLED: 'false'
|
||||
|
||||
test-frontend-2:
|
||||
docker: &docker-node-browsers-image
|
||||
@@ -618,7 +695,7 @@ jobs:
|
||||
test-preview-service:
|
||||
docker:
|
||||
- image: cimg/node:22.6.0-browsers
|
||||
- image: cimg/postgres:14.11
|
||||
- image: cimg/postgres:16.4@sha256:2e4f1a965bdd9ba77aa6a0a7b93968c07576ba2a8a7cf86d5eb7b31483db1378
|
||||
environment:
|
||||
POSTGRES_DB: preview_service_test
|
||||
POSTGRES_PASSWORD: preview_service_test
|
||||
@@ -965,10 +1042,15 @@ jobs:
|
||||
FOLDER: utils
|
||||
SPECKLE_SERVER_PACKAGE: test-deployment
|
||||
|
||||
docker-build-monitor-container:
|
||||
docker-build-postgres-container:
|
||||
<<: *build-job
|
||||
environment:
|
||||
FOLDER: utils
|
||||
SPECKLE_SERVER_PACKAGE: postgres
|
||||
|
||||
docker-build-monitor-container:
|
||||
<<: *build-job
|
||||
environment:
|
||||
SPECKLE_SERVER_PACKAGE: monitor-deployment
|
||||
|
||||
docker-build-docker-compose-ingress:
|
||||
@@ -1029,10 +1111,15 @@ jobs:
|
||||
FOLDER: utils
|
||||
SPECKLE_SERVER_PACKAGE: test-deployment
|
||||
|
||||
docker-publish-monitor-container:
|
||||
docker-publish-postgres-container:
|
||||
<<: *publish-job
|
||||
environment:
|
||||
FOLDER: utils
|
||||
SPECKLE_SERVER_PACKAGE: postgres
|
||||
|
||||
docker-publish-monitor-container:
|
||||
<<: *publish-job
|
||||
environment:
|
||||
SPECKLE_SERVER_PACKAGE: monitor-deployment
|
||||
|
||||
docker-publish-docker-compose-ingress:
|
||||
@@ -1117,6 +1204,7 @@ jobs:
|
||||
publish-viewer-sandbox-cloudflare-pages:
|
||||
docker: *docker-node-image
|
||||
working_directory: *work-dir
|
||||
resource_class: large
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
||||
@@ -45,11 +45,12 @@ k8s_yaml('./manifests/priorityclass.yaml')
|
||||
k8s_yaml('./manifests/speckle-server.secret.yaml')
|
||||
|
||||
# Install charts
|
||||
# Postgres 16.4 is packaged in chart 15.5.38
|
||||
helm_resource('postgresql',
|
||||
release_name='postgresql',
|
||||
namespace='postgres',
|
||||
chart='oci://registry-1.docker.io/bitnamicharts/postgresql',
|
||||
flags=['--version=^12.0.0',
|
||||
flags=['--version=^15.5.38',
|
||||
'--values=./values/postgres.values.yaml',
|
||||
'--kube-context=kind-speckle-server'],
|
||||
deps=['./values/postgres.values.yaml'],
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"main": {
|
||||
"postgres": {
|
||||
"connectionUri": "postgresql://speckle:speckle@127.0.0.1:5432/speckle2_test"
|
||||
},
|
||||
"blobStorage": {
|
||||
"accessKey": "minioadmin",
|
||||
"secretKey": "minioadmin",
|
||||
"bucket": "speckle-server",
|
||||
"createBucketIfNotExists": true,
|
||||
"endpoint": "http://127.0.0.1:9000",
|
||||
"s3Region": "us-east-1"
|
||||
}
|
||||
},
|
||||
"regions": {
|
||||
"region1": {
|
||||
"postgres": {
|
||||
"connectionUri": "postgresql://speckle:speckle@127.0.0.1:5433/speckle2_test"
|
||||
},
|
||||
"blobStorage": {
|
||||
"accessKey": "minioadmin",
|
||||
"secretKey": "minioadmin",
|
||||
"bucket": "speckle-server",
|
||||
"createBucketIfNotExists": true,
|
||||
"endpoint": "http://127.0.0.1:9020",
|
||||
"s3Region": "us-east-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
services:
|
||||
postgres:
|
||||
# Docker Hub image
|
||||
image: postgres:14
|
||||
image: postgres:16.4-bookworm@sha256:e62fbf9d3e2b49816a32c400ed2dba83e3b361e6833e624024309c35d334b412
|
||||
env:
|
||||
POSTGRES_DB: preview_service_test
|
||||
POSTGRES_PASSWORD: preview_service_test
|
||||
|
||||
+10
-1
@@ -71,4 +71,13 @@ minio-data/
|
||||
postgres-data/
|
||||
redis-data/
|
||||
|
||||
.tshy-build
|
||||
.tshy-build
|
||||
obj/
|
||||
bin/
|
||||
!packages/monitor-deployment/bin
|
||||
!packages/preview-service/bin
|
||||
!packages/server/bin
|
||||
|
||||
# Server
|
||||
multiregion.json
|
||||
multiregion.test.json
|
||||
+5
-1
@@ -38,4 +38,8 @@ venv
|
||||
|
||||
storybook-static
|
||||
.tshy
|
||||
.tshy-build
|
||||
.tshy-build
|
||||
|
||||
packages/fileimport-service/ifc-dotnet/
|
||||
packages/fileimport-service/stl/
|
||||
packages/fileimport-service/obj/
|
||||
@@ -2,12 +2,14 @@
|
||||
<img src="https://user-images.githubusercontent.com/2679513/131189167-18ea5fe1-c578-47f6-9785-3748178e4312.png" width="150px"/><br/>
|
||||
Speckle | Server
|
||||
</h1>
|
||||
|
||||
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&style=flat-square&logo=discourse&logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&logo=read-the-docs&logoColor=white" alt="docs"></a></p>
|
||||
|
||||
> Speckle is the first AEC data hub that connects with your favorite AEC tools. Speckle exists to overcome the challenges of working in a fragmented industry where communication, creative workflows, and the exchange of data are often hindered by siloed software and processes. It is here to make the industry better.
|
||||
|
||||
<h3 align="center">
|
||||
Server and Web packages
|
||||
</h3>
|
||||
<p align="center"><b>Speckle</b> is data infrastructure for the AEC industry.</p><br/>
|
||||
|
||||
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&style=flat-square&logo=discourse&logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&logo=read-the-docs&logoColor=white" alt="docs"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://codecov.io/gh/specklesystems/speckle-server">
|
||||
@@ -18,37 +20,6 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# About Speckle
|
||||
|
||||
What is Speckle? Check our [](https://www.youtube.com/watch?v=B9humiSpHzM)
|
||||
|
||||
## Features
|
||||
|
||||
- **Object-based:** say goodbye to files! Speckle is the first object based platform for the AEC industry
|
||||
- **Version control:** Speckle is the Git & Hub for geometry and BIM data
|
||||
- **Collaboration:** share your designs collaborate with others
|
||||
- **3D Viewer:** see your CAD and BIM models online, share and embed them anywhere
|
||||
- **Connectivity:** get your CAD and BIM models into other software without exporting or importing
|
||||
- **Real time:** get real time updates and notifications and changes
|
||||
- **GraphQL API:** get what you need anywhere you want it
|
||||
- **Webhooks:** the base for a automation and next-gen pipelines
|
||||
- **Built for developers:** we are building Speckle with developers in mind and have tools for every stack
|
||||
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender, ArchiCAD and more!
|
||||
|
||||
## Try Speckle now!
|
||||
|
||||
Give Speckle a try in no time by:
|
||||
|
||||
- [](https://app.speckle.systems) ⇒ Create an account at app.speckle.systems
|
||||
- [](<[https://](https://speckle.guide/dev/server-manualsetup.html)>) ⇒ Deploy on your own infrastructure with Docker Compose
|
||||
- [](<[https://](https://speckle.guide/dev/server-setup-k8s.html)>) ⇒ Deploy on your own infrastructure with Kubernetes
|
||||
|
||||
## Resources
|
||||
|
||||
- [](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
|
||||
- [](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
|
||||
- [](https://speckle.guide/dev/) reference on almost any end-user and developer functionality
|
||||
|
||||
# Repo structure
|
||||
|
||||
This monorepo is the home of the Speckle v2 web packages:
|
||||
@@ -65,8 +36,10 @@ This monorepo is the home of the Speckle v2 web packages:
|
||||
|
||||
Make sure to also check and ⭐️ these other Speckle repositories:
|
||||
|
||||
- [`speckle-sharp`](https://github.com/specklesystems/speckle-sharp): .NET tooling, connectors and interoperability
|
||||
- [`speckle-sharp-connectors`](https://github.com/specklesystems/speckle-sharp-connectors): .NET connectors and desktop UI
|
||||
- [`speckle-sharp-sdk`](https://github.com/specklesystems/speckle-sharp-sdk): .NET SDK, tests, and Objects
|
||||
- [`specklepy`](https://github.com/specklesystems/specklepy): Python SDK 🐍
|
||||
- [`speckle-sketchup`](https://github.com/specklesystems/speckle-sketchup): Sketchup connector
|
||||
- [`speckle-excel`](https://github.com/specklesystems/speckle-excel): Excel connector
|
||||
- [`speckle-unity`](https://github.com/specklesystems/speckle-unity): Unity 3D connector
|
||||
- [`speckle-blender`](https://github.com/specklesystems/speckle-blender): Blender connector
|
||||
@@ -126,6 +99,14 @@ EMAIL_PORT="1025"
|
||||
|
||||
The web portal is available at `localhost:1080` and it's listening for mail on port `1025`.
|
||||
|
||||
### Minio (S3 storage)
|
||||
|
||||
Default credentials are: `minioadmin:minioadmin`
|
||||
Main storage Web UI: [http://localhost:9001/](http://localhost:9001/)
|
||||
Region1 storage Web UI: [http://localhost:9021/](http://localhost:9021/)
|
||||
|
||||
You can use the web UI to validate uploaded blobs
|
||||
|
||||
# Contributing
|
||||
|
||||
Please make sure you read the [contribution guidelines](https://github.com/specklesystems/speckle-server/blob/main/CONTRIBUTING.md) for an overview of the best practices we try to follow.
|
||||
|
||||
+29
-1
@@ -5,7 +5,7 @@ services:
|
||||
postgres:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/postgres/Dockerfile
|
||||
dockerfile: utils/postgres/Dockerfile
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: speckle
|
||||
@@ -18,6 +18,22 @@ services:
|
||||
ports:
|
||||
- '127.0.0.1:5432:5432'
|
||||
|
||||
postgres-region1:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: utils/postgres/Dockerfile
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: speckle
|
||||
POSTGRES_USER: speckle
|
||||
POSTGRES_PASSWORD: speckle
|
||||
volumes:
|
||||
- postgres-region1-data:/var/lib/postgresql/data/
|
||||
- ./setup/db/10-docker_postgres_init.sql:/docker-entrypoint-initdb.d/10-docker_postgres_init.sql
|
||||
- ./setup/db/11-docker_postgres_keycloack_init.sql:/docker-entrypoint-initdb.d/11-docker_postgres_keycloack_init.sql
|
||||
ports:
|
||||
- '127.0.0.1:5401:5432'
|
||||
|
||||
redis:
|
||||
image: 'redis:7-alpine'
|
||||
restart: always
|
||||
@@ -36,6 +52,16 @@ services:
|
||||
- '127.0.0.1:9000:9000'
|
||||
- '127.0.0.1:9001:9001'
|
||||
|
||||
minio-region1:
|
||||
image: 'minio/minio'
|
||||
command: server /data --console-address ":9001"
|
||||
restart: always
|
||||
volumes:
|
||||
- minio-region1-data:/data
|
||||
ports:
|
||||
- '127.0.0.1:9020:9000'
|
||||
- '127.0.0.1:9021:9001'
|
||||
|
||||
# Local OIDC provider for testing
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:25.0
|
||||
@@ -106,7 +132,9 @@ services:
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
postgres-region1-data:
|
||||
redis-data:
|
||||
pgadmin-data:
|
||||
redis_insight-data:
|
||||
minio-data:
|
||||
minio-region1-data:
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
FROM postgres:14.5-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache 'git=~2.36' \
|
||||
'build-base=~0.5' \
|
||||
'clang=~13.0' \
|
||||
'llvm13=~13.0'
|
||||
|
||||
WORKDIR /
|
||||
RUN git clone --branch 1.1.9 https://github.com/aiven/aiven-extras.git aiven-extras
|
||||
|
||||
WORKDIR /aiven-extras
|
||||
RUN git checkout 36598ab \
|
||||
&& git clean -df \
|
||||
&& make \
|
||||
&& make install
|
||||
|
||||
FROM postgres:14.5-alpine
|
||||
|
||||
COPY --from=builder /aiven-extras/aiven_extras.control /usr/local/share/postgresql/extension/aiven_extras.control
|
||||
COPY --from=builder /aiven-extras/sql/aiven_extras.sql /usr/local/share/postgresql/extension/aiven_extras--1.1.9.sql
|
||||
COPY --from=builder /aiven-extras/aiven_extras.so /usr/local/lib/postgresql/aiven_extras.so
|
||||
|
||||
EXPOSE 5432
|
||||
|
||||
CMD ["postgres"]
|
||||
@@ -21,6 +21,7 @@
|
||||
"dev:docker": "docker compose -f ./docker-compose-deps.yml",
|
||||
"dev:docker:up": "docker compose -f ./docker-compose-deps.yml up -d",
|
||||
"dev:docker:down": "docker compose -f ./docker-compose-deps.yml down",
|
||||
"dev:docker:restart": "yarn dev:docker:down && yarn dev:docker:up",
|
||||
"dev:kind:up": "ctlptl apply --filename ./.circleci/deployment/cluster-config.yaml",
|
||||
"dev:kind:down": "ctlptl delete -f ./.circleci/deployment/cluster-config.yaml",
|
||||
"dev:kind:helm:up": "yarn dev:kind:up && tilt up --file ./.circleci/deployment/Tiltfile.helm --context kind-speckle-server",
|
||||
@@ -58,6 +59,7 @@
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^12.3.7",
|
||||
"lockfile": "^1.0.4",
|
||||
"npkill": "^0.12.2",
|
||||
"pino-pretty": "^9.1.1",
|
||||
"prettier": "^2.5.1",
|
||||
"ts-node": "^10.9.1",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -80,7 +80,7 @@ function createCache(): InMemoryCache {
|
||||
},
|
||||
streams: {
|
||||
keyArgs: ['query'],
|
||||
merge: buildAbstractCollectionMergeFunction('StreamCollection', {
|
||||
merge: buildAbstractCollectionMergeFunction('UserStreamCollection', {
|
||||
checkIdentity: true
|
||||
})
|
||||
},
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import speckleTheme from '@speckle/tailwind-theme'
|
||||
import { tailwindContentEntry as themeEntry } from '@speckle/tailwind-theme/tailwind-configure'
|
||||
import { tailwindContentEntry as uiLibEntry } from '@speckle/ui-components/tailwind-configure'
|
||||
import { tailwindContentEntries as themeEntries } from '@speckle/tailwind-theme/tailwind-configure'
|
||||
import { tailwindContentEntries as uiLibEntries } from '@speckle/ui-components/tailwind-configure'
|
||||
import formsPlugin from '@tailwindcss/forms'
|
||||
|
||||
import { createRequire } from 'module'
|
||||
const req = createRequire(import.meta.url)
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const config = {
|
||||
darkMode: 'class',
|
||||
@@ -19,8 +16,8 @@ const config = {
|
||||
'./app.vue',
|
||||
'./.storybook/**/*.{js,ts,vue}',
|
||||
'./lib/**/composables/*.{js,ts}',
|
||||
themeEntry(req),
|
||||
uiLibEntry(req)
|
||||
...themeEntries(),
|
||||
...uiLibEntries()
|
||||
// `./lib/**/*.{js,ts,vue}`, // TODO: Wait for fix https://github.com/nuxt/framework/issues/2886#issuecomment-1108312903
|
||||
],
|
||||
plugins: [speckleTheme, formsPlugin]
|
||||
|
||||
+5
-3
@@ -5,11 +5,13 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch via NPM",
|
||||
"name": "Launch via Yarn",
|
||||
"request": "launch",
|
||||
"runtimeArgs": ["run-script", "dev"],
|
||||
"runtimeExecutable": "npm",
|
||||
"console": "integratedTerminal",
|
||||
"runtimeArgs": ["dev"],
|
||||
"runtimeExecutable": "yarn",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"type": "node"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,97 +1,81 @@
|
||||
ARG NODE_ENV=production
|
||||
|
||||
FROM node:22-bookworm-slim@sha256:221ee67425de7a3c11ce4e81e63e50caaec82ede3a7d34599ab20e59d29a0cb5 AS build-stage
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0-noble AS dotnet-build-stage
|
||||
WORKDIR /app
|
||||
COPY packages/fileimport-service/ifc-dotnet .
|
||||
RUN dotnet publish ifc-converter.csproj -c Release -o output/
|
||||
|
||||
ARG NODE_ENV
|
||||
ENV NODE_ENV=${NODE_ENV}
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/runtime:8.0-noble AS runtime
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
WORKDIR /speckle-server
|
||||
|
||||
# install tini
|
||||
# configure tini
|
||||
ARG TINI_VERSION=v0.19.0
|
||||
ENV TINI_VERSION=${TINI_VERSION}
|
||||
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini ./tini
|
||||
RUN chmod +x ./tini
|
||||
|
||||
# install wait
|
||||
ARG WAIT_VERSION=2.8.0
|
||||
ENV WAIT_VERSION=${WAIT_VERSION}
|
||||
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/${WAIT_VERSION}/wait ./wait
|
||||
RUN chmod +x ./wait
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update -y \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
--no-install-recommends \
|
||||
ca-certificates=20240203 \
|
||||
curl=8.5.0-2ubuntu10.6 \
|
||||
gosu=1.17-1ubuntu0.24.04.2 \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
&& curl -fsSL https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini -o /usr/bin/tini \
|
||||
&& chmod +x /usr/bin/tini \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh \
|
||||
&& gosu root:root bash nodesource_setup.sh \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
--no-install-recommends \
|
||||
nodejs \
|
||||
&& echo ">>>>>> NODE Version: $(node --version)" \
|
||||
&& npm install -g corepack@0.30.0 \
|
||||
&& corepack enable \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get remove curl -y \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# download yarn dependencies for building shared libraries
|
||||
COPY .yarnrc.yml .
|
||||
COPY .yarn ./.yarn
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
COPY packages/frontend-2/type-augmentations/stubs ./packages/frontend-2/type-augmentations/stubs/
|
||||
COPY packages/shared/package.json ./packages/shared/
|
||||
COPY packages/fileimport-service/package.json ./packages/fileimport-service/
|
||||
COPY packages/frontend-2/type-augmentations/stubs packages/frontend-2/type-augmentations/stubs/
|
||||
COPY packages/shared/package.json packages/shared/
|
||||
COPY packages/fileimport-service/package.json packages/fileimport-service/
|
||||
|
||||
RUN yarn workspaces focus --all
|
||||
|
||||
# build shared libraries
|
||||
COPY packages/shared ./packages/shared/
|
||||
COPY packages/fileimport-service ./packages/fileimport-service/
|
||||
COPY packages/shared packages/shared/
|
||||
COPY packages/fileimport-service packages/fileimport-service/
|
||||
RUN yarn workspaces foreach -W run build
|
||||
|
||||
# Install python virtual env and python dependencies
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
--no-install-recommends \
|
||||
python3-venv=3.11.2-1+b1 \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& python3 -m venv /venv
|
||||
RUN apt-get update -y \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
--no-install-recommends \
|
||||
python3.12=3.12.3-1ubuntu0.4 \
|
||||
python3-pip=24.0+dfsg-1ubuntu1.1 \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY packages/fileimport-service/requirements.txt /speckle-server/
|
||||
RUN /venv/bin/pip install --disable-pip-version-check --no-cache-dir --requirement /speckle-server/requirements.txt
|
||||
|
||||
FROM node:22-bookworm-slim@sha256:221ee67425de7a3c11ce4e81e63e50caaec82ede3a7d34599ab20e59d29a0cb5 AS dependency-stage
|
||||
# installing just the production dependencies
|
||||
# separate stage to avoid including development dependencies
|
||||
ARG NODE_ENV
|
||||
ENV NODE_ENV=${NODE_ENV}
|
||||
|
||||
WORKDIR /speckle-server
|
||||
COPY .yarnrc.yml .
|
||||
COPY .yarn ./.yarn
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
COPY packages/frontend-2/type-augmentations/stubs ./packages/frontend-2/type-augmentations/stubs/
|
||||
COPY packages/shared/package.json ./packages/shared/
|
||||
COPY packages/fileimport-service/package.json ./packages/fileimport-service/
|
||||
|
||||
WORKDIR /speckle-server/packages/fileimport-service
|
||||
RUN yarn workspaces focus --production
|
||||
|
||||
FROM gcr.io/distroless/python3-debian12:nonroot@sha256:14c62b8925d3bb30319de2f346bde203fe18103a68898284a62db9d4aa54c794 AS python-image
|
||||
|
||||
FROM gcr.io/distroless/nodejs22-debian12:nonroot@sha256:ed26b3ab750110c51d9dbdfd6c697561dc40a01c296460c3494d47b550ef4126 AS distributable-stage
|
||||
RUN pip install --break-system-packages --disable-pip-version-check --no-cache-dir --requirement /speckle-server/requirements.txt
|
||||
|
||||
ARG NODE_ENV
|
||||
ENV NODE_ENV=${NODE_ENV}
|
||||
ARG NODE_BINARY_PATH=/nodejs/bin/node
|
||||
ARG NODE_BINARY_PATH=/usr/bin/node
|
||||
ENV NODE_BINARY_PATH=${NODE_BINARY_PATH}
|
||||
ARG PYTHON_BINARY_PATH=/venv/bin/python3
|
||||
ARG PYTHON_BINARY_PATH=/usr/bin/python3
|
||||
ENV PYTHON_BINARY_PATH=${PYTHON_BINARY_PATH}
|
||||
ARG DOTNET_BINARY_PATH=/usr/bin/dotnet
|
||||
ENV DOTNET_BINARY_PATH=${DOTNET_BINARY_PATH}
|
||||
|
||||
WORKDIR /speckle-server
|
||||
|
||||
COPY --from=python-image / /
|
||||
COPY --from=build-stage /speckle-server/tini /usr/bin/tini
|
||||
COPY --from=build-stage /speckle-server/wait /usr/bin/wait
|
||||
COPY --from=build-stage /speckle-server/packages/shared ./packages/shared
|
||||
COPY --from=build-stage /speckle-server/packages/fileimport-service ./packages/fileimport-service
|
||||
COPY --from=build-stage /venv /venv
|
||||
COPY --from=dependency-stage /speckle-server/node_modules ./node_modules
|
||||
COPY --link --from=dotnet-build-stage /app/output packages/fileimport-service/ifc-dotnet
|
||||
|
||||
WORKDIR /speckle-server/packages/fileimport-service
|
||||
|
||||
# Prefixing PATH with our virtual environment should seek required binaries
|
||||
# from virtual environment first.
|
||||
# Unsetting python home
|
||||
ENV PATH=/venv/bin:${PATH} \
|
||||
PYTHONHOME=
|
||||
|
||||
ENTRYPOINT [ "tini", "--", "/nodejs/bin/node", "--no-experimental-fetch", "src/daemon.js"]
|
||||
ENTRYPOINT [ "tini", "--", "node", "--no-experimental-fetch", "src/daemon.js"]
|
||||
|
||||
@@ -5,6 +5,9 @@ import { baseConfigs, globals } from '../../eslint.config.mjs'
|
||||
*/
|
||||
const configs = [
|
||||
...baseConfigs,
|
||||
{
|
||||
ignores: ['**/ifc/**', '**/obj/**', '**/stl/**']
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"csharpier": {
|
||||
"version": "0.30.1",
|
||||
"commands": [
|
||||
"dotnet-csharpier"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Converter;
|
||||
|
||||
public class ConsoleProgress : IProgress<ProgressArgs>
|
||||
{
|
||||
private readonly TimeSpan DEBOUNCE = TimeSpan.FromSeconds(1);
|
||||
private DateTime _lastTime = DateTime.UtcNow;
|
||||
|
||||
private long _totalBytes;
|
||||
|
||||
public void Report(ProgressArgs value)
|
||||
{
|
||||
if (value.ProgressEvent == ProgressEvent.DownloadBytes)
|
||||
{
|
||||
Interlocked.Add(ref _totalBytes, value.Count);
|
||||
}
|
||||
var now = DateTime.UtcNow;
|
||||
if (now - _lastTime >= DEBOUNCE)
|
||||
{
|
||||
if (value.ProgressEvent == ProgressEvent.DownloadBytes)
|
||||
{
|
||||
Console.WriteLine(value.ProgressEvent + " t " + _totalBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(value.ProgressEvent + " c " + value.Count + " t " + value.Total);
|
||||
}
|
||||
|
||||
_lastTime = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.WebIfc.Importer;
|
||||
|
||||
var filePathArgument = new Argument<string>(name: "filePath");
|
||||
var outputPathArgument = new Argument<string>("outputPath");
|
||||
var streamIdArgument = new Argument<string>("streamId");
|
||||
var commitMessageArgument = new Argument<string>("commitMessage");
|
||||
var modelIdArgument = new Argument<string>("modelId");
|
||||
var regionNameArgument = new Argument<string>("regionName");
|
||||
|
||||
var rootCommand = new RootCommand
|
||||
{
|
||||
filePathArgument,
|
||||
outputPathArgument,
|
||||
streamIdArgument,
|
||||
commitMessageArgument,
|
||||
modelIdArgument,
|
||||
regionNameArgument,
|
||||
};
|
||||
rootCommand.SetHandler(
|
||||
async (filePath, outputPath, streamId, commitMessage, modelId, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var token = Environment.GetEnvironmentVariable("USER_TOKEN").NotNull("USER_TOKEN is missing");
|
||||
var url = Environment.GetEnvironmentVariable("SPECKLE_SERVER_URL") ?? "http://127.0.0.1:3000";
|
||||
var commitId = await Import.Ifc(url, filePath, streamId, modelId, commitMessage, token);
|
||||
File.WriteAllText(outputPath, JsonSerializer.Serialize(new { success = true, commitId }));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
File.WriteAllText(
|
||||
outputPath,
|
||||
JsonSerializer.Serialize(new { success = false, error = e.ToString() })
|
||||
);
|
||||
}
|
||||
},
|
||||
filePathArgument,
|
||||
outputPathArgument,
|
||||
streamIdArgument,
|
||||
commitMessageArgument,
|
||||
modelIdArgument,
|
||||
regionNameArgument
|
||||
);
|
||||
await rootCommand.InvokeAsync(args);
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>Speckle.Converter</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Speckle.WebIfc.Importer" Version="0.0.7" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ifc-converter", "ifc-converter.csproj", "{4D63FBD3-8ABF-4F51-A08F-740B17BDCA28}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{4D63FBD3-8ABF-4F51-A08F-740B17BDCA28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4D63FBD3-8ABF-4F51-A08F-740B17BDCA28}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4D63FBD3-8ABF-4F51-A08F-740B17BDCA28}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4D63FBD3-8ABF-4F51-A08F-740B17BDCA28}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
+10475
-10475
File diff suppressed because it is too large
Load Diff
+2890
-2890
File diff suppressed because it is too large
Load Diff
+49675
-49675
File diff suppressed because it is too large
Load Diff
+52264
-52264
File diff suppressed because it is too large
Load Diff
+52264
-52264
File diff suppressed because it is too large
Load Diff
+647
-647
File diff suppressed because it is too large
Load Diff
+2890
-2890
File diff suppressed because it is too large
Load Diff
+421
-421
@@ -1,421 +1,421 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('IFC2X3.exp'),'2;1');
|
||||
FILE_NAME('C:\\TeklaStructuresModels\\Acis_Sat\\plate_steel_example-tek_1fix.ifc','2006-05-12T10:07:38',('Steel2 macro version:12.0 Build:179423,2.5.2006'),('Structural Designer'),'EXPRESS Data Manager version:20040806','Tekla Structures 12.0','');
|
||||
FILE_SCHEMA(('IFC2X3'));
|
||||
ENDSEC;
|
||||
|
||||
DATA;
|
||||
#1= IFCPERSON('TEKLAAD/lli','Undefined',$,$,$,$,$,$);
|
||||
#3= IFCORGANIZATION($,'Tekla Corporation',$,$,$);
|
||||
#7= IFCPERSONANDORGANIZATION(#1,#3,$);
|
||||
#8= IFCAPPLICATION(#3,'12.0','Tekla Structures','Multi material modeling');
|
||||
#9= IFCOWNERHISTORY(#7,#8,$,.ADDED.,$,$,$,1147417657);
|
||||
#10= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
|
||||
#11= IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
|
||||
#12= IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
|
||||
#13= IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
|
||||
#14= IFCSIUNIT(*,.SOLIDANGLEUNIT.,$,.STERADIAN.);
|
||||
#15= IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
|
||||
#16= IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.);
|
||||
#17= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.DEGREE_CELSIUS.);
|
||||
#18= IFCSIUNIT(*,.LUMINOUSINTENSITYUNIT.,$,.LUMEN.);
|
||||
#19= IFCUNITASSIGNMENT((#10,#11,#12,#13,#14,#15,#16,#17,#18));
|
||||
#21= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#25= IFCDIRECTION((1.,0.,0.));
|
||||
#29= IFCDIRECTION((0.,1.,0.));
|
||||
#33= IFCDIRECTION((0.,0.,1.));
|
||||
#37= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#40= IFCGEOMETRICREPRESENTATIONCONTEXT('Plan','Design',3,1.0000000E-5,#37,$);
|
||||
#43= IFCGEOMETRICREPRESENTATIONCONTEXT('Plan','Sketch',3,1.0000000E-5,#37,$);
|
||||
#46= IFCPROJECT('2gPUQOiNz2FR1H6lWQ8j0k',#9,'PROJ: NAME','Description','Object type','LongName','Phase',(#40,#43),#19);
|
||||
#53= IFCMATERIAL('A36');
|
||||
#56= IFCMATERIAL('A992');
|
||||
#59= IFCMATERIAL('A500-GR.B');
|
||||
#62= IFCPERSON('TEKLAAD/chke','Undefined',$,$,$,$,$,$);
|
||||
#64= IFCPERSONANDORGANIZATION(#62,#3,$);
|
||||
#65= IFCOWNERHISTORY(#64,#8,$,.ADDED.,$,$,$,1147417657);
|
||||
#66= IFCSITE('1_WapmNXfFdOqQ3garaDJn',#65,'Undefined',$,$,$,$,$,.ELEMENT.,$,$,$,$,$);
|
||||
#76= IFCRELAGGREGATES('36yaCMhuT2DxfpF3ieGira',#65,$,$,#46,(#66));
|
||||
#78= IFCBUILDING('2iHnVT4$n9JQE18R_2cJEI',#65,'Undefined',$,$,$,$,$,.ELEMENT.,$,$,$);
|
||||
#88= IFCRELAGGREGATES('0A543zcq1Fdv_lsrHHX8Kv',#65,$,$,#66,(#78));
|
||||
#90= IFCBUILDINGSTOREY('0UguZM0$L8L9OUA114_ZEd',#65,'Undefined',$,$,$,$,$,.ELEMENT.,$);
|
||||
#100= IFCRELAGGREGATES('09$Ux3Y999qBeEKV8wCsfA',#65,$,$,#78,(#90));
|
||||
#102= IFCPOLYLINE((#106,#111,#115,#119,#123,#127,#131));
|
||||
#106= IFCCARTESIANPOINT((0.,318.8494));
|
||||
#111= IFCCARTESIANPOINT((95.250002,318.8494));
|
||||
#115= IFCCARTESIANPOINT((120.65,293.4494));
|
||||
#119= IFCCARTESIANPOINT((120.65,25.4));
|
||||
#123= IFCCARTESIANPOINT((95.250002,0.));
|
||||
#127= IFCCARTESIANPOINT((0.,0.));
|
||||
#131= IFCCARTESIANPOINT((0.,318.8494));
|
||||
#135= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL19.1',#102);
|
||||
#136= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#139= IFCEXTRUDEDAREASOLID(#135,#136,#33,19.1);
|
||||
#142= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#146= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#139));
|
||||
#152= IFCPRODUCTDEFINITIONSHAPE($,$,(#146));
|
||||
#156= IFCCARTESIANPOINT((5240.3064,7493.,-337.7327));
|
||||
#160= IFCDIRECTION((1.,0.,0.));
|
||||
#164= IFCDIRECTION((0.,1.,0.));
|
||||
#168= IFCAXIS2PLACEMENT3D(#156,#160,#164);
|
||||
#171= IFCLOCALPLACEMENT($,#168);
|
||||
#174= IFCPLATE('13c6Dt0003iJ4nCpGmDZ4q',#65,'PLATE','PL19.1',$,#171,#152,$);
|
||||
#193= IFCRELCONTAINEDINSPATIALSTRUCTURE('0119IaMyb8cxCzo7l7Qnbw',#65,$,$,(#174,#268,#350,#434,#521,#633,#720,#808,#895,#982,#1078,#1193,#1385,#1473),#90);
|
||||
#195= IFCSTRUCTURALPROFILEPROPERTIES('PL19.1',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#196= IFCPOLYLINE((#200,#204,#208,#212,#216,#220,#224));
|
||||
#200= IFCCARTESIANPOINT((0.,293.4494));
|
||||
#204= IFCCARTESIANPOINT((25.4,318.8494));
|
||||
#208= IFCCARTESIANPOINT((120.65,318.8494));
|
||||
#212= IFCCARTESIANPOINT((120.65,0.));
|
||||
#216= IFCCARTESIANPOINT((25.4,0.));
|
||||
#220= IFCCARTESIANPOINT((0.,25.4));
|
||||
#224= IFCCARTESIANPOINT((0.,293.4494));
|
||||
#228= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL19.1',#196);
|
||||
#229= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#232= IFCEXTRUDEDAREASOLID(#228,#229,#33,19.1);
|
||||
#235= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#240= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#232));
|
||||
#246= IFCPRODUCTDEFINITIONSHAPE($,$,(#240));
|
||||
#250= IFCCARTESIANPOINT((5240.3064,7626.35,-337.7327));
|
||||
#254= IFCDIRECTION((1.,0.,0.));
|
||||
#258= IFCDIRECTION((0.,1.,0.));
|
||||
#262= IFCAXIS2PLACEMENT3D(#250,#254,#258);
|
||||
#265= IFCLOCALPLACEMENT($,#262);
|
||||
#268= IFCPLATE('13c6Dt0003gZ4nCpGmDZ4q',#65,'PLATE','PL19.1',$,#265,#246,$);
|
||||
#287= IFCPOLYLINE((#291,#295,#299,#303,#307));
|
||||
#291= IFCCARTESIANPOINT((0.,0.));
|
||||
#295= IFCCARTESIANPOINT((0.,318.45248));
|
||||
#299= IFCCARTESIANPOINT((99.695007,318.45248));
|
||||
#303= IFCCARTESIANPOINT((99.695007,0.));
|
||||
#307= IFCCARTESIANPOINT((0.,0.));
|
||||
#311= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL9.5',#287);
|
||||
#312= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#315= IFCEXTRUDEDAREASOLID(#311,#312,#33,9.5);
|
||||
#318= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#322= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#315));
|
||||
#328= IFCPRODUCTDEFINITIONSHAPE($,$,(#322));
|
||||
#332= IFCCARTESIANPOINT((4411.98,7492.5555,-342.9125));
|
||||
#336= IFCDIRECTION((0.,0.,-1.));
|
||||
#340= IFCDIRECTION((0.,1.,0.));
|
||||
#344= IFCAXIS2PLACEMENT3D(#332,#336,#340);
|
||||
#347= IFCLOCALPLACEMENT($,#344);
|
||||
#350= IFCPLATE('13c6Dt0002ip4nCpGmDZ4o',#65,'PLATE','PL9.5',$,#347,#328,$);
|
||||
#370= IFCSTRUCTURALPROFILEPROPERTIES('PL9.5',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#371= IFCPOLYLINE((#375,#379,#383,#387,#391));
|
||||
#375= IFCCARTESIANPOINT((0.,0.));
|
||||
#379= IFCCARTESIANPOINT((0.,318.45248));
|
||||
#383= IFCCARTESIANPOINT((99.695007,318.45248));
|
||||
#387= IFCCARTESIANPOINT((99.695007,0.));
|
||||
#391= IFCCARTESIANPOINT((0.,0.));
|
||||
#395= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL9.5',#371);
|
||||
#396= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#399= IFCEXTRUDEDAREASOLID(#395,#396,#33,9.5);
|
||||
#402= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#406= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#399));
|
||||
#412= IFCPRODUCTDEFINITIONSHAPE($,$,(#406));
|
||||
#416= IFCCARTESIANPOINT((4411.98,7492.5555,-28.5875));
|
||||
#420= IFCDIRECTION((0.,0.,-1.));
|
||||
#424= IFCDIRECTION((0.,1.,0.));
|
||||
#428= IFCAXIS2PLACEMENT3D(#416,#420,#424);
|
||||
#431= IFCLOCALPLACEMENT($,#428);
|
||||
#434= IFCPLATE('13c6Dt0002hp4nCpGmDZ4o',#65,'PLATE','PL9.5',$,#431,#412,$);
|
||||
#453= IFCCARTESIANPOINT((0.,0.));
|
||||
#457= IFCDIRECTION((1.,0.));
|
||||
#461= IFCAXIS2PLACEMENT2D(#453,#457);
|
||||
#464= IFCRECTANGLEPROFILEDEF(.AREA.,$,#461,9.5,234.9);
|
||||
#465= IFCDIRECTION((0.,0.,-1.));
|
||||
#469= IFCDIRECTION((-1.,0.,0.));
|
||||
#473= IFCAXIS2PLACEMENT3D(#21,#469,#33);
|
||||
#476= IFCEXTRUDEDAREASOLID(#464,#473,#465,304.8);
|
||||
#479= IFCCARTESIANPOINT((0.,-117.45,-4.75));
|
||||
#483= IFCBOUNDINGBOX(#479,304.8,234.9,9.5);
|
||||
#486= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#476));
|
||||
#492= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#483));
|
||||
#499= IFCPRODUCTDEFINITIONSHAPE($,$,(#486,#492));
|
||||
#503= IFCCARTESIANPOINT((4581.8225,7496.2,-342.9));
|
||||
#507= IFCDIRECTION((-1.,0.,0.));
|
||||
#511= IFCDIRECTION((0.,0.,1.));
|
||||
#515= IFCAXIS2PLACEMENT3D(#503,#507,#511);
|
||||
#518= IFCLOCALPLACEMENT($,#515);
|
||||
#521= IFCCOLUMN('13c6Dt0002c34nCpGmDZ4o',#65,'PLATE','PL9.5X234.9',$,#518,#499,$);
|
||||
#540= IFCSTRUCTURALPROFILEPROPERTIES('PL9.5X234.9',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#541= IFCCARTESIANPOINT((0.,0.));
|
||||
#545= IFCDIRECTION((1.,0.));
|
||||
#549= IFCAXIS2PLACEMENT2D(#541,#545);
|
||||
#552= IFCISHAPEPROFILEDEF(.AREA.,$,#549,177.41901,402.84399,9.5249996,10.922,17.653);
|
||||
#553= IFCDIRECTION((0.,0.,-1.));
|
||||
#557= IFCDIRECTION((-1.,0.,0.));
|
||||
#561= IFCAXIS2PLACEMENT3D(#21,#557,#33);
|
||||
#564= IFCEXTRUDEDAREASOLID(#552,#561,#553,2278.7992);
|
||||
#567= IFCCARTESIANPOINT((0.,-201.422,-88.709503));
|
||||
#571= IFCBOUNDINGBOX(#567,778.79918,402.84399,177.41901);
|
||||
#574= IFCCARTESIANPOINT((778.79918,301.422,0.));
|
||||
#578= IFCDIRECTION((1.,0.,0.));
|
||||
#582= IFCDIRECTION((0.,0.,-1.));
|
||||
#586= IFCAXIS2PLACEMENT3D(#574,#578,#582);
|
||||
#589= IFCPLANE(#586);
|
||||
#592= IFCHALFSPACESOLID(#589,.F.);
|
||||
#595= IFCBOOLEANCLIPPINGRESULT(.DIFFERENCE.,#564,#592);
|
||||
#598= IFCSHAPEREPRESENTATION(#40,'Body','Clipping',(#595));
|
||||
#604= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#571));
|
||||
#610= IFCPRODUCTDEFINITIONSHAPE($,$,(#598,#604));
|
||||
#614= IFCCARTESIANPOINT((4572.,6688.3563,-201.422));
|
||||
#618= IFCDIRECTION((1.,0.,0.));
|
||||
#623= IFCDIRECTION((0.,1.,0.));
|
||||
#627= IFCAXIS2PLACEMENT3D(#614,#618,#623);
|
||||
#630= IFCLOCALPLACEMENT($,#627);
|
||||
#633= IFCBEAM('13c6Dt0001iZ4nCpGmDZ0v',#65,'BEAM','W16X36',$,#630,#610,$);
|
||||
#652= IFCSTRUCTURALPROFILEPROPERTIES('W16X36',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#653= IFCCARTESIANPOINT((0.,0.));
|
||||
#657= IFCDIRECTION((1.,0.));
|
||||
#661= IFCAXIS2PLACEMENT2D(#653,#657);
|
||||
#664= IFCLSHAPEPROFILEDEF(.AREA.,$,#661,101.6,88.900002,9.5249996,9.525,$,$,$,$);
|
||||
#665= IFCDIRECTION((0.,0.,-1.));
|
||||
#669= IFCDIRECTION((-1.,0.,0.));
|
||||
#673= IFCAXIS2PLACEMENT3D(#21,#669,#33);
|
||||
#676= IFCEXTRUDEDAREASOLID(#664,#673,#665,254.);
|
||||
#679= IFCCARTESIANPOINT((0.,-50.799999,-44.450001));
|
||||
#683= IFCBOUNDINGBOX(#679,254.,101.6,88.900002);
|
||||
#686= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#676));
|
||||
#692= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#683));
|
||||
#698= IFCPRODUCTDEFINITIONSHAPE($,$,(#686,#692));
|
||||
#702= IFCCARTESIANPOINT((4794.758,7562.85,-298.45));
|
||||
#706= IFCDIRECTION((1.,0.,0.));
|
||||
#710= IFCDIRECTION((0.,0.,1.));
|
||||
#714= IFCAXIS2PLACEMENT3D(#702,#706,#710);
|
||||
#717= IFCLOCALPLACEMENT($,#714);
|
||||
#720= IFCCOLUMN('13c6Dt0001GZ4nCpGmDZ0u',#65,'ANGLE','L4X3-1/2X3/8',$,#717,#698,$);
|
||||
#739= IFCSTRUCTURALPROFILEPROPERTIES('L4X3-1/2X3/8',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#740= IFCCARTESIANPOINT((0.,0.));
|
||||
#744= IFCDIRECTION((1.,0.));
|
||||
#748= IFCAXIS2PLACEMENT2D(#740,#744);
|
||||
#752= IFCLSHAPEPROFILEDEF(.AREA.,$,#748,101.6,88.900002,9.5249996,9.525,$,$,$,$);
|
||||
#753= IFCDIRECTION((0.,0.,-1.));
|
||||
#757= IFCDIRECTION((-1.,0.,0.));
|
||||
#761= IFCAXIS2PLACEMENT3D(#21,#757,#33);
|
||||
#764= IFCEXTRUDEDAREASOLID(#752,#761,#753,254.);
|
||||
#767= IFCCARTESIANPOINT((0.,-50.799999,-44.450001));
|
||||
#771= IFCBOUNDINGBOX(#767,254.,101.6,88.900002);
|
||||
#774= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#764));
|
||||
#780= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#771));
|
||||
#786= IFCPRODUCTDEFINITIONSHAPE($,$,(#774,#780));
|
||||
#790= IFCCARTESIANPOINT((4794.758,7677.15,-44.45));
|
||||
#794= IFCDIRECTION((1.,0.,0.));
|
||||
#798= IFCDIRECTION((0.,0.,-1.));
|
||||
#802= IFCAXIS2PLACEMENT3D(#790,#794,#798);
|
||||
#805= IFCLOCALPLACEMENT($,#802);
|
||||
#808= IFCBEAM('13c6Dt0001FZ4nCpGmDZ0u',#65,'ANGLE','L4X3-1/2X3/8',$,#805,#786,$);
|
||||
#827= IFCCARTESIANPOINT((0.,0.));
|
||||
#831= IFCDIRECTION((1.,0.));
|
||||
#835= IFCAXIS2PLACEMENT2D(#827,#831);
|
||||
#838= IFCLSHAPEPROFILEDEF(.AREA.,$,#835,101.6,101.60025,9.5249996,9.525,$,$,$,$);
|
||||
#839= IFCDIRECTION((0.,0.,-1.));
|
||||
#843= IFCDIRECTION((-1.,0.,0.));
|
||||
#847= IFCAXIS2PLACEMENT3D(#21,#843,#33);
|
||||
#850= IFCEXTRUDEDAREASOLID(#838,#847,#839,292.1);
|
||||
#853= IFCCARTESIANPOINT((0.,-50.799999,-50.800125));
|
||||
#857= IFCBOUNDINGBOX(#853,292.1,101.6,101.60025);
|
||||
#860= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#850));
|
||||
#866= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#857));
|
||||
#872= IFCPRODUCTDEFINITIONSHAPE($,$,(#860,#866));
|
||||
#876= IFCCARTESIANPOINT((4915.409,7564.4494,50.799999));
|
||||
#881= IFCDIRECTION((0.,-1.,0.));
|
||||
#885= IFCDIRECTION((1.,0.,0.));
|
||||
#889= IFCAXIS2PLACEMENT3D(#876,#881,#885);
|
||||
#892= IFCLOCALPLACEMENT($,#889);
|
||||
#895= IFCBEAM('13c6Dt0000F34nCpGmDZ0s',#65,'BEAM','L4X4X3/8',$,#892,#872,$);
|
||||
#914= IFCSTRUCTURALPROFILEPROPERTIES('L4X4X3/8',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#915= IFCCARTESIANPOINT((0.,0.));
|
||||
#919= IFCDIRECTION((1.,0.));
|
||||
#923= IFCAXIS2PLACEMENT2D(#915,#919);
|
||||
#926= IFCLSHAPEPROFILEDEF(.AREA.,$,#923,101.6,101.60025,9.5249996,9.525,$,$,$,$);
|
||||
#927= IFCDIRECTION((0.,0.,-1.));
|
||||
#931= IFCDIRECTION((-1.,0.,0.));
|
||||
#935= IFCAXIS2PLACEMENT3D(#21,#931,#33);
|
||||
#938= IFCEXTRUDEDAREASOLID(#926,#935,#927,330.2);
|
||||
#941= IFCCARTESIANPOINT((0.,-50.799999,-50.800125));
|
||||
#945= IFCBOUNDINGBOX(#941,330.2,101.6,101.60025);
|
||||
#948= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#938));
|
||||
#954= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#945));
|
||||
#960= IFCPRODUCTDEFINITIONSHAPE($,$,(#948,#954));
|
||||
#964= IFCCARTESIANPOINT((4801.108,7564.4494,495.301));
|
||||
#968= IFCDIRECTION((0.,-1.,0.));
|
||||
#972= IFCDIRECTION((0.,0.,-1.));
|
||||
#976= IFCAXIS2PLACEMENT3D(#964,#968,#972);
|
||||
#979= IFCLOCALPLACEMENT($,#976);
|
||||
#982= IFCBEAM('13c6Dt00006J4nCpGmDZ0s',#65,'BEAM','L4X4X3/8',$,#979,#960,$);
|
||||
#1001= IFCPOLYLINE((#1006,#1010,#1014,#1018,#1022,#1026));
|
||||
#1006= IFCCARTESIANPOINT((0.,0.));
|
||||
#1010= IFCCARTESIANPOINT((-0.001,486.84845));
|
||||
#1014= IFCCARTESIANPOINT((369.07135,486.84845));
|
||||
#1018= IFCCARTESIANPOINT((512.75543,343.16437));
|
||||
#1022= IFCCARTESIANPOINT((512.75543,0.));
|
||||
#1026= IFCCARTESIANPOINT((0.,0.));
|
||||
#1030= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL9.5',#1001);
|
||||
#1031= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#1034= IFCEXTRUDEDAREASOLID(#1030,#1031,#33,9.5);
|
||||
#1037= IFCCARTESIANPOINT((-0.0010532137,0.,-4.75));
|
||||
#1041= IFCBOUNDINGBOX(#1037,486.8495,512.75614,9.5);
|
||||
#1044= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1034));
|
||||
#1050= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1041));
|
||||
#1056= IFCPRODUCTDEFINITIONSHAPE($,$,(#1044,#1050));
|
||||
#1060= IFCCARTESIANPOINT((4763.008,7615.262,12.701));
|
||||
#1064= IFCDIRECTION((0.,1.,0.));
|
||||
#1068= IFCDIRECTION((0.,0.,1.));
|
||||
#1072= IFCAXIS2PLACEMENT3D(#1060,#1064,#1068);
|
||||
#1075= IFCLOCALPLACEMENT($,#1072);
|
||||
#1078= IFCPLATE('13c6Dt00005J4nCpGmDZ0s',#65,'PLATE','PL9.5',$,#1075,#1056,$);
|
||||
#1097= IFCCARTESIANPOINT((0.,0.));
|
||||
#1101= IFCDIRECTION((1.,0.));
|
||||
#1105= IFCAXIS2PLACEMENT2D(#1097,#1101);
|
||||
#1108= IFCRECTANGLEHOLLOWPROFILEDEF(.AREA.,$,#1105,152.39999,152.39999,9.5249996,9.5250004,19.05);
|
||||
#1109= IFCDIRECTION((0.,0.,-1.));
|
||||
#1113= IFCDIRECTION((-1.,0.,0.));
|
||||
#1117= IFCAXIS2PLACEMENT3D(#1130,#1113,#33);
|
||||
#1120= IFCEXTRUDEDAREASOLID(#1108,#1117,#1109,2177.4228);
|
||||
#1123= IFCCARTESIANPOINT((-1.2141754E-10,-76.199997,-76.199997));
|
||||
#1127= IFCBOUNDINGBOX(#1123,677.4228,152.39999,152.39999);
|
||||
#1130= IFCCARTESIANPOINT((-1500.,0.,0.));
|
||||
#1135= IFCCARTESIANPOINT((-1.0913936E-10,-9.0949470E-12,4.7379999));
|
||||
#1139= IFCDIRECTION((-1.,-1.6052583E-13,0.));
|
||||
#1143= IFCDIRECTION((-1.6052583E-13,1.,0.));
|
||||
#1147= IFCAXIS2PLACEMENT3D(#1135,#1139,#1143);
|
||||
#1150= IFCPLANE(#1147);
|
||||
#1153= IFCHALFSPACESOLID(#1150,.F.);
|
||||
#1156= IFCBOOLEANCLIPPINGRESULT(.DIFFERENCE.,#1120,#1153);
|
||||
#1159= IFCSHAPEREPRESENTATION(#40,'Body','Clipping',(#1156));
|
||||
#1165= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1127));
|
||||
#1171= IFCPRODUCTDEFINITIONSHAPE($,$,(#1159,#1165));
|
||||
#1175= IFCCARTESIANPOINT((5007.3895,7620.,282.98953));
|
||||
#1179= IFCDIRECTION((0.,-1.,0.));
|
||||
#1183= IFCDIRECTION((0.70710678,0.,0.70710678));
|
||||
#1187= IFCAXIS2PLACEMENT3D(#1175,#1179,#1183);
|
||||
#1190= IFCLOCALPLACEMENT($,#1187);
|
||||
#1193= IFCBEAM('13c6Dt00003p4nCpGmDZ0r',#65,'BRACE','TS6X6X3/8',$,#1190,#1171,$);
|
||||
#1212= IFCCARTESIANPOINT((5153.6546,7620.012,307.34937));
|
||||
#1216= IFCDIRECTION((0.,-1.,0.));
|
||||
#1220= IFCDIRECTION((-0.70710678,0.,0.70710678));
|
||||
#1224= IFCAXIS2PLACEMENT3D(#1212,#1216,#1220);
|
||||
#1227= IFCLOCALPLACEMENT($,#1224);
|
||||
#1230= IFCCARTESIANPOINT((0.,0.));
|
||||
#1234= IFCDIRECTION((1.,0.));
|
||||
#1238= IFCAXIS2PLACEMENT2D(#1230,#1234);
|
||||
#1241= IFCRECTANGLEPROFILEDEF(.AREA.,$,#1238,9.5,266.7);
|
||||
#1242= IFCDIRECTION((0.,0.,-1.));
|
||||
#1246= IFCDIRECTION((-1.,0.,0.));
|
||||
#1250= IFCAXIS2PLACEMENT3D(#21,#1246,#33);
|
||||
#1253= IFCEXTRUDEDAREASOLID(#1241,#1250,#1242,172.39997);
|
||||
#1256= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1253));
|
||||
#1263= IFCPRODUCTDEFINITIONSHAPE($,$,(#1256));
|
||||
#1267= IFCOPENINGELEMENT('13c6Dt0000Ip4nCpGmDZ0s',#65,$,$,$,#1227,#1263,$);
|
||||
#1288= IFCRELVOIDSELEMENT('3gz_1FcMHExhEMJzH8zl$M',#65,$,$,#1193,#1267);
|
||||
#1289= IFCSTRUCTURALPROFILEPROPERTIES('TS6X6X3/8',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#1290= IFCCARTESIANPOINT((0.,0.));
|
||||
#1294= IFCDIRECTION((1.,0.));
|
||||
#1298= IFCAXIS2PLACEMENT2D(#1290,#1294);
|
||||
#1301= IFCISHAPEPROFILEDEF(.AREA.,$,#1298,254.88901,356.616,12.7,18.288,21.3995);
|
||||
#1302= IFCDIRECTION((0.,0.,-1.));
|
||||
#1306= IFCDIRECTION((-1.,0.,0.));
|
||||
#1310= IFCAXIS2PLACEMENT3D(#1323,#1306,#33);
|
||||
#1313= IFCEXTRUDEDAREASOLID(#1301,#1310,#1302,2768.2595);
|
||||
#1316= IFCCARTESIANPOINT((0.,-178.308,-127.4445));
|
||||
#1320= IFCBOUNDINGBOX(#1316,1268.2595,356.616,254.88901);
|
||||
#1323= IFCCARTESIANPOINT((-1500.,0.,0.));
|
||||
#1327= IFCCARTESIANPOINT((0.,278.308,0.));
|
||||
#1331= IFCDIRECTION((-1.,0.,0.));
|
||||
#1335= IFCDIRECTION((0.,0.,1.));
|
||||
#1339= IFCAXIS2PLACEMENT3D(#1327,#1331,#1335);
|
||||
#1342= IFCPLANE(#1339);
|
||||
#1345= IFCHALFSPACESOLID(#1342,.F.);
|
||||
#1348= IFCBOOLEANCLIPPINGRESULT(.DIFFERENCE.,#1313,#1345);
|
||||
#1351= IFCSHAPEREPRESENTATION(#40,'Body','Clipping',(#1348));
|
||||
#1357= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1320));
|
||||
#1363= IFCPRODUCTDEFINITIONSHAPE($,$,(#1351,#1357));
|
||||
#1367= IFCCARTESIANPOINT((4763.008,7620.,-178.308));
|
||||
#1371= IFCDIRECTION((0.,-1.,0.));
|
||||
#1375= IFCDIRECTION((1.,0.,-7.2832534E-15));
|
||||
#1379= IFCAXIS2PLACEMENT3D(#1367,#1371,#1375);
|
||||
#1382= IFCLOCALPLACEMENT($,#1379);
|
||||
#1385= IFCBEAM('13c6Dt00002p4nCpGmDZ0r',#65,'BEAM','W14X68',$,#1382,#1363,$);
|
||||
#1405= IFCSTRUCTURALPROFILEPROPERTIES('W14X68',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#1406= IFCCARTESIANPOINT((0.,0.));
|
||||
#1410= IFCDIRECTION((1.,0.));
|
||||
#1414= IFCAXIS2PLACEMENT2D(#1406,#1410);
|
||||
#1417= IFCISHAPEPROFILEDEF(.AREA.,$,#1414,254.88901,356.616,12.7,18.288,21.3995);
|
||||
#1418= IFCDIRECTION((0.,0.,-1.));
|
||||
#1422= IFCDIRECTION((-1.,0.,0.));
|
||||
#1426= IFCAXIS2PLACEMENT3D(#21,#1422,#33);
|
||||
#1429= IFCEXTRUDEDAREASOLID(#1417,#1426,#1418,1524.);
|
||||
#1432= IFCCARTESIANPOINT((0.,-178.308,-127.4445));
|
||||
#1436= IFCBOUNDINGBOX(#1432,1524.,356.616,254.88901);
|
||||
#1439= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1429));
|
||||
#1445= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1436));
|
||||
#1451= IFCPRODUCTDEFINITIONSHAPE($,$,(#1439,#1445));
|
||||
#1455= IFCCARTESIANPOINT((4572.,7620.,-609.6));
|
||||
#1459= IFCDIRECTION((0.,-1.,0.));
|
||||
#1463= IFCDIRECTION((7.1613756E-15,0.,1.));
|
||||
#1467= IFCAXIS2PLACEMENT3D(#1455,#1459,#1463);
|
||||
#1470= IFCLOCALPLACEMENT($,#1467);
|
||||
#1473= IFCCOLUMN('13c6Dt00001p4nCpGmDZ0q',#65,'COLUMN','W14X68',$,#1470,#1451,$);
|
||||
#1492= IFCCARTESIANPOINT((4661.154,7490.05,308.83104));
|
||||
#1496= IFCDIRECTION((0.,1.,0.));
|
||||
#1500= IFCDIRECTION((-1.,0.,7.0134555E-15));
|
||||
#1504= IFCAXIS2PLACEMENT3D(#1492,#1496,#1500);
|
||||
#1507= IFCLOCALPLACEMENT($,#1504);
|
||||
#1510= IFCPOLYLINE((#1514,#1519,#1523,#1527,#1531,#1535,#1539,#1543,#1547,#1551,#1555,#1559,#1563,#1567,#1571,#1575,#1579,#1583,#1587,#1591,#1595,#1599,#1603,#1607,#1611,#1615,#1619,#1623,#1627,#1631,#1635,#1639,#1643,#1648,#1652,#1656,#1660));
|
||||
#1514= IFCCARTESIANPOINT((0.38429439,16.098194));
|
||||
#1519= IFCCARTESIANPOINT((0.,20.));
|
||||
#1523= IFCCARTESIANPOINT((0.,336.616));
|
||||
#1527= IFCCARTESIANPOINT((0.38429439,340.5178));
|
||||
#1531= IFCCARTESIANPOINT((1.5224093,344.26967));
|
||||
#1535= IFCCARTESIANPOINT((3.3706078,347.7274));
|
||||
#1539= IFCCARTESIANPOINT((5.8578644,350.75813));
|
||||
#1543= IFCCARTESIANPOINT((8.8885953,353.24539));
|
||||
#1547= IFCCARTESIANPOINT((12.346331,355.09359));
|
||||
#1551= IFCCARTESIANPOINT((16.098194,356.2317));
|
||||
#1555= IFCCARTESIANPOINT((20.,356.616));
|
||||
#1559= IFCCARTESIANPOINT((158.308,356.616));
|
||||
#1563= IFCCARTESIANPOINT((162.20981,356.2317));
|
||||
#1567= IFCCARTESIANPOINT((165.96167,355.09359));
|
||||
#1571= IFCCARTESIANPOINT((169.4194,353.24539));
|
||||
#1575= IFCCARTESIANPOINT((172.45013,350.75813));
|
||||
#1579= IFCCARTESIANPOINT((174.93739,347.7274));
|
||||
#1583= IFCCARTESIANPOINT((176.78559,344.26967));
|
||||
#1587= IFCCARTESIANPOINT((177.9237,340.5178));
|
||||
#1591= IFCCARTESIANPOINT((178.308,336.616));
|
||||
#1595= IFCCARTESIANPOINT((178.308,20.));
|
||||
#1599= IFCCARTESIANPOINT((177.9237,16.098194));
|
||||
#1603= IFCCARTESIANPOINT((176.78559,12.346331));
|
||||
#1607= IFCCARTESIANPOINT((174.93739,8.8885953));
|
||||
#1611= IFCCARTESIANPOINT((172.45013,5.8578644));
|
||||
#1615= IFCCARTESIANPOINT((169.4194,3.3706078));
|
||||
#1619= IFCCARTESIANPOINT((165.96167,1.5224093));
|
||||
#1623= IFCCARTESIANPOINT((162.20981,0.38429439));
|
||||
#1627= IFCCARTESIANPOINT((158.308,0.));
|
||||
#1631= IFCCARTESIANPOINT((20.,0.));
|
||||
#1635= IFCCARTESIANPOINT((16.098194,0.38429439));
|
||||
#1639= IFCCARTESIANPOINT((12.346331,1.5224093));
|
||||
#1643= IFCCARTESIANPOINT((8.8885953,3.3706078));
|
||||
#1648= IFCCARTESIANPOINT((5.8578644,5.8578644));
|
||||
#1652= IFCCARTESIANPOINT((3.3706078,8.8885953));
|
||||
#1656= IFCCARTESIANPOINT((1.5224093,12.346331));
|
||||
#1660= IFCCARTESIANPOINT((0.38429439,16.098194));
|
||||
#1664= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL259.9',#1510);
|
||||
#1665= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#1668= IFCEXTRUDEDAREASOLID(#1664,#1665,#33,259.9);
|
||||
#1671= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1668));
|
||||
#1677= IFCPRODUCTDEFINITIONSHAPE($,$,(#1671));
|
||||
#1681= IFCOPENINGELEMENT('13c6Dt0002m34nCpGmDZ4o',#65,$,$,$,#1507,#1677,$);
|
||||
#1702= IFCRELVOIDSELEMENT('2WI1gpRhj1dwB5Pft5vGvO',#65,$,$,#1473,#1681);
|
||||
#1703= IFCRELASSOCIATESMATERIAL('1GOaWfy957kRCtp2R28$a9',#9,$,$,(#1193),#59);
|
||||
#1705= IFCRELASSOCIATESMATERIAL('3Vqwp2DmT3TeKWHZyFzO7N',#9,$,$,(#1473,#1385,#633),#56);
|
||||
#1707= IFCRELASSOCIATESMATERIAL('0SMrmHf_5BRAYDVih2VNTN',#9,$,$,(#1078,#982,#895,#808,#720,#521,#434,#350,#268,#174),#53);
|
||||
#1709= IFCRELASSOCIATESPROFILEPROPERTIES('2mzP0x3pj2GwUm7Wy7Xfs0',#9,$,$,(#633),#652,$,$);
|
||||
#1711= IFCRELASSOCIATESPROFILEPROPERTIES('1iUJLoCsfD6ug6keUDRGVW',#9,$,$,(#808),#739,$,$);
|
||||
#1713= IFCRELASSOCIATESPROFILEPROPERTIES('1LidMEK_vEQuG8Js1MD1ND',#9,$,$,(#895,#982),#914,$,$);
|
||||
#1715= IFCRELASSOCIATESPROFILEPROPERTIES('22dp9maVb0sAxZG8F2xTIR',#9,$,$,(#1193),#1289,$,$);
|
||||
#1717= IFCRELASSOCIATESPROFILEPROPERTIES('36LPEeuXn6VgX3frK_urjy',#9,$,$,(#1385),#1405,$,$);
|
||||
ENDSEC;
|
||||
|
||||
END-ISO-10303-21;
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('IFC2X3.exp'),'2;1');
|
||||
FILE_NAME('C:\\TeklaStructuresModels\\Acis_Sat\\plate_steel_example-tek_1fix.ifc','2006-05-12T10:07:38',('Steel2 macro version:12.0 Build:179423,2.5.2006'),('Structural Designer'),'EXPRESS Data Manager version:20040806','Tekla Structures 12.0','');
|
||||
FILE_SCHEMA(('IFC2X3'));
|
||||
ENDSEC;
|
||||
|
||||
DATA;
|
||||
#1= IFCPERSON('TEKLAAD/lli','Undefined',$,$,$,$,$,$);
|
||||
#3= IFCORGANIZATION($,'Tekla Corporation',$,$,$);
|
||||
#7= IFCPERSONANDORGANIZATION(#1,#3,$);
|
||||
#8= IFCAPPLICATION(#3,'12.0','Tekla Structures','Multi material modeling');
|
||||
#9= IFCOWNERHISTORY(#7,#8,$,.ADDED.,$,$,$,1147417657);
|
||||
#10= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
|
||||
#11= IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
|
||||
#12= IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
|
||||
#13= IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
|
||||
#14= IFCSIUNIT(*,.SOLIDANGLEUNIT.,$,.STERADIAN.);
|
||||
#15= IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
|
||||
#16= IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.);
|
||||
#17= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.DEGREE_CELSIUS.);
|
||||
#18= IFCSIUNIT(*,.LUMINOUSINTENSITYUNIT.,$,.LUMEN.);
|
||||
#19= IFCUNITASSIGNMENT((#10,#11,#12,#13,#14,#15,#16,#17,#18));
|
||||
#21= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#25= IFCDIRECTION((1.,0.,0.));
|
||||
#29= IFCDIRECTION((0.,1.,0.));
|
||||
#33= IFCDIRECTION((0.,0.,1.));
|
||||
#37= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#40= IFCGEOMETRICREPRESENTATIONCONTEXT('Plan','Design',3,1.0000000E-5,#37,$);
|
||||
#43= IFCGEOMETRICREPRESENTATIONCONTEXT('Plan','Sketch',3,1.0000000E-5,#37,$);
|
||||
#46= IFCPROJECT('2gPUQOiNz2FR1H6lWQ8j0k',#9,'PROJ: NAME','Description','Object type','LongName','Phase',(#40,#43),#19);
|
||||
#53= IFCMATERIAL('A36');
|
||||
#56= IFCMATERIAL('A992');
|
||||
#59= IFCMATERIAL('A500-GR.B');
|
||||
#62= IFCPERSON('TEKLAAD/chke','Undefined',$,$,$,$,$,$);
|
||||
#64= IFCPERSONANDORGANIZATION(#62,#3,$);
|
||||
#65= IFCOWNERHISTORY(#64,#8,$,.ADDED.,$,$,$,1147417657);
|
||||
#66= IFCSITE('1_WapmNXfFdOqQ3garaDJn',#65,'Undefined',$,$,$,$,$,.ELEMENT.,$,$,$,$,$);
|
||||
#76= IFCRELAGGREGATES('36yaCMhuT2DxfpF3ieGira',#65,$,$,#46,(#66));
|
||||
#78= IFCBUILDING('2iHnVT4$n9JQE18R_2cJEI',#65,'Undefined',$,$,$,$,$,.ELEMENT.,$,$,$);
|
||||
#88= IFCRELAGGREGATES('0A543zcq1Fdv_lsrHHX8Kv',#65,$,$,#66,(#78));
|
||||
#90= IFCBUILDINGSTOREY('0UguZM0$L8L9OUA114_ZEd',#65,'Undefined',$,$,$,$,$,.ELEMENT.,$);
|
||||
#100= IFCRELAGGREGATES('09$Ux3Y999qBeEKV8wCsfA',#65,$,$,#78,(#90));
|
||||
#102= IFCPOLYLINE((#106,#111,#115,#119,#123,#127,#131));
|
||||
#106= IFCCARTESIANPOINT((0.,318.8494));
|
||||
#111= IFCCARTESIANPOINT((95.250002,318.8494));
|
||||
#115= IFCCARTESIANPOINT((120.65,293.4494));
|
||||
#119= IFCCARTESIANPOINT((120.65,25.4));
|
||||
#123= IFCCARTESIANPOINT((95.250002,0.));
|
||||
#127= IFCCARTESIANPOINT((0.,0.));
|
||||
#131= IFCCARTESIANPOINT((0.,318.8494));
|
||||
#135= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL19.1',#102);
|
||||
#136= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#139= IFCEXTRUDEDAREASOLID(#135,#136,#33,19.1);
|
||||
#142= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#146= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#139));
|
||||
#152= IFCPRODUCTDEFINITIONSHAPE($,$,(#146));
|
||||
#156= IFCCARTESIANPOINT((5240.3064,7493.,-337.7327));
|
||||
#160= IFCDIRECTION((1.,0.,0.));
|
||||
#164= IFCDIRECTION((0.,1.,0.));
|
||||
#168= IFCAXIS2PLACEMENT3D(#156,#160,#164);
|
||||
#171= IFCLOCALPLACEMENT($,#168);
|
||||
#174= IFCPLATE('13c6Dt0003iJ4nCpGmDZ4q',#65,'PLATE','PL19.1',$,#171,#152,$);
|
||||
#193= IFCRELCONTAINEDINSPATIALSTRUCTURE('0119IaMyb8cxCzo7l7Qnbw',#65,$,$,(#174,#268,#350,#434,#521,#633,#720,#808,#895,#982,#1078,#1193,#1385,#1473),#90);
|
||||
#195= IFCSTRUCTURALPROFILEPROPERTIES('PL19.1',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#196= IFCPOLYLINE((#200,#204,#208,#212,#216,#220,#224));
|
||||
#200= IFCCARTESIANPOINT((0.,293.4494));
|
||||
#204= IFCCARTESIANPOINT((25.4,318.8494));
|
||||
#208= IFCCARTESIANPOINT((120.65,318.8494));
|
||||
#212= IFCCARTESIANPOINT((120.65,0.));
|
||||
#216= IFCCARTESIANPOINT((25.4,0.));
|
||||
#220= IFCCARTESIANPOINT((0.,25.4));
|
||||
#224= IFCCARTESIANPOINT((0.,293.4494));
|
||||
#228= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL19.1',#196);
|
||||
#229= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#232= IFCEXTRUDEDAREASOLID(#228,#229,#33,19.1);
|
||||
#235= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#240= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#232));
|
||||
#246= IFCPRODUCTDEFINITIONSHAPE($,$,(#240));
|
||||
#250= IFCCARTESIANPOINT((5240.3064,7626.35,-337.7327));
|
||||
#254= IFCDIRECTION((1.,0.,0.));
|
||||
#258= IFCDIRECTION((0.,1.,0.));
|
||||
#262= IFCAXIS2PLACEMENT3D(#250,#254,#258);
|
||||
#265= IFCLOCALPLACEMENT($,#262);
|
||||
#268= IFCPLATE('13c6Dt0003gZ4nCpGmDZ4q',#65,'PLATE','PL19.1',$,#265,#246,$);
|
||||
#287= IFCPOLYLINE((#291,#295,#299,#303,#307));
|
||||
#291= IFCCARTESIANPOINT((0.,0.));
|
||||
#295= IFCCARTESIANPOINT((0.,318.45248));
|
||||
#299= IFCCARTESIANPOINT((99.695007,318.45248));
|
||||
#303= IFCCARTESIANPOINT((99.695007,0.));
|
||||
#307= IFCCARTESIANPOINT((0.,0.));
|
||||
#311= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL9.5',#287);
|
||||
#312= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#315= IFCEXTRUDEDAREASOLID(#311,#312,#33,9.5);
|
||||
#318= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#322= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#315));
|
||||
#328= IFCPRODUCTDEFINITIONSHAPE($,$,(#322));
|
||||
#332= IFCCARTESIANPOINT((4411.98,7492.5555,-342.9125));
|
||||
#336= IFCDIRECTION((0.,0.,-1.));
|
||||
#340= IFCDIRECTION((0.,1.,0.));
|
||||
#344= IFCAXIS2PLACEMENT3D(#332,#336,#340);
|
||||
#347= IFCLOCALPLACEMENT($,#344);
|
||||
#350= IFCPLATE('13c6Dt0002ip4nCpGmDZ4o',#65,'PLATE','PL9.5',$,#347,#328,$);
|
||||
#370= IFCSTRUCTURALPROFILEPROPERTIES('PL9.5',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#371= IFCPOLYLINE((#375,#379,#383,#387,#391));
|
||||
#375= IFCCARTESIANPOINT((0.,0.));
|
||||
#379= IFCCARTESIANPOINT((0.,318.45248));
|
||||
#383= IFCCARTESIANPOINT((99.695007,318.45248));
|
||||
#387= IFCCARTESIANPOINT((99.695007,0.));
|
||||
#391= IFCCARTESIANPOINT((0.,0.));
|
||||
#395= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL9.5',#371);
|
||||
#396= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#399= IFCEXTRUDEDAREASOLID(#395,#396,#33,9.5);
|
||||
#402= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#406= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#399));
|
||||
#412= IFCPRODUCTDEFINITIONSHAPE($,$,(#406));
|
||||
#416= IFCCARTESIANPOINT((4411.98,7492.5555,-28.5875));
|
||||
#420= IFCDIRECTION((0.,0.,-1.));
|
||||
#424= IFCDIRECTION((0.,1.,0.));
|
||||
#428= IFCAXIS2PLACEMENT3D(#416,#420,#424);
|
||||
#431= IFCLOCALPLACEMENT($,#428);
|
||||
#434= IFCPLATE('13c6Dt0002hp4nCpGmDZ4o',#65,'PLATE','PL9.5',$,#431,#412,$);
|
||||
#453= IFCCARTESIANPOINT((0.,0.));
|
||||
#457= IFCDIRECTION((1.,0.));
|
||||
#461= IFCAXIS2PLACEMENT2D(#453,#457);
|
||||
#464= IFCRECTANGLEPROFILEDEF(.AREA.,$,#461,9.5,234.9);
|
||||
#465= IFCDIRECTION((0.,0.,-1.));
|
||||
#469= IFCDIRECTION((-1.,0.,0.));
|
||||
#473= IFCAXIS2PLACEMENT3D(#21,#469,#33);
|
||||
#476= IFCEXTRUDEDAREASOLID(#464,#473,#465,304.8);
|
||||
#479= IFCCARTESIANPOINT((0.,-117.45,-4.75));
|
||||
#483= IFCBOUNDINGBOX(#479,304.8,234.9,9.5);
|
||||
#486= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#476));
|
||||
#492= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#483));
|
||||
#499= IFCPRODUCTDEFINITIONSHAPE($,$,(#486,#492));
|
||||
#503= IFCCARTESIANPOINT((4581.8225,7496.2,-342.9));
|
||||
#507= IFCDIRECTION((-1.,0.,0.));
|
||||
#511= IFCDIRECTION((0.,0.,1.));
|
||||
#515= IFCAXIS2PLACEMENT3D(#503,#507,#511);
|
||||
#518= IFCLOCALPLACEMENT($,#515);
|
||||
#521= IFCCOLUMN('13c6Dt0002c34nCpGmDZ4o',#65,'PLATE','PL9.5X234.9',$,#518,#499,$);
|
||||
#540= IFCSTRUCTURALPROFILEPROPERTIES('PL9.5X234.9',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#541= IFCCARTESIANPOINT((0.,0.));
|
||||
#545= IFCDIRECTION((1.,0.));
|
||||
#549= IFCAXIS2PLACEMENT2D(#541,#545);
|
||||
#552= IFCISHAPEPROFILEDEF(.AREA.,$,#549,177.41901,402.84399,9.5249996,10.922,17.653);
|
||||
#553= IFCDIRECTION((0.,0.,-1.));
|
||||
#557= IFCDIRECTION((-1.,0.,0.));
|
||||
#561= IFCAXIS2PLACEMENT3D(#21,#557,#33);
|
||||
#564= IFCEXTRUDEDAREASOLID(#552,#561,#553,2278.7992);
|
||||
#567= IFCCARTESIANPOINT((0.,-201.422,-88.709503));
|
||||
#571= IFCBOUNDINGBOX(#567,778.79918,402.84399,177.41901);
|
||||
#574= IFCCARTESIANPOINT((778.79918,301.422,0.));
|
||||
#578= IFCDIRECTION((1.,0.,0.));
|
||||
#582= IFCDIRECTION((0.,0.,-1.));
|
||||
#586= IFCAXIS2PLACEMENT3D(#574,#578,#582);
|
||||
#589= IFCPLANE(#586);
|
||||
#592= IFCHALFSPACESOLID(#589,.F.);
|
||||
#595= IFCBOOLEANCLIPPINGRESULT(.DIFFERENCE.,#564,#592);
|
||||
#598= IFCSHAPEREPRESENTATION(#40,'Body','Clipping',(#595));
|
||||
#604= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#571));
|
||||
#610= IFCPRODUCTDEFINITIONSHAPE($,$,(#598,#604));
|
||||
#614= IFCCARTESIANPOINT((4572.,6688.3563,-201.422));
|
||||
#618= IFCDIRECTION((1.,0.,0.));
|
||||
#623= IFCDIRECTION((0.,1.,0.));
|
||||
#627= IFCAXIS2PLACEMENT3D(#614,#618,#623);
|
||||
#630= IFCLOCALPLACEMENT($,#627);
|
||||
#633= IFCBEAM('13c6Dt0001iZ4nCpGmDZ0v',#65,'BEAM','W16X36',$,#630,#610,$);
|
||||
#652= IFCSTRUCTURALPROFILEPROPERTIES('W16X36',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#653= IFCCARTESIANPOINT((0.,0.));
|
||||
#657= IFCDIRECTION((1.,0.));
|
||||
#661= IFCAXIS2PLACEMENT2D(#653,#657);
|
||||
#664= IFCLSHAPEPROFILEDEF(.AREA.,$,#661,101.6,88.900002,9.5249996,9.525,$,$,$,$);
|
||||
#665= IFCDIRECTION((0.,0.,-1.));
|
||||
#669= IFCDIRECTION((-1.,0.,0.));
|
||||
#673= IFCAXIS2PLACEMENT3D(#21,#669,#33);
|
||||
#676= IFCEXTRUDEDAREASOLID(#664,#673,#665,254.);
|
||||
#679= IFCCARTESIANPOINT((0.,-50.799999,-44.450001));
|
||||
#683= IFCBOUNDINGBOX(#679,254.,101.6,88.900002);
|
||||
#686= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#676));
|
||||
#692= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#683));
|
||||
#698= IFCPRODUCTDEFINITIONSHAPE($,$,(#686,#692));
|
||||
#702= IFCCARTESIANPOINT((4794.758,7562.85,-298.45));
|
||||
#706= IFCDIRECTION((1.,0.,0.));
|
||||
#710= IFCDIRECTION((0.,0.,1.));
|
||||
#714= IFCAXIS2PLACEMENT3D(#702,#706,#710);
|
||||
#717= IFCLOCALPLACEMENT($,#714);
|
||||
#720= IFCCOLUMN('13c6Dt0001GZ4nCpGmDZ0u',#65,'ANGLE','L4X3-1/2X3/8',$,#717,#698,$);
|
||||
#739= IFCSTRUCTURALPROFILEPROPERTIES('L4X3-1/2X3/8',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#740= IFCCARTESIANPOINT((0.,0.));
|
||||
#744= IFCDIRECTION((1.,0.));
|
||||
#748= IFCAXIS2PLACEMENT2D(#740,#744);
|
||||
#752= IFCLSHAPEPROFILEDEF(.AREA.,$,#748,101.6,88.900002,9.5249996,9.525,$,$,$,$);
|
||||
#753= IFCDIRECTION((0.,0.,-1.));
|
||||
#757= IFCDIRECTION((-1.,0.,0.));
|
||||
#761= IFCAXIS2PLACEMENT3D(#21,#757,#33);
|
||||
#764= IFCEXTRUDEDAREASOLID(#752,#761,#753,254.);
|
||||
#767= IFCCARTESIANPOINT((0.,-50.799999,-44.450001));
|
||||
#771= IFCBOUNDINGBOX(#767,254.,101.6,88.900002);
|
||||
#774= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#764));
|
||||
#780= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#771));
|
||||
#786= IFCPRODUCTDEFINITIONSHAPE($,$,(#774,#780));
|
||||
#790= IFCCARTESIANPOINT((4794.758,7677.15,-44.45));
|
||||
#794= IFCDIRECTION((1.,0.,0.));
|
||||
#798= IFCDIRECTION((0.,0.,-1.));
|
||||
#802= IFCAXIS2PLACEMENT3D(#790,#794,#798);
|
||||
#805= IFCLOCALPLACEMENT($,#802);
|
||||
#808= IFCBEAM('13c6Dt0001FZ4nCpGmDZ0u',#65,'ANGLE','L4X3-1/2X3/8',$,#805,#786,$);
|
||||
#827= IFCCARTESIANPOINT((0.,0.));
|
||||
#831= IFCDIRECTION((1.,0.));
|
||||
#835= IFCAXIS2PLACEMENT2D(#827,#831);
|
||||
#838= IFCLSHAPEPROFILEDEF(.AREA.,$,#835,101.6,101.60025,9.5249996,9.525,$,$,$,$);
|
||||
#839= IFCDIRECTION((0.,0.,-1.));
|
||||
#843= IFCDIRECTION((-1.,0.,0.));
|
||||
#847= IFCAXIS2PLACEMENT3D(#21,#843,#33);
|
||||
#850= IFCEXTRUDEDAREASOLID(#838,#847,#839,292.1);
|
||||
#853= IFCCARTESIANPOINT((0.,-50.799999,-50.800125));
|
||||
#857= IFCBOUNDINGBOX(#853,292.1,101.6,101.60025);
|
||||
#860= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#850));
|
||||
#866= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#857));
|
||||
#872= IFCPRODUCTDEFINITIONSHAPE($,$,(#860,#866));
|
||||
#876= IFCCARTESIANPOINT((4915.409,7564.4494,50.799999));
|
||||
#881= IFCDIRECTION((0.,-1.,0.));
|
||||
#885= IFCDIRECTION((1.,0.,0.));
|
||||
#889= IFCAXIS2PLACEMENT3D(#876,#881,#885);
|
||||
#892= IFCLOCALPLACEMENT($,#889);
|
||||
#895= IFCBEAM('13c6Dt0000F34nCpGmDZ0s',#65,'BEAM','L4X4X3/8',$,#892,#872,$);
|
||||
#914= IFCSTRUCTURALPROFILEPROPERTIES('L4X4X3/8',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#915= IFCCARTESIANPOINT((0.,0.));
|
||||
#919= IFCDIRECTION((1.,0.));
|
||||
#923= IFCAXIS2PLACEMENT2D(#915,#919);
|
||||
#926= IFCLSHAPEPROFILEDEF(.AREA.,$,#923,101.6,101.60025,9.5249996,9.525,$,$,$,$);
|
||||
#927= IFCDIRECTION((0.,0.,-1.));
|
||||
#931= IFCDIRECTION((-1.,0.,0.));
|
||||
#935= IFCAXIS2PLACEMENT3D(#21,#931,#33);
|
||||
#938= IFCEXTRUDEDAREASOLID(#926,#935,#927,330.2);
|
||||
#941= IFCCARTESIANPOINT((0.,-50.799999,-50.800125));
|
||||
#945= IFCBOUNDINGBOX(#941,330.2,101.6,101.60025);
|
||||
#948= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#938));
|
||||
#954= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#945));
|
||||
#960= IFCPRODUCTDEFINITIONSHAPE($,$,(#948,#954));
|
||||
#964= IFCCARTESIANPOINT((4801.108,7564.4494,495.301));
|
||||
#968= IFCDIRECTION((0.,-1.,0.));
|
||||
#972= IFCDIRECTION((0.,0.,-1.));
|
||||
#976= IFCAXIS2PLACEMENT3D(#964,#968,#972);
|
||||
#979= IFCLOCALPLACEMENT($,#976);
|
||||
#982= IFCBEAM('13c6Dt00006J4nCpGmDZ0s',#65,'BEAM','L4X4X3/8',$,#979,#960,$);
|
||||
#1001= IFCPOLYLINE((#1006,#1010,#1014,#1018,#1022,#1026));
|
||||
#1006= IFCCARTESIANPOINT((0.,0.));
|
||||
#1010= IFCCARTESIANPOINT((-0.001,486.84845));
|
||||
#1014= IFCCARTESIANPOINT((369.07135,486.84845));
|
||||
#1018= IFCCARTESIANPOINT((512.75543,343.16437));
|
||||
#1022= IFCCARTESIANPOINT((512.75543,0.));
|
||||
#1026= IFCCARTESIANPOINT((0.,0.));
|
||||
#1030= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL9.5',#1001);
|
||||
#1031= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#1034= IFCEXTRUDEDAREASOLID(#1030,#1031,#33,9.5);
|
||||
#1037= IFCCARTESIANPOINT((-0.0010532137,0.,-4.75));
|
||||
#1041= IFCBOUNDINGBOX(#1037,486.8495,512.75614,9.5);
|
||||
#1044= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1034));
|
||||
#1050= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1041));
|
||||
#1056= IFCPRODUCTDEFINITIONSHAPE($,$,(#1044,#1050));
|
||||
#1060= IFCCARTESIANPOINT((4763.008,7615.262,12.701));
|
||||
#1064= IFCDIRECTION((0.,1.,0.));
|
||||
#1068= IFCDIRECTION((0.,0.,1.));
|
||||
#1072= IFCAXIS2PLACEMENT3D(#1060,#1064,#1068);
|
||||
#1075= IFCLOCALPLACEMENT($,#1072);
|
||||
#1078= IFCPLATE('13c6Dt00005J4nCpGmDZ0s',#65,'PLATE','PL9.5',$,#1075,#1056,$);
|
||||
#1097= IFCCARTESIANPOINT((0.,0.));
|
||||
#1101= IFCDIRECTION((1.,0.));
|
||||
#1105= IFCAXIS2PLACEMENT2D(#1097,#1101);
|
||||
#1108= IFCRECTANGLEHOLLOWPROFILEDEF(.AREA.,$,#1105,152.39999,152.39999,9.5249996,9.5250004,19.05);
|
||||
#1109= IFCDIRECTION((0.,0.,-1.));
|
||||
#1113= IFCDIRECTION((-1.,0.,0.));
|
||||
#1117= IFCAXIS2PLACEMENT3D(#1130,#1113,#33);
|
||||
#1120= IFCEXTRUDEDAREASOLID(#1108,#1117,#1109,2177.4228);
|
||||
#1123= IFCCARTESIANPOINT((-1.2141754E-10,-76.199997,-76.199997));
|
||||
#1127= IFCBOUNDINGBOX(#1123,677.4228,152.39999,152.39999);
|
||||
#1130= IFCCARTESIANPOINT((-1500.,0.,0.));
|
||||
#1135= IFCCARTESIANPOINT((-1.0913936E-10,-9.0949470E-12,4.7379999));
|
||||
#1139= IFCDIRECTION((-1.,-1.6052583E-13,0.));
|
||||
#1143= IFCDIRECTION((-1.6052583E-13,1.,0.));
|
||||
#1147= IFCAXIS2PLACEMENT3D(#1135,#1139,#1143);
|
||||
#1150= IFCPLANE(#1147);
|
||||
#1153= IFCHALFSPACESOLID(#1150,.F.);
|
||||
#1156= IFCBOOLEANCLIPPINGRESULT(.DIFFERENCE.,#1120,#1153);
|
||||
#1159= IFCSHAPEREPRESENTATION(#40,'Body','Clipping',(#1156));
|
||||
#1165= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1127));
|
||||
#1171= IFCPRODUCTDEFINITIONSHAPE($,$,(#1159,#1165));
|
||||
#1175= IFCCARTESIANPOINT((5007.3895,7620.,282.98953));
|
||||
#1179= IFCDIRECTION((0.,-1.,0.));
|
||||
#1183= IFCDIRECTION((0.70710678,0.,0.70710678));
|
||||
#1187= IFCAXIS2PLACEMENT3D(#1175,#1179,#1183);
|
||||
#1190= IFCLOCALPLACEMENT($,#1187);
|
||||
#1193= IFCBEAM('13c6Dt00003p4nCpGmDZ0r',#65,'BRACE','TS6X6X3/8',$,#1190,#1171,$);
|
||||
#1212= IFCCARTESIANPOINT((5153.6546,7620.012,307.34937));
|
||||
#1216= IFCDIRECTION((0.,-1.,0.));
|
||||
#1220= IFCDIRECTION((-0.70710678,0.,0.70710678));
|
||||
#1224= IFCAXIS2PLACEMENT3D(#1212,#1216,#1220);
|
||||
#1227= IFCLOCALPLACEMENT($,#1224);
|
||||
#1230= IFCCARTESIANPOINT((0.,0.));
|
||||
#1234= IFCDIRECTION((1.,0.));
|
||||
#1238= IFCAXIS2PLACEMENT2D(#1230,#1234);
|
||||
#1241= IFCRECTANGLEPROFILEDEF(.AREA.,$,#1238,9.5,266.7);
|
||||
#1242= IFCDIRECTION((0.,0.,-1.));
|
||||
#1246= IFCDIRECTION((-1.,0.,0.));
|
||||
#1250= IFCAXIS2PLACEMENT3D(#21,#1246,#33);
|
||||
#1253= IFCEXTRUDEDAREASOLID(#1241,#1250,#1242,172.39997);
|
||||
#1256= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1253));
|
||||
#1263= IFCPRODUCTDEFINITIONSHAPE($,$,(#1256));
|
||||
#1267= IFCOPENINGELEMENT('13c6Dt0000Ip4nCpGmDZ0s',#65,$,$,$,#1227,#1263,$);
|
||||
#1288= IFCRELVOIDSELEMENT('3gz_1FcMHExhEMJzH8zl$M',#65,$,$,#1193,#1267);
|
||||
#1289= IFCSTRUCTURALPROFILEPROPERTIES('TS6X6X3/8',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#1290= IFCCARTESIANPOINT((0.,0.));
|
||||
#1294= IFCDIRECTION((1.,0.));
|
||||
#1298= IFCAXIS2PLACEMENT2D(#1290,#1294);
|
||||
#1301= IFCISHAPEPROFILEDEF(.AREA.,$,#1298,254.88901,356.616,12.7,18.288,21.3995);
|
||||
#1302= IFCDIRECTION((0.,0.,-1.));
|
||||
#1306= IFCDIRECTION((-1.,0.,0.));
|
||||
#1310= IFCAXIS2PLACEMENT3D(#1323,#1306,#33);
|
||||
#1313= IFCEXTRUDEDAREASOLID(#1301,#1310,#1302,2768.2595);
|
||||
#1316= IFCCARTESIANPOINT((0.,-178.308,-127.4445));
|
||||
#1320= IFCBOUNDINGBOX(#1316,1268.2595,356.616,254.88901);
|
||||
#1323= IFCCARTESIANPOINT((-1500.,0.,0.));
|
||||
#1327= IFCCARTESIANPOINT((0.,278.308,0.));
|
||||
#1331= IFCDIRECTION((-1.,0.,0.));
|
||||
#1335= IFCDIRECTION((0.,0.,1.));
|
||||
#1339= IFCAXIS2PLACEMENT3D(#1327,#1331,#1335);
|
||||
#1342= IFCPLANE(#1339);
|
||||
#1345= IFCHALFSPACESOLID(#1342,.F.);
|
||||
#1348= IFCBOOLEANCLIPPINGRESULT(.DIFFERENCE.,#1313,#1345);
|
||||
#1351= IFCSHAPEREPRESENTATION(#40,'Body','Clipping',(#1348));
|
||||
#1357= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1320));
|
||||
#1363= IFCPRODUCTDEFINITIONSHAPE($,$,(#1351,#1357));
|
||||
#1367= IFCCARTESIANPOINT((4763.008,7620.,-178.308));
|
||||
#1371= IFCDIRECTION((0.,-1.,0.));
|
||||
#1375= IFCDIRECTION((1.,0.,-7.2832534E-15));
|
||||
#1379= IFCAXIS2PLACEMENT3D(#1367,#1371,#1375);
|
||||
#1382= IFCLOCALPLACEMENT($,#1379);
|
||||
#1385= IFCBEAM('13c6Dt00002p4nCpGmDZ0r',#65,'BEAM','W14X68',$,#1382,#1363,$);
|
||||
#1405= IFCSTRUCTURALPROFILEPROPERTIES('W14X68',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
|
||||
#1406= IFCCARTESIANPOINT((0.,0.));
|
||||
#1410= IFCDIRECTION((1.,0.));
|
||||
#1414= IFCAXIS2PLACEMENT2D(#1406,#1410);
|
||||
#1417= IFCISHAPEPROFILEDEF(.AREA.,$,#1414,254.88901,356.616,12.7,18.288,21.3995);
|
||||
#1418= IFCDIRECTION((0.,0.,-1.));
|
||||
#1422= IFCDIRECTION((-1.,0.,0.));
|
||||
#1426= IFCAXIS2PLACEMENT3D(#21,#1422,#33);
|
||||
#1429= IFCEXTRUDEDAREASOLID(#1417,#1426,#1418,1524.);
|
||||
#1432= IFCCARTESIANPOINT((0.,-178.308,-127.4445));
|
||||
#1436= IFCBOUNDINGBOX(#1432,1524.,356.616,254.88901);
|
||||
#1439= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1429));
|
||||
#1445= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1436));
|
||||
#1451= IFCPRODUCTDEFINITIONSHAPE($,$,(#1439,#1445));
|
||||
#1455= IFCCARTESIANPOINT((4572.,7620.,-609.6));
|
||||
#1459= IFCDIRECTION((0.,-1.,0.));
|
||||
#1463= IFCDIRECTION((7.1613756E-15,0.,1.));
|
||||
#1467= IFCAXIS2PLACEMENT3D(#1455,#1459,#1463);
|
||||
#1470= IFCLOCALPLACEMENT($,#1467);
|
||||
#1473= IFCCOLUMN('13c6Dt00001p4nCpGmDZ0q',#65,'COLUMN','W14X68',$,#1470,#1451,$);
|
||||
#1492= IFCCARTESIANPOINT((4661.154,7490.05,308.83104));
|
||||
#1496= IFCDIRECTION((0.,1.,0.));
|
||||
#1500= IFCDIRECTION((-1.,0.,7.0134555E-15));
|
||||
#1504= IFCAXIS2PLACEMENT3D(#1492,#1496,#1500);
|
||||
#1507= IFCLOCALPLACEMENT($,#1504);
|
||||
#1510= IFCPOLYLINE((#1514,#1519,#1523,#1527,#1531,#1535,#1539,#1543,#1547,#1551,#1555,#1559,#1563,#1567,#1571,#1575,#1579,#1583,#1587,#1591,#1595,#1599,#1603,#1607,#1611,#1615,#1619,#1623,#1627,#1631,#1635,#1639,#1643,#1648,#1652,#1656,#1660));
|
||||
#1514= IFCCARTESIANPOINT((0.38429439,16.098194));
|
||||
#1519= IFCCARTESIANPOINT((0.,20.));
|
||||
#1523= IFCCARTESIANPOINT((0.,336.616));
|
||||
#1527= IFCCARTESIANPOINT((0.38429439,340.5178));
|
||||
#1531= IFCCARTESIANPOINT((1.5224093,344.26967));
|
||||
#1535= IFCCARTESIANPOINT((3.3706078,347.7274));
|
||||
#1539= IFCCARTESIANPOINT((5.8578644,350.75813));
|
||||
#1543= IFCCARTESIANPOINT((8.8885953,353.24539));
|
||||
#1547= IFCCARTESIANPOINT((12.346331,355.09359));
|
||||
#1551= IFCCARTESIANPOINT((16.098194,356.2317));
|
||||
#1555= IFCCARTESIANPOINT((20.,356.616));
|
||||
#1559= IFCCARTESIANPOINT((158.308,356.616));
|
||||
#1563= IFCCARTESIANPOINT((162.20981,356.2317));
|
||||
#1567= IFCCARTESIANPOINT((165.96167,355.09359));
|
||||
#1571= IFCCARTESIANPOINT((169.4194,353.24539));
|
||||
#1575= IFCCARTESIANPOINT((172.45013,350.75813));
|
||||
#1579= IFCCARTESIANPOINT((174.93739,347.7274));
|
||||
#1583= IFCCARTESIANPOINT((176.78559,344.26967));
|
||||
#1587= IFCCARTESIANPOINT((177.9237,340.5178));
|
||||
#1591= IFCCARTESIANPOINT((178.308,336.616));
|
||||
#1595= IFCCARTESIANPOINT((178.308,20.));
|
||||
#1599= IFCCARTESIANPOINT((177.9237,16.098194));
|
||||
#1603= IFCCARTESIANPOINT((176.78559,12.346331));
|
||||
#1607= IFCCARTESIANPOINT((174.93739,8.8885953));
|
||||
#1611= IFCCARTESIANPOINT((172.45013,5.8578644));
|
||||
#1615= IFCCARTESIANPOINT((169.4194,3.3706078));
|
||||
#1619= IFCCARTESIANPOINT((165.96167,1.5224093));
|
||||
#1623= IFCCARTESIANPOINT((162.20981,0.38429439));
|
||||
#1627= IFCCARTESIANPOINT((158.308,0.));
|
||||
#1631= IFCCARTESIANPOINT((20.,0.));
|
||||
#1635= IFCCARTESIANPOINT((16.098194,0.38429439));
|
||||
#1639= IFCCARTESIANPOINT((12.346331,1.5224093));
|
||||
#1643= IFCCARTESIANPOINT((8.8885953,3.3706078));
|
||||
#1648= IFCCARTESIANPOINT((5.8578644,5.8578644));
|
||||
#1652= IFCCARTESIANPOINT((3.3706078,8.8885953));
|
||||
#1656= IFCCARTESIANPOINT((1.5224093,12.346331));
|
||||
#1660= IFCCARTESIANPOINT((0.38429439,16.098194));
|
||||
#1664= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL259.9',#1510);
|
||||
#1665= IFCAXIS2PLACEMENT3D(#21,#33,#25);
|
||||
#1668= IFCEXTRUDEDAREASOLID(#1664,#1665,#33,259.9);
|
||||
#1671= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1668));
|
||||
#1677= IFCPRODUCTDEFINITIONSHAPE($,$,(#1671));
|
||||
#1681= IFCOPENINGELEMENT('13c6Dt0002m34nCpGmDZ4o',#65,$,$,$,#1507,#1677,$);
|
||||
#1702= IFCRELVOIDSELEMENT('2WI1gpRhj1dwB5Pft5vGvO',#65,$,$,#1473,#1681);
|
||||
#1703= IFCRELASSOCIATESMATERIAL('1GOaWfy957kRCtp2R28$a9',#9,$,$,(#1193),#59);
|
||||
#1705= IFCRELASSOCIATESMATERIAL('3Vqwp2DmT3TeKWHZyFzO7N',#9,$,$,(#1473,#1385,#633),#56);
|
||||
#1707= IFCRELASSOCIATESMATERIAL('0SMrmHf_5BRAYDVih2VNTN',#9,$,$,(#1078,#982,#895,#808,#720,#521,#434,#350,#268,#174),#53);
|
||||
#1709= IFCRELASSOCIATESPROFILEPROPERTIES('2mzP0x3pj2GwUm7Wy7Xfs0',#9,$,$,(#633),#652,$,$);
|
||||
#1711= IFCRELASSOCIATESPROFILEPROPERTIES('1iUJLoCsfD6ug6keUDRGVW',#9,$,$,(#808),#739,$,$);
|
||||
#1713= IFCRELASSOCIATESPROFILEPROPERTIES('1LidMEK_vEQuG8Js1MD1ND',#9,$,$,(#895,#982),#914,$,$);
|
||||
#1715= IFCRELASSOCIATESPROFILEPROPERTIES('22dp9maVb0sAxZG8F2xTIR',#9,$,$,(#1193),#1289,$,$);
|
||||
#1717= IFCRELASSOCIATESPROFILEPROPERTIES('36LPEeuXn6VgX3frK_urjy',#9,$,$,(#1385),#1405,$,$);
|
||||
ENDSEC;
|
||||
|
||||
END-ISO-10303-21;
|
||||
@@ -1,21 +1,30 @@
|
||||
const fs = require('fs')
|
||||
const { logger: parentLogger } = require('../observability/logging')
|
||||
|
||||
const TMP_RESULTS_PATH = '/tmp/import_result.json'
|
||||
|
||||
const { parseAndCreateCommit } = require('./index')
|
||||
const { parseAndCreateCommitFactory } = require('./index')
|
||||
const Observability = require('@speckle/shared/dist/commonjs/observability/index.js')
|
||||
const getDbClients = require('../knex')
|
||||
|
||||
async function main() {
|
||||
const cmdArgs = process.argv.slice(2)
|
||||
|
||||
const [filePath, userId, streamId, branchName, commitMessage, fileId] = cmdArgs
|
||||
const [
|
||||
filePath,
|
||||
tmpResultsPath,
|
||||
userId,
|
||||
streamId,
|
||||
branchName,
|
||||
commitMessage,
|
||||
fileId,
|
||||
branchId,
|
||||
regionName
|
||||
] = cmdArgs
|
||||
const logger = Observability.extendLoggerComponent(
|
||||
parentLogger.child({ streamId, branchName, userId, fileId, filePath }),
|
||||
parentLogger.child({ streamId, branchName, userId, fileId, branchId, filePath }),
|
||||
'ifc'
|
||||
)
|
||||
|
||||
logger.info('ARGV: ', filePath, userId, streamId, branchName, commitMessage)
|
||||
logger.info({ commitMessage }, 'IFC parser started.')
|
||||
|
||||
const data = fs.readFileSync(filePath)
|
||||
|
||||
@@ -25,6 +34,7 @@ async function main() {
|
||||
userId,
|
||||
message: commitMessage || ' Imported file',
|
||||
fileId,
|
||||
branchId,
|
||||
logger
|
||||
}
|
||||
if (branchName) ifcInput.branchName = branchName
|
||||
@@ -34,8 +44,10 @@ async function main() {
|
||||
error: 'Unknown error'
|
||||
}
|
||||
|
||||
const dbClients = await getDbClients()
|
||||
const knex = dbClients[regionName].public
|
||||
try {
|
||||
const commitId = await parseAndCreateCommit(ifcInput)
|
||||
const commitId = await parseAndCreateCommitFactory({ db: knex })(ifcInput)
|
||||
output = {
|
||||
success: true,
|
||||
commitId
|
||||
@@ -48,7 +60,7 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(TMP_RESULTS_PATH, JSON.stringify(output))
|
||||
fs.writeFileSync(tmpResultsPath, JSON.stringify(output))
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
@@ -1,86 +1,84 @@
|
||||
const { performance } = require('perf_hooks')
|
||||
const { fetch } = require('undici')
|
||||
const Parser = require('./parser_v2')
|
||||
const ServerAPI = require('./api.js')
|
||||
const Parser = require('./parser')
|
||||
const ServerAPI = require('../src/api.js')
|
||||
const Observability = require('@speckle/shared/dist/commonjs/observability/index.js')
|
||||
const { logger: parentLogger } = require('../observability/logging')
|
||||
|
||||
async function parseAndCreateCommit({
|
||||
data,
|
||||
streamId,
|
||||
branchName = 'uploads',
|
||||
userId,
|
||||
message = 'Manual IFC file upload',
|
||||
fileId,
|
||||
logger
|
||||
}) {
|
||||
if (!logger) {
|
||||
logger = Observability.extendLoggerComponent(
|
||||
parentLogger.child({ streamId, branchName, userId, fileId }),
|
||||
'ifc'
|
||||
const parseAndCreateCommitFactory =
|
||||
({ db }) =>
|
||||
async ({
|
||||
data,
|
||||
streamId,
|
||||
branchName = 'uploads',
|
||||
userId,
|
||||
message = 'Manual IFC file upload',
|
||||
fileId,
|
||||
branchId,
|
||||
logger
|
||||
}) => {
|
||||
if (!logger) {
|
||||
logger = Observability.extendLoggerComponent(
|
||||
parentLogger.child({ streamId, branchName, userId, fileId }),
|
||||
'ifc'
|
||||
)
|
||||
}
|
||||
const serverApi = new ServerAPI({ db, streamId, logger })
|
||||
const myParser = new Parser({ serverApi, fileId, logger })
|
||||
|
||||
const start = performance.now()
|
||||
const { id, tCount } = await myParser.parse(data)
|
||||
logger = logger.child({ objectId: id })
|
||||
const end = performance.now()
|
||||
logger.info(
|
||||
{
|
||||
fileProcessingDurationMs: (end - start).toFixed(2)
|
||||
},
|
||||
'Total processing time V2: {fileProcessingDurationMs}ms'
|
||||
)
|
||||
}
|
||||
const serverApi = new ServerAPI({ streamId, logger })
|
||||
const myParser = new Parser({ serverApi, fileId, logger })
|
||||
|
||||
const start = performance.now()
|
||||
const { id, tCount } = await myParser.parse(data)
|
||||
logger = logger.child({ objectId: id })
|
||||
const end = performance.now()
|
||||
logger.info(
|
||||
{
|
||||
fileProcessingDurationMs: (end - start).toFixed(2)
|
||||
},
|
||||
'Total processing time V2: {fileProcessingDurationMs}ms'
|
||||
)
|
||||
|
||||
const commit = {
|
||||
streamId,
|
||||
branchName,
|
||||
objectId: id,
|
||||
message,
|
||||
sourceApplication: 'IFC',
|
||||
totalChildrenCount: tCount
|
||||
}
|
||||
|
||||
const branch = await serverApi.getBranchByNameAndStreamId({
|
||||
streamId,
|
||||
name: branchName
|
||||
})
|
||||
|
||||
if (!branch) {
|
||||
logger.info("Branch '{branchName}' not found, creating it.")
|
||||
await serverApi.createBranch({
|
||||
name: branchName,
|
||||
const commit = {
|
||||
streamId,
|
||||
description: branchName === 'uploads' ? 'File upload branch' : null,
|
||||
authorId: userId
|
||||
branchName,
|
||||
objectId: id,
|
||||
message,
|
||||
sourceApplication: 'IFC',
|
||||
totalChildrenCount: tCount
|
||||
}
|
||||
|
||||
if (!branchId) {
|
||||
logger.info("Branch '{branchName}' not found, creating it.")
|
||||
await serverApi.createBranch({
|
||||
name: branchName,
|
||||
streamId,
|
||||
description: branchName === 'uploads' ? 'File upload branch' : null,
|
||||
authorId: userId
|
||||
})
|
||||
}
|
||||
|
||||
const userToken = process.env.USER_TOKEN
|
||||
|
||||
const serverBaseUrl = process.env.SPECKLE_SERVER_URL || 'http://127.0.0.1:3000'
|
||||
logger.info(`Creating commit for object ({objectId}), with message "${message}"`)
|
||||
const response = await fetch(serverBaseUrl + '/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${userToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query:
|
||||
'mutation createCommit( $myCommitInput: CommitCreateInput!) { commitCreate( commit: $myCommitInput ) }',
|
||||
variables: {
|
||||
myCommitInput: commit
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const json = await response.json()
|
||||
logger.info(json, 'Commit created')
|
||||
|
||||
return json.data.commitCreate
|
||||
}
|
||||
|
||||
const userToken = process.env.USER_TOKEN
|
||||
|
||||
const serverBaseUrl = process.env.SPECKLE_SERVER_URL || 'http://127.0.0.1:3000'
|
||||
logger.info(`Creating commit for object ({objectId}), with message "${message}"`)
|
||||
const response = await fetch(serverBaseUrl + '/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${userToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query:
|
||||
'mutation createCommit( $myCommitInput: CommitCreateInput!) { commitCreate( commit: $myCommitInput ) }',
|
||||
variables: {
|
||||
myCommitInput: commit
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const json = await response.json()
|
||||
logger.info(json, 'Commit created')
|
||||
|
||||
return json.data.commitCreate
|
||||
}
|
||||
|
||||
module.exports = { parseAndCreateCommit }
|
||||
module.exports = { parseAndCreateCommitFactory }
|
||||
|
||||
@@ -1,328 +1,403 @@
|
||||
/* eslint-disable camelcase */
|
||||
const { performance } = require('perf_hooks')
|
||||
const WebIFC = require('web-ifc/web-ifc-api-node')
|
||||
const { logger } = require('../observability/logging.js')
|
||||
const ServerAPI = require('./api.js')
|
||||
const {
|
||||
getHash,
|
||||
IfcElements,
|
||||
PropNames,
|
||||
GeometryTypes,
|
||||
IfcTypesMap
|
||||
} = require('./utils')
|
||||
const Observability = require('@speckle/shared/dist/commonjs/observability/index.js')
|
||||
const { logger: parentLogger } = require('../observability/logging')
|
||||
|
||||
module.exports = class IFCParser {
|
||||
constructor({ serverApi, logger }) {
|
||||
this.api = new WebIFC.IfcAPI()
|
||||
this.serverApi = serverApi || new ServerAPI({ logger })
|
||||
constructor({ serverApi, fileId, logger }) {
|
||||
this.ifcapi = new WebIFC.IfcAPI()
|
||||
this.ifcapi.SetWasmPath('./', false)
|
||||
this.serverApi = serverApi
|
||||
this.fileId = fileId
|
||||
this.logger =
|
||||
logger ||
|
||||
Observability.extendLoggerComponent(parentLogger.child({ fileId }), 'ifc')
|
||||
}
|
||||
|
||||
async parse(data) {
|
||||
if (this.api.wasmModule === undefined) await this.api.Init()
|
||||
|
||||
this.modelId = this.api.OpenModel(data, {
|
||||
COORDINATE_TO_ORIGIN: true,
|
||||
await this.ifcapi.Init()
|
||||
this.modelId = this.ifcapi.OpenModel(new Uint8Array(data), {
|
||||
USE_FAST_BOOLS: true
|
||||
})
|
||||
|
||||
this.projectId = this.api.GetLineIDsWithType(this.modelId, WebIFC.IFCPROJECT).get(0)
|
||||
this.startTime = performance.now()
|
||||
|
||||
this.project = this.api.GetLine(this.modelId, this.projectId, true)
|
||||
this.project.__closure = {}
|
||||
// prepoulate types
|
||||
this.types = await this.getAllTypesOfModel()
|
||||
|
||||
this.cache = {}
|
||||
this.closureCache = {}
|
||||
// prime caches for property sets and their relating objects, as well as,
|
||||
// most importantly, all the properties.
|
||||
const { psetLines, psetRelations, properties } = await this.getAllProps()
|
||||
this.psetLines = psetLines
|
||||
this.psetRelations = psetRelations
|
||||
this.properties = properties
|
||||
|
||||
// Steps: create and store in speckle all the geometries (meshes) from this project and store them
|
||||
// as reference objects in this.productGeo
|
||||
this.productGeo = {}
|
||||
await this.createGeometries()
|
||||
logger.info(`Geometries created: ${Object.keys(this.productGeo).length} meshes.`)
|
||||
this.propCache = {}
|
||||
|
||||
// Lastly, traverse the ifc project object and parse it into something friendly; as well as
|
||||
// replace all its geometries with actual references to speckle meshes from the productGeo map
|
||||
// This is used to pre-batch ifc objects that need to be persisted.
|
||||
this.objectBucket = []
|
||||
|
||||
await this.traverse(this.project, true, 0)
|
||||
// create and save the geometries; we're storing only references locally.
|
||||
this.geometryReferences = await this.createAndSaveMeshes()
|
||||
|
||||
const id = await this.serverApi.saveObject(this.project)
|
||||
return { id, tCount: Object.keys(this.project.__closure).length }
|
||||
// create and save the spatial tree, populating both properties and geometry references
|
||||
// where appropriate
|
||||
this.spatialNodeCount = 0
|
||||
const structure = await this.createSpatialStructure()
|
||||
return { id: structure.id, tCount: structure.closureLen }
|
||||
}
|
||||
|
||||
async createGeometries() {
|
||||
this.rawGeo = this.api.LoadAllGeometry(this.modelId)
|
||||
async createSpatialStructure() {
|
||||
const chunks = await this.getSpatialTreeChunks()
|
||||
const allProjectLines = await this.ifcapi.GetLineIDsWithType(
|
||||
this.modelId,
|
||||
WebIFC.IFCPROJECT
|
||||
)
|
||||
const project = {
|
||||
expressID: allProjectLines.get(0),
|
||||
type: 'IFCPROJECT',
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'Base',
|
||||
elements: []
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.rawGeo.size(); i++) {
|
||||
const mesh = this.rawGeo.get(i)
|
||||
const prodId = mesh.expressID
|
||||
this.productGeo[prodId] = []
|
||||
await this.populateSpatialNode(project, chunks, [], 0)
|
||||
|
||||
for (let j = 0; j < mesh.geometries.size(); j++) {
|
||||
const placedGeom = mesh.geometries.get(j)
|
||||
const geom = this.api.GetGeometry(this.modelId, placedGeom.geometryExpressID)
|
||||
this.endTime = performance.now()
|
||||
project.parseTime = (this.endTime - this.startTime).toFixed(2) + 'ms'
|
||||
project.fileId = this.fileId
|
||||
|
||||
const matrix = placedGeom.flatTransformation
|
||||
const raw = {
|
||||
color: geom.color, // NOTE: material: x, y, z = rgb, w = opacity
|
||||
vertices: this.api.GetVertexArray(
|
||||
geom.GetVertexData(),
|
||||
geom.GetVertexDataSize()
|
||||
),
|
||||
indices: this.api.GetIndexArray(geom.GetIndexData(), geom.GetIndexDataSize())
|
||||
// Last save to db call, empty the last bucket
|
||||
if (this.objectBucket.length !== 0) {
|
||||
await this.flushObjectBucket()
|
||||
}
|
||||
return project
|
||||
}
|
||||
|
||||
async populateSpatialNode(node, chunks, closures, depth) {
|
||||
depth++
|
||||
this.logger.debug(`${this.spatialNodeCount++} nodes generated.`)
|
||||
closures.push([])
|
||||
await this.getChildren(node, chunks, PropNames.aggregates, closures, depth)
|
||||
await this.getChildren(node, chunks, PropNames.spatial, closures, depth)
|
||||
|
||||
node.closure = [...new Set(closures.pop())]
|
||||
|
||||
// get geometry, set displayValue
|
||||
// add geometry ids to closure
|
||||
if (
|
||||
this.geometryReferences[node.expressID] &&
|
||||
this.geometryReferences[node.expressID].length !== 0
|
||||
) {
|
||||
node['@displayValue'] = this.geometryReferences[node.expressID]
|
||||
node.closure.push(
|
||||
...this.geometryReferences[node.expressID].map((ref) => ref.referencedId)
|
||||
)
|
||||
}
|
||||
// node.closureLen = node.closure.length
|
||||
node.__closure = this.formatClosure(node.closure)
|
||||
node.id = getHash(node)
|
||||
|
||||
// Save to db
|
||||
this.objectBucket.push(node)
|
||||
if (this.objectBucket.length > 3000) {
|
||||
await this.flushObjectBucket()
|
||||
}
|
||||
|
||||
// remove project level node closure
|
||||
if (depth === 1) {
|
||||
delete node.closure
|
||||
}
|
||||
return node.id
|
||||
}
|
||||
|
||||
async flushObjectBucket() {
|
||||
if (this.objectBucket.length === 0) return
|
||||
await this.serverApi.saveObjectBatch(this.objectBucket)
|
||||
this.objectBucket = []
|
||||
}
|
||||
|
||||
formatClosure(idsArray) {
|
||||
const cl = {}
|
||||
for (const id of idsArray) cl[id] = 1
|
||||
return cl
|
||||
}
|
||||
|
||||
async getChildren(node, chunks, propName, closures) {
|
||||
const children = chunks[node.expressID]
|
||||
if (!children) return
|
||||
const prop = propName.key
|
||||
const nodes = []
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
let cnode = this.createNode(child)
|
||||
cnode = { ...cnode, ...(await this.getItemProperties(cnode.expressID)) }
|
||||
cnode.id = await this.populateSpatialNode(cnode, chunks, closures)
|
||||
|
||||
for (const closure of closures) {
|
||||
closure.push(cnode.id)
|
||||
if (cnode['closure'].length > 30_000)
|
||||
for (const id of cnode['closure']) closure.push(id)
|
||||
else closure.push(...cnode['closure']) // can stack overflow for large arguments
|
||||
}
|
||||
|
||||
delete cnode.closure
|
||||
nodes.push(cnode)
|
||||
}
|
||||
|
||||
node[prop] = nodes.map((node) => ({
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'reference',
|
||||
referencedId: node.id
|
||||
}))
|
||||
}
|
||||
|
||||
async getItemProperties(id) {
|
||||
if (this.propCache[id]) return this.propCache[id]
|
||||
|
||||
let props = {}
|
||||
const directProps = this.properties[id.toString()]
|
||||
props = { ...directProps }
|
||||
|
||||
const psetIds = []
|
||||
for (let i = 0; i < this.psetRelations.length; i++) {
|
||||
if (this.psetRelations[i].includes(id))
|
||||
psetIds.push(this.psetLines.get(i).toString())
|
||||
}
|
||||
|
||||
const rawPsetIds = psetIds.map((id) =>
|
||||
this.properties[id].RelatingPropertyDefinition.toString()
|
||||
)
|
||||
const rawPsets = rawPsetIds.map((id) => this.properties[id])
|
||||
for (const pset of rawPsets) {
|
||||
props[pset.Name] = this.unpackPsetOrComplexProp(pset)
|
||||
}
|
||||
|
||||
this.propCache[id] = props
|
||||
return props
|
||||
}
|
||||
|
||||
unpackPsetOrComplexProp(pset) {
|
||||
const parsed = {}
|
||||
if (!pset.HasProperties || !Array.isArray(pset.HasProperties)) return parsed
|
||||
for (const id of pset.HasProperties) {
|
||||
const value = this.properties[id.toString()]
|
||||
if (value?.type === 'IFCCOMPLEXPROPERTY') {
|
||||
parsed[value.Name] = this.unpackPsetOrComplexProp(value)
|
||||
} else if (value?.type === 'IFCPROPERTYSINGLEVALUE') {
|
||||
parsed[value.Name] = value.NominalValue
|
||||
}
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
async getSpatialTreeChunks() {
|
||||
const treeChunks = {}
|
||||
await this.getChunks(treeChunks, PropNames.aggregates)
|
||||
await this.getChunks(treeChunks, PropNames.spatial)
|
||||
return treeChunks
|
||||
}
|
||||
|
||||
async getChunks(chunks, propName) {
|
||||
const relation = await this.ifcapi.GetLineIDsWithType(this.modelId, propName.name)
|
||||
for (let i = 0; i < relation.size(); i++) {
|
||||
const rel = await this.ifcapi.GetLine(this.modelId, relation.get(i), false)
|
||||
this.saveChunk(chunks, propName, rel)
|
||||
}
|
||||
}
|
||||
|
||||
saveChunk(chunks, propName, rel) {
|
||||
const relating = rel[propName.relating].value
|
||||
const related = rel[propName.related].map((r) => r.value)
|
||||
if (chunks[relating] === undefined) {
|
||||
chunks[relating] = related
|
||||
} else {
|
||||
chunks[relating] = chunks[relating].concat(related)
|
||||
}
|
||||
}
|
||||
|
||||
async getAllTypesOfModel() {
|
||||
const result = {}
|
||||
const elements = Object.keys(IfcElements).map((e) => parseInt(e))
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i]
|
||||
const lines = await this.ifcapi.GetLineIDsWithType(this.modelId, element)
|
||||
const size = lines.size()
|
||||
for (let i = 0; i < size; i++) result[lines.get(i)] = element
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async getAllProps() {
|
||||
const psetLines = this.ifcapi.GetLineIDsWithType(
|
||||
this.modelId,
|
||||
WebIFC.IFCRELDEFINESBYPROPERTIES
|
||||
)
|
||||
const psetRelations = []
|
||||
const properties = {}
|
||||
|
||||
const geometryIds = await this.getAllGeometriesIds()
|
||||
const allLinesIDs = await this.ifcapi.GetAllLines(this.modelId)
|
||||
const allLinesCount = allLinesIDs.size()
|
||||
for (let i = 0; i < allLinesCount; i++) {
|
||||
this.logger.debug(`${((i / allLinesCount) * 100).toFixed(3)}% props.`)
|
||||
const id = allLinesIDs.get(i)
|
||||
if (!geometryIds.has(id)) {
|
||||
const props = await this.getItemProperty(id)
|
||||
if (props) {
|
||||
if (props.type === 'IFCRELDEFINESBYPROPERTIES' && props.RelatedObjects) {
|
||||
psetRelations.push(props.RelatedObjects)
|
||||
}
|
||||
properties[id] = props
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { vertices } = this.extractVertexData(raw.vertices)
|
||||
return { psetLines, psetRelations, properties }
|
||||
}
|
||||
|
||||
for (let k = 0; k < vertices.length; k += 3) {
|
||||
const x = vertices[k],
|
||||
y = vertices[k + 1],
|
||||
z = vertices[k + 2]
|
||||
vertices[k] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12]
|
||||
vertices[k + 1] =
|
||||
(matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]) * -1
|
||||
vertices[k + 2] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13]
|
||||
}
|
||||
async getItemProperty(id) {
|
||||
try {
|
||||
const props = await this.ifcapi.GetLine(this.modelId, id)
|
||||
if (props.type) {
|
||||
props.type = IfcTypesMap[props.type]
|
||||
}
|
||||
this.inPlaceFormatItemProperties(props)
|
||||
return props
|
||||
} catch (e) {
|
||||
this.logger.error(e, `There was an issue getting props of id ${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Since all faces are triangles, we must add a `0` before each group of 3.
|
||||
const spcklFaces = []
|
||||
for (let i = 0; i < raw.indices.length; i++) {
|
||||
if (i % 3 === 0) spcklFaces.push(0)
|
||||
spcklFaces.push(raw.indices[i])
|
||||
}
|
||||
inPlaceFormatItemProperties(props) {
|
||||
Object.keys(props).forEach((key) => {
|
||||
const value = props[key]
|
||||
if (value && value.value !== undefined) props[key] = value.value
|
||||
else if (Array.isArray(value))
|
||||
props[key] = value.map((item) => {
|
||||
if (item && item.value) return item.value
|
||||
return item
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Create a proper Speckle Mesh
|
||||
const spcklMesh = {
|
||||
createNode(id) {
|
||||
const typeName = this.getNodeType(id)
|
||||
return {
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: typeName,
|
||||
expressID: id,
|
||||
type: typeName,
|
||||
elements: [],
|
||||
properties: null
|
||||
}
|
||||
}
|
||||
|
||||
getNodeType(id) {
|
||||
const typeID = this.types[id]
|
||||
return IfcElements[typeID]
|
||||
}
|
||||
|
||||
async getAllGeometriesIds() {
|
||||
const geometriesIds = new Set()
|
||||
const geomTypesArray = Array.from(GeometryTypes)
|
||||
for (let i = 0; i < geomTypesArray.length; i++) {
|
||||
const category = geomTypesArray[i]
|
||||
const ids = await this.ifcapi.GetLineIDsWithType(this.modelId, category)
|
||||
const idsSize = ids.size()
|
||||
for (let j = 0; j < idsSize; j++) {
|
||||
geometriesIds.add(ids.get(j))
|
||||
}
|
||||
}
|
||||
this.geometryIdsCount = geometriesIds.size
|
||||
return geometriesIds
|
||||
}
|
||||
|
||||
async createAndSaveMeshes() {
|
||||
const geometryReferences = {}
|
||||
let count = 0
|
||||
const speckleMeshes = []
|
||||
|
||||
this.ifcapi.StreamAllMeshes(this.modelId, async (mesh) => {
|
||||
const placedGeometries = mesh.geometries
|
||||
geometryReferences[mesh.expressID] = []
|
||||
for (let i = 0; i < placedGeometries.size(); i++) {
|
||||
const placedGeometry = placedGeometries.get(i)
|
||||
const geometry = this.ifcapi.GetGeometry(
|
||||
this.modelId,
|
||||
placedGeometry.geometryExpressID
|
||||
)
|
||||
|
||||
const verts = [
|
||||
...this.ifcapi.GetVertexArray(
|
||||
geometry.GetVertexData(),
|
||||
geometry.GetVertexDataSize()
|
||||
)
|
||||
]
|
||||
|
||||
const indices = [
|
||||
...this.ifcapi.GetIndexArray(
|
||||
geometry.GetIndexData(),
|
||||
geometry.GetIndexDataSize()
|
||||
)
|
||||
]
|
||||
|
||||
const { vertices } = this.extractVertexData(
|
||||
verts,
|
||||
placedGeometry.flatTransformation
|
||||
)
|
||||
const faces = this.extractFaces(indices)
|
||||
|
||||
const speckleMesh = {
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'Objects.Geometry.Mesh',
|
||||
units: 'm',
|
||||
volume: 0,
|
||||
area: 0,
|
||||
faces: spcklFaces,
|
||||
vertices: Array.from(vertices),
|
||||
renderMaterial: placedGeom.color
|
||||
? this.colorToMaterial(placedGeom.color)
|
||||
// random: Math.random(), // TODO: remove, this is here just for performance benchmarking/explicit cache poisoning
|
||||
vertices,
|
||||
faces,
|
||||
renderMaterial: placedGeometry.color
|
||||
? this.colorToMaterial(placedGeometry.color)
|
||||
: null
|
||||
}
|
||||
|
||||
const id = await this.serverApi.saveObject(spcklMesh)
|
||||
const ref = { speckle_type: 'reference', referencedId: id }
|
||||
this.productGeo[prodId].push(ref)
|
||||
speckleMesh.id = getHash(speckleMesh)
|
||||
// Note: the web-ifc api disposes of the data post callback, and doesn't know that it's async;
|
||||
// we cannot and should not await things in here. I'm not entirely sure what's going on :)
|
||||
// await this.serverApi.saveObject(speckleMesh)
|
||||
|
||||
speckleMeshes.push(speckleMesh)
|
||||
geometryReferences[mesh.expressID].push({
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'reference',
|
||||
referencedId: speckleMesh.id
|
||||
})
|
||||
this.logger.debug(`${(count++).toFixed(3)} geoms generated.`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await this.serverApi.saveObjectBatch(speckleMeshes)
|
||||
return geometryReferences
|
||||
}
|
||||
|
||||
async traverse(
|
||||
element,
|
||||
recursive = true,
|
||||
depth = 0,
|
||||
specialTypes = [
|
||||
{ type: 'IfcProject', key: 'Name' },
|
||||
{ type: 'IfcBuilding', key: 'Name' },
|
||||
{ type: 'IfcSite', key: 'Name' }
|
||||
]
|
||||
) {
|
||||
// Fast exit if null/undefined
|
||||
if (!element) return
|
||||
|
||||
// If array, traverse all items in it.
|
||||
if (Array.isArray(element)) {
|
||||
return await Promise.all(
|
||||
element.map(
|
||||
async (el) => await this.traverse(el, recursive, depth + 1, specialTypes)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// If it has no expressID, its either a simple type or a { type, value } object.
|
||||
if (!element.expressID) {
|
||||
return await Promise.resolve(
|
||||
element.value !== null && element.value !== undefined ? element.value : element
|
||||
)
|
||||
}
|
||||
|
||||
if (this.cache[element.expressID.toString()])
|
||||
return this.cache[element.expressID.toString()]
|
||||
// If you got here -> It's an IFC Element: create base object, upload and return ref.
|
||||
// logger.debug( `Traversing element ${element.expressID}; Recurse: ${recursive}; Stack ${depth}` )
|
||||
|
||||
// Traverse all key/value pairs first.
|
||||
for (const key of Object.keys(element)) {
|
||||
element[key] = await this.traverse(
|
||||
element[key],
|
||||
recursive,
|
||||
depth + 1,
|
||||
specialTypes
|
||||
)
|
||||
}
|
||||
|
||||
// Assign speckle_type and empty closure table.
|
||||
element.speckle_type = element.constructor.name
|
||||
element.__closure = {}
|
||||
|
||||
// Find spatial children and assign to element
|
||||
const spatialChildrenIds = this.getAllRelatedItemsOfType(
|
||||
element.expressID,
|
||||
WebIFC.IFCRELAGGREGATES,
|
||||
'RelatingObject',
|
||||
'RelatedObjects'
|
||||
)
|
||||
if (spatialChildrenIds.length > 0)
|
||||
element.rawSpatialChildren = spatialChildrenIds.map((childId) =>
|
||||
this.api.GetLine(this.modelId, childId, true)
|
||||
)
|
||||
|
||||
// Find children and populate element
|
||||
const childrenIds = this.getAllRelatedItemsOfType(
|
||||
element.expressID,
|
||||
WebIFC.IFCRELCONTAINEDINSPATIALSTRUCTURE,
|
||||
'RelatingStructure',
|
||||
'RelatedElements'
|
||||
)
|
||||
if (childrenIds.length > 0)
|
||||
element.rawChildren = childrenIds.map((childId) =>
|
||||
this.api.GetLine(this.modelId, childId, true)
|
||||
)
|
||||
|
||||
// Find related property sets
|
||||
const psetsIds = this.getAllRelatedItemsOfType(
|
||||
element.expressID,
|
||||
WebIFC.IFCRELDEFINESBYPROPERTIES,
|
||||
'RelatingPropertyDefinition',
|
||||
'RelatedObjects'
|
||||
)
|
||||
if (psetsIds.length > 0)
|
||||
element.rawPsets = psetsIds.map((childId) =>
|
||||
this.api.GetLine(this.modelId, childId, true)
|
||||
)
|
||||
|
||||
// Find related type properties
|
||||
const typePropsId = this.getAllRelatedItemsOfType(
|
||||
element.expressID,
|
||||
WebIFC.IFCRELDEFINESBYTYPE,
|
||||
'RelatingType',
|
||||
'RelatedObjects'
|
||||
)
|
||||
if (typePropsId.length > 0)
|
||||
element.rawTypeProps = typePropsId.map((childId) =>
|
||||
this.api.GetLine(this.modelId, childId, true)
|
||||
)
|
||||
|
||||
// Lookup geometry in generated geometries object
|
||||
if (this.productGeo[element.expressID]) {
|
||||
element['@displayValue'] = this.productGeo[element.expressID]
|
||||
this.productGeo[element.expressID].forEach((ref) => {
|
||||
this.project.__closure[ref.referencedId.toString()] = depth
|
||||
element.__closure[ref.referencedId.toString()] = 1
|
||||
})
|
||||
}
|
||||
|
||||
const isSpecial = specialTypes.find((t) => t.type === element.speckle_type)
|
||||
// Recurse all children
|
||||
if (recursive) {
|
||||
await this.processSubElements(
|
||||
element,
|
||||
'rawSpatialChildren',
|
||||
'spatialChildren',
|
||||
isSpecial,
|
||||
recursive,
|
||||
depth,
|
||||
specialTypes
|
||||
)
|
||||
await this.processSubElements(
|
||||
element,
|
||||
'rawChildren',
|
||||
'children',
|
||||
isSpecial,
|
||||
recursive,
|
||||
depth,
|
||||
specialTypes
|
||||
)
|
||||
await this.processSubElements(
|
||||
element,
|
||||
'rawPsets',
|
||||
'propertySets',
|
||||
false,
|
||||
recursive,
|
||||
depth,
|
||||
specialTypes
|
||||
)
|
||||
await this.processSubElements(
|
||||
element,
|
||||
'rawTypeProps',
|
||||
'typeProps',
|
||||
false,
|
||||
recursive,
|
||||
depth,
|
||||
specialTypes
|
||||
)
|
||||
|
||||
if (
|
||||
element.children ||
|
||||
element.spatialChildren ||
|
||||
element.propertySets ||
|
||||
element.typeProps
|
||||
) {
|
||||
logger.info(
|
||||
`${element.constructor.name} ${element.GlobalId}:\n\tchildren count: ${
|
||||
element.children ? element.children.length : '0'
|
||||
};\n\tspatial children count: ${
|
||||
element.spatialChildren ? element.spatialChildren.length : '0'
|
||||
};\n\tproperty sets count: ${
|
||||
element.propertySets ? element.propertySets.length : 0
|
||||
};\n\ttype properties: ${element.typeProps ? element.typeProps.length : 0}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.productGeo[element.expressID] ||
|
||||
element.spatialChildren ||
|
||||
element.children
|
||||
) {
|
||||
const id = await this.serverApi.saveObject(element)
|
||||
const ref = { speckle_type: 'reference', referencedId: id }
|
||||
this.cache[element.expressID.toString()] = ref
|
||||
this.closureCache[element.expressID.toString()] = element.__closure
|
||||
return ref
|
||||
} else {
|
||||
this.cache[element.expressID.toString()] = element
|
||||
this.closureCache[element.expressID.toString()] = element.__closure
|
||||
return element
|
||||
extractFaces(indices) {
|
||||
const faces = []
|
||||
for (let i = 0; i < indices.length; i++) {
|
||||
if (i % 3 === 0) faces.push(0)
|
||||
faces.push(indices[i])
|
||||
}
|
||||
return faces
|
||||
}
|
||||
|
||||
async processSubElements(
|
||||
element,
|
||||
key,
|
||||
newKey,
|
||||
isSpecial,
|
||||
recursive,
|
||||
depth,
|
||||
specialTypes
|
||||
) {
|
||||
if (element[key]) {
|
||||
if (!isSpecial) element[newKey] = []
|
||||
const childCount = {}
|
||||
for (const child of element[key]) {
|
||||
const res = await this.traverse(child, recursive, depth + 1, specialTypes)
|
||||
if (res.referencedId) {
|
||||
if (isSpecial) {
|
||||
let name = child[isSpecial.key]
|
||||
if (!name || name.length === 0) name = 'Undefined'
|
||||
if (!childCount[name]) childCount[name] = 0
|
||||
if (childCount[name] > 0) name += '-' + childCount[name]++
|
||||
element[name] = res
|
||||
} else element[newKey].push(res)
|
||||
this.project.__closure[res.referencedId.toString()] = depth
|
||||
element.__closure[res.referencedId.toString()] = 1
|
||||
|
||||
// adds to parent (this element) the child's closure tree.
|
||||
if (this.closureCache[child.expressID.toString()]) {
|
||||
for (const key of Object.keys(
|
||||
this.closureCache[child.expressID.toString()]
|
||||
)) {
|
||||
element.__closure[key] =
|
||||
this.closureCache[child.expressID.toString()][key] + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete element[key]
|
||||
}
|
||||
}
|
||||
|
||||
// (c) https://github.com/agviegas/web-ifc-three
|
||||
extractVertexData(vertexData) {
|
||||
extractVertexData(vertexData, matrix) {
|
||||
const vertices = []
|
||||
const normals = []
|
||||
let isNormalData = false
|
||||
@@ -330,45 +405,37 @@ module.exports = class IFCParser {
|
||||
isNormalData ? normals.push(vertexData[i]) : vertices.push(vertexData[i])
|
||||
if ((i + 1) % 3 === 0) isNormalData = !isNormalData
|
||||
}
|
||||
|
||||
// apply the transform
|
||||
for (let k = 0; k < vertices.length; k += 3) {
|
||||
const x = vertices[k],
|
||||
y = vertices[k + 1],
|
||||
z = vertices[k + 2]
|
||||
vertices[k] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12]
|
||||
vertices[k + 1] =
|
||||
(matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]) * -1
|
||||
vertices[k + 2] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13]
|
||||
}
|
||||
|
||||
return { vertices, normals }
|
||||
}
|
||||
|
||||
// (c) https://github.com/agviegas/web-ifc-three/blob/907e08b5673d5e1c18261a4fceade7189d6b2db7/src/IFC/PropertyManager.ts#L110
|
||||
getAllRelatedItemsOfType(elementID, type, relation, relatedProperty) {
|
||||
const lines = this.api.GetLineIDsWithType(this.modelId, type)
|
||||
const IDs = []
|
||||
|
||||
for (let i = 0; i < lines.size(); i++) {
|
||||
const relID = lines.get(i)
|
||||
const rel = this.api.GetLine(this.modelId, relID)
|
||||
const relatedItems = rel[relation]
|
||||
let foundElement = false
|
||||
|
||||
if (Array.isArray(relatedItems)) {
|
||||
const values = relatedItems.map((item) => item.value)
|
||||
foundElement = values.includes(elementID)
|
||||
} else foundElement = relatedItems.value === elementID
|
||||
|
||||
if (foundElement) {
|
||||
const element = rel[relatedProperty]
|
||||
if (!Array.isArray(element)) IDs.push(element.value)
|
||||
else element.forEach((ele) => IDs.push(ele.value))
|
||||
}
|
||||
}
|
||||
|
||||
return IDs
|
||||
}
|
||||
|
||||
colorToMaterial(color) {
|
||||
const intColor =
|
||||
(color.w << 24) + ((color.x * 255) << 16) + ((color.y * 255) << 8) + color.z * 255
|
||||
|
||||
return {
|
||||
const intColor = Math.floor(
|
||||
((color.w * 255) << 24) +
|
||||
((color.x * 255) << 16) +
|
||||
((color.y * 255) << 8) +
|
||||
color.z * 255
|
||||
)
|
||||
const material = {
|
||||
diffuse: intColor,
|
||||
opacity: color.w,
|
||||
metalness: 0,
|
||||
roughness: 1,
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'Objects.Other.RenderMaterial'
|
||||
}
|
||||
material.id = getHash(material)
|
||||
return material
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,441 +0,0 @@
|
||||
const { performance } = require('perf_hooks')
|
||||
const WebIFC = require('web-ifc/web-ifc-api-node')
|
||||
const {
|
||||
getHash,
|
||||
IfcElements,
|
||||
PropNames,
|
||||
GeometryTypes,
|
||||
IfcTypesMap
|
||||
} = require('./utils')
|
||||
const Observability = require('@speckle/shared/dist/commonjs/observability/index.js')
|
||||
const { logger: parentLogger } = require('../observability/logging')
|
||||
|
||||
module.exports = class IFCParser {
|
||||
constructor({ serverApi, fileId, logger }) {
|
||||
this.ifcapi = new WebIFC.IfcAPI()
|
||||
this.ifcapi.SetWasmPath('./', false)
|
||||
this.serverApi = serverApi
|
||||
this.fileId = fileId
|
||||
this.logger =
|
||||
logger ||
|
||||
Observability.extendLoggerComponent(parentLogger.child({ fileId }), 'ifc')
|
||||
}
|
||||
|
||||
async parse(data) {
|
||||
await this.ifcapi.Init()
|
||||
this.modelId = this.ifcapi.OpenModel(new Uint8Array(data), {
|
||||
USE_FAST_BOOLS: true
|
||||
})
|
||||
|
||||
this.startTime = performance.now()
|
||||
|
||||
// prepoulate types
|
||||
this.types = await this.getAllTypesOfModel()
|
||||
|
||||
// prime caches for property sets and their relating objects, as well as,
|
||||
// most importantly, all the properties.
|
||||
const { psetLines, psetRelations, properties } = await this.getAllProps()
|
||||
this.psetLines = psetLines
|
||||
this.psetRelations = psetRelations
|
||||
this.properties = properties
|
||||
|
||||
this.propCache = {}
|
||||
|
||||
// This is used to pre-batch ifc objects that need to be persisted.
|
||||
this.objectBucket = []
|
||||
|
||||
// create and save the geometries; we're storing only references locally.
|
||||
this.geometryReferences = await this.createAndSaveMeshes()
|
||||
|
||||
// create and save the spatial tree, populating both properties and geometry references
|
||||
// where appropriate
|
||||
this.spatialNodeCount = 0
|
||||
const structure = await this.createSpatialStructure()
|
||||
return { id: structure.id, tCount: structure.closureLen }
|
||||
}
|
||||
|
||||
async createSpatialStructure() {
|
||||
const chunks = await this.getSpatialTreeChunks()
|
||||
const allProjectLines = await this.ifcapi.GetLineIDsWithType(
|
||||
this.modelId,
|
||||
WebIFC.IFCPROJECT
|
||||
)
|
||||
const project = {
|
||||
expressID: allProjectLines.get(0),
|
||||
type: 'IFCPROJECT',
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'Base',
|
||||
elements: []
|
||||
}
|
||||
|
||||
await this.populateSpatialNode(project, chunks, [], 0)
|
||||
|
||||
this.endTime = performance.now()
|
||||
project.parseTime = (this.endTime - this.startTime).toFixed(2) + 'ms'
|
||||
project.fileId = this.fileId
|
||||
|
||||
// Last save to db call, empty the last bucket
|
||||
if (this.objectBucket.length !== 0) {
|
||||
await this.flushObjectBucket()
|
||||
}
|
||||
return project
|
||||
}
|
||||
|
||||
async populateSpatialNode(node, chunks, closures, depth) {
|
||||
depth++
|
||||
this.logger.debug(`${this.spatialNodeCount++} nodes generated.`)
|
||||
closures.push([])
|
||||
await this.getChildren(node, chunks, PropNames.aggregates, closures, depth)
|
||||
await this.getChildren(node, chunks, PropNames.spatial, closures, depth)
|
||||
|
||||
node.closure = [...new Set(closures.pop())]
|
||||
|
||||
// get geometry, set displayValue
|
||||
// add geometry ids to closure
|
||||
if (
|
||||
this.geometryReferences[node.expressID] &&
|
||||
this.geometryReferences[node.expressID].length !== 0
|
||||
) {
|
||||
node['@displayValue'] = this.geometryReferences[node.expressID]
|
||||
node.closure.push(
|
||||
...this.geometryReferences[node.expressID].map((ref) => ref.referencedId)
|
||||
)
|
||||
}
|
||||
// node.closureLen = node.closure.length
|
||||
node.__closure = this.formatClosure(node.closure)
|
||||
node.id = getHash(node)
|
||||
|
||||
// Save to db
|
||||
this.objectBucket.push(node)
|
||||
if (this.objectBucket.length > 3000) {
|
||||
await this.flushObjectBucket()
|
||||
}
|
||||
|
||||
// remove project level node closure
|
||||
if (depth === 1) {
|
||||
delete node.closure
|
||||
}
|
||||
return node.id
|
||||
}
|
||||
|
||||
async flushObjectBucket() {
|
||||
if (this.objectBucket.length === 0) return
|
||||
await this.serverApi.saveObjectBatch(this.objectBucket)
|
||||
this.objectBucket = []
|
||||
}
|
||||
|
||||
formatClosure(idsArray) {
|
||||
const cl = {}
|
||||
for (const id of idsArray) cl[id] = 1
|
||||
return cl
|
||||
}
|
||||
|
||||
async getChildren(node, chunks, propName, closures) {
|
||||
const children = chunks[node.expressID]
|
||||
if (!children) return
|
||||
const prop = propName.key
|
||||
const nodes = []
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
let cnode = this.createNode(child)
|
||||
cnode = { ...cnode, ...(await this.getItemProperties(cnode.expressID)) }
|
||||
cnode.id = await this.populateSpatialNode(cnode, chunks, closures)
|
||||
|
||||
for (const closure of closures) {
|
||||
closure.push(cnode.id)
|
||||
if (cnode['closure'].length > 30_000)
|
||||
for (const id of cnode['closure']) closure.push(id)
|
||||
else closure.push(...cnode['closure']) // can stack overflow for large arguments
|
||||
}
|
||||
|
||||
delete cnode.closure
|
||||
nodes.push(cnode)
|
||||
}
|
||||
|
||||
node[prop] = nodes.map((node) => ({
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'reference',
|
||||
referencedId: node.id
|
||||
}))
|
||||
}
|
||||
|
||||
async getItemProperties(id) {
|
||||
if (this.propCache[id]) return this.propCache[id]
|
||||
|
||||
let props = {}
|
||||
const directProps = this.properties[id.toString()]
|
||||
props = { ...directProps }
|
||||
|
||||
const psetIds = []
|
||||
for (let i = 0; i < this.psetRelations.length; i++) {
|
||||
if (this.psetRelations[i].includes(id))
|
||||
psetIds.push(this.psetLines.get(i).toString())
|
||||
}
|
||||
|
||||
const rawPsetIds = psetIds.map((id) =>
|
||||
this.properties[id].RelatingPropertyDefinition.toString()
|
||||
)
|
||||
const rawPsets = rawPsetIds.map((id) => this.properties[id])
|
||||
for (const pset of rawPsets) {
|
||||
props[pset.Name] = this.unpackPsetOrComplexProp(pset)
|
||||
}
|
||||
|
||||
this.propCache[id] = props
|
||||
return props
|
||||
}
|
||||
|
||||
unpackPsetOrComplexProp(pset) {
|
||||
const parsed = {}
|
||||
if (!pset.HasProperties || !Array.isArray(pset.HasProperties)) return parsed
|
||||
for (const id of pset.HasProperties) {
|
||||
const value = this.properties[id.toString()]
|
||||
if (value?.type === 'IFCCOMPLEXPROPERTY') {
|
||||
parsed[value.Name] = this.unpackPsetOrComplexProp(value)
|
||||
} else if (value?.type === 'IFCPROPERTYSINGLEVALUE') {
|
||||
parsed[value.Name] = value.NominalValue
|
||||
}
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
async getSpatialTreeChunks() {
|
||||
const treeChunks = {}
|
||||
await this.getChunks(treeChunks, PropNames.aggregates)
|
||||
await this.getChunks(treeChunks, PropNames.spatial)
|
||||
return treeChunks
|
||||
}
|
||||
|
||||
async getChunks(chunks, propName) {
|
||||
const relation = await this.ifcapi.GetLineIDsWithType(this.modelId, propName.name)
|
||||
for (let i = 0; i < relation.size(); i++) {
|
||||
const rel = await this.ifcapi.GetLine(this.modelId, relation.get(i), false)
|
||||
this.saveChunk(chunks, propName, rel)
|
||||
}
|
||||
}
|
||||
|
||||
saveChunk(chunks, propName, rel) {
|
||||
const relating = rel[propName.relating].value
|
||||
const related = rel[propName.related].map((r) => r.value)
|
||||
if (chunks[relating] === undefined) {
|
||||
chunks[relating] = related
|
||||
} else {
|
||||
chunks[relating] = chunks[relating].concat(related)
|
||||
}
|
||||
}
|
||||
|
||||
async getAllTypesOfModel() {
|
||||
const result = {}
|
||||
const elements = Object.keys(IfcElements).map((e) => parseInt(e))
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i]
|
||||
const lines = await this.ifcapi.GetLineIDsWithType(this.modelId, element)
|
||||
const size = lines.size()
|
||||
for (let i = 0; i < size; i++) result[lines.get(i)] = element
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async getAllProps() {
|
||||
const psetLines = this.ifcapi.GetLineIDsWithType(
|
||||
this.modelId,
|
||||
WebIFC.IFCRELDEFINESBYPROPERTIES
|
||||
)
|
||||
const psetRelations = []
|
||||
const properties = {}
|
||||
|
||||
const geometryIds = await this.getAllGeometriesIds()
|
||||
const allLinesIDs = await this.ifcapi.GetAllLines(this.modelId)
|
||||
const allLinesCount = allLinesIDs.size()
|
||||
for (let i = 0; i < allLinesCount; i++) {
|
||||
this.logger.debug(`${((i / allLinesCount) * 100).toFixed(3)}% props.`)
|
||||
const id = allLinesIDs.get(i)
|
||||
if (!geometryIds.has(id)) {
|
||||
const props = await this.getItemProperty(id)
|
||||
if (props) {
|
||||
if (props.type === 'IFCRELDEFINESBYPROPERTIES' && props.RelatedObjects) {
|
||||
psetRelations.push(props.RelatedObjects)
|
||||
}
|
||||
properties[id] = props
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { psetLines, psetRelations, properties }
|
||||
}
|
||||
|
||||
async getItemProperty(id) {
|
||||
try {
|
||||
const props = await this.ifcapi.GetLine(this.modelId, id)
|
||||
if (props.type) {
|
||||
props.type = IfcTypesMap[props.type]
|
||||
}
|
||||
this.inPlaceFormatItemProperties(props)
|
||||
return props
|
||||
} catch (e) {
|
||||
this.logger.error(e, `There was an issue getting props of id ${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
inPlaceFormatItemProperties(props) {
|
||||
Object.keys(props).forEach((key) => {
|
||||
const value = props[key]
|
||||
if (value && value.value !== undefined) props[key] = value.value
|
||||
else if (Array.isArray(value))
|
||||
props[key] = value.map((item) => {
|
||||
if (item && item.value) return item.value
|
||||
return item
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
createNode(id) {
|
||||
const typeName = this.getNodeType(id)
|
||||
return {
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: typeName,
|
||||
expressID: id,
|
||||
type: typeName,
|
||||
elements: [],
|
||||
properties: null
|
||||
}
|
||||
}
|
||||
|
||||
getNodeType(id) {
|
||||
const typeID = this.types[id]
|
||||
return IfcElements[typeID]
|
||||
}
|
||||
|
||||
async getAllGeometriesIds() {
|
||||
const geometriesIds = new Set()
|
||||
const geomTypesArray = Array.from(GeometryTypes)
|
||||
for (let i = 0; i < geomTypesArray.length; i++) {
|
||||
const category = geomTypesArray[i]
|
||||
const ids = await this.ifcapi.GetLineIDsWithType(this.modelId, category)
|
||||
const idsSize = ids.size()
|
||||
for (let j = 0; j < idsSize; j++) {
|
||||
geometriesIds.add(ids.get(j))
|
||||
}
|
||||
}
|
||||
this.geometryIdsCount = geometriesIds.size
|
||||
return geometriesIds
|
||||
}
|
||||
|
||||
async createAndSaveMeshes() {
|
||||
const geometryReferences = {}
|
||||
let count = 0
|
||||
const speckleMeshes = []
|
||||
|
||||
this.ifcapi.StreamAllMeshes(this.modelId, async (mesh) => {
|
||||
const placedGeometries = mesh.geometries
|
||||
geometryReferences[mesh.expressID] = []
|
||||
for (let i = 0; i < placedGeometries.size(); i++) {
|
||||
const placedGeometry = placedGeometries.get(i)
|
||||
const geometry = this.ifcapi.GetGeometry(
|
||||
this.modelId,
|
||||
placedGeometry.geometryExpressID
|
||||
)
|
||||
|
||||
const verts = [
|
||||
...this.ifcapi.GetVertexArray(
|
||||
geometry.GetVertexData(),
|
||||
geometry.GetVertexDataSize()
|
||||
)
|
||||
]
|
||||
|
||||
const indices = [
|
||||
...this.ifcapi.GetIndexArray(
|
||||
geometry.GetIndexData(),
|
||||
geometry.GetIndexDataSize()
|
||||
)
|
||||
]
|
||||
|
||||
const { vertices } = this.extractVertexData(
|
||||
verts,
|
||||
placedGeometry.flatTransformation
|
||||
)
|
||||
const faces = this.extractFaces(indices)
|
||||
|
||||
const speckleMesh = {
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'Objects.Geometry.Mesh',
|
||||
units: 'm',
|
||||
volume: 0,
|
||||
area: 0,
|
||||
// random: Math.random(), // TODO: remove, this is here just for performance benchmarking/explicit cache poisoning
|
||||
vertices,
|
||||
faces,
|
||||
renderMaterial: placedGeometry.color
|
||||
? this.colorToMaterial(placedGeometry.color)
|
||||
: null
|
||||
}
|
||||
|
||||
speckleMesh.id = getHash(speckleMesh)
|
||||
// Note: the web-ifc api disposes of the data post callback, and doesn't know that it's async;
|
||||
// we cannot and should not await things in here. I'm not entirely sure what's going on :)
|
||||
// await this.serverApi.saveObject(speckleMesh)
|
||||
|
||||
speckleMeshes.push(speckleMesh)
|
||||
geometryReferences[mesh.expressID].push({
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'reference',
|
||||
referencedId: speckleMesh.id
|
||||
})
|
||||
this.logger.debug(`${(count++).toFixed(3)} geoms generated.`)
|
||||
}
|
||||
})
|
||||
|
||||
await this.serverApi.saveObjectBatch(speckleMeshes)
|
||||
return geometryReferences
|
||||
}
|
||||
|
||||
extractFaces(indices) {
|
||||
const faces = []
|
||||
for (let i = 0; i < indices.length; i++) {
|
||||
if (i % 3 === 0) faces.push(0)
|
||||
faces.push(indices[i])
|
||||
}
|
||||
return faces
|
||||
}
|
||||
|
||||
extractVertexData(vertexData, matrix) {
|
||||
const vertices = []
|
||||
const normals = []
|
||||
let isNormalData = false
|
||||
for (let i = 0; i < vertexData.length; i++) {
|
||||
isNormalData ? normals.push(vertexData[i]) : vertices.push(vertexData[i])
|
||||
if ((i + 1) % 3 === 0) isNormalData = !isNormalData
|
||||
}
|
||||
|
||||
// apply the transform
|
||||
for (let k = 0; k < vertices.length; k += 3) {
|
||||
const x = vertices[k],
|
||||
y = vertices[k + 1],
|
||||
z = vertices[k + 2]
|
||||
vertices[k] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12]
|
||||
vertices[k + 1] =
|
||||
(matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]) * -1
|
||||
vertices[k + 2] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13]
|
||||
}
|
||||
|
||||
return { vertices, normals }
|
||||
}
|
||||
|
||||
colorToMaterial(color) {
|
||||
const intColor = Math.floor(
|
||||
((color.w * 255) << 24) +
|
||||
((color.x * 255) << 16) +
|
||||
((color.y * 255) << 8) +
|
||||
color.z * 255
|
||||
)
|
||||
const material = {
|
||||
diffuse: intColor,
|
||||
opacity: color.w,
|
||||
metalness: 0,
|
||||
roughness: 1,
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: 'Objects.Other.RenderMaterial'
|
||||
}
|
||||
material.id = getHash(material)
|
||||
return material
|
||||
}
|
||||
}
|
||||
Executable → Regular
@@ -1,18 +1,61 @@
|
||||
/* eslint-disable camelcase */
|
||||
'use strict'
|
||||
|
||||
module.exports = require('knex')({
|
||||
client: 'pg',
|
||||
connection: {
|
||||
application_name: 'speckle_fileimport_service',
|
||||
connectionString:
|
||||
process.env.PG_CONNECTION_STRING || 'postgres://speckle:speckle@127.0.0.1/speckle'
|
||||
},
|
||||
pool: {
|
||||
min: 0,
|
||||
max: parseInt(process.env.POSTGRES_MAX_CONNECTIONS_FILE_IMPORT_SERVICE) || 1,
|
||||
acquireTimeoutMillis: 16000, //allows for 3x creation attempts plus idle time between attempts
|
||||
createTimeoutMillis: 5000
|
||||
const Environment = require('@speckle/shared/dist/commonjs/environment/index.js')
|
||||
const {
|
||||
loadMultiRegionsConfig,
|
||||
configureKnexClient
|
||||
} = require('@speckle/shared/dist/commonjs/environment/multiRegionConfig.js')
|
||||
const { logger } = require('./observability/logging')
|
||||
|
||||
const { FF_WORKSPACES_MULTI_REGION_ENABLED } = Environment.getFeatureFlags()
|
||||
|
||||
const isDevEnv = process.env.NODE_ENV !== 'production'
|
||||
|
||||
let dbClients
|
||||
const getDbClients = async () => {
|
||||
if (dbClients) return dbClients
|
||||
const maxConnections = parseInt(
|
||||
process.env['POSTGRES_MAX_CONNECTIONS_FILE_IMPORT_SERVICE'] || '1'
|
||||
)
|
||||
const connectionAcquireTimeoutMillis = parseInt(
|
||||
process.env['POSTGRES_CONNECTION_ACQUIRE_TIMEOUT_MILLIS'] || '16000'
|
||||
)
|
||||
const connectionCreateTimeoutMillis = parseInt(
|
||||
process.env['POSTGRES_CONNECTION_CREATE_TIMEOUT_MILLIS'] || '5000'
|
||||
)
|
||||
|
||||
const configArgs = {
|
||||
migrationDirs: [],
|
||||
isTestEnv: isDevEnv,
|
||||
isDevOrTestEnv: isDevEnv,
|
||||
logger,
|
||||
maxConnections,
|
||||
applicationName: 'speckle_fileimport_service',
|
||||
connectionAcquireTimeoutMillis,
|
||||
connectionCreateTimeoutMillis
|
||||
}
|
||||
// migrations are in managed in the server package
|
||||
})
|
||||
if (!FF_WORKSPACES_MULTI_REGION_ENABLED) {
|
||||
const mainClient = configureKnexClient(
|
||||
{
|
||||
postgres: {
|
||||
connectionUri:
|
||||
process.env.PG_CONNECTION_STRING ||
|
||||
'postgres://speckle:speckle@127.0.0.1/speckle'
|
||||
}
|
||||
},
|
||||
configArgs
|
||||
)
|
||||
dbClients = { main: mainClient }
|
||||
} else {
|
||||
const configPath = process.env.MULTI_REGION_CONFIG_PATH || 'multiregion.json'
|
||||
const config = await loadMultiRegionsConfig({ path: configPath })
|
||||
const clients = [['main', configureKnexClient(config.main, configArgs)]]
|
||||
Object.entries(config.regions).map(([key, config]) => {
|
||||
clients.push([key, configureKnexClient(config, configArgs)])
|
||||
})
|
||||
dbClients = Object.fromEntries(clients)
|
||||
}
|
||||
return dbClients
|
||||
}
|
||||
|
||||
module.exports = getDbClients
|
||||
|
||||
@@ -37,7 +37,6 @@ structlog.configure(
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
LOG = structlog.get_logger()
|
||||
TMP_RESULTS_PATH = "/tmp/import_result.json"
|
||||
DEFAULT_BRANCH = "uploads"
|
||||
|
||||
|
||||
@@ -57,7 +56,17 @@ def convert_material(obj_mat):
|
||||
|
||||
|
||||
def import_obj():
|
||||
file_path, _, stream_id, branch_name, commit_message = sys.argv[1:]
|
||||
(
|
||||
file_path,
|
||||
tmp_results_path,
|
||||
_,
|
||||
stream_id,
|
||||
branch_name,
|
||||
commit_message,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
) = sys.argv[1:]
|
||||
LOG.info("ImportOBJ argv[1:]:%s", sys.argv[1:])
|
||||
|
||||
# Parse input
|
||||
@@ -149,19 +158,21 @@ def import_obj():
|
||||
source_application="OBJ",
|
||||
)
|
||||
|
||||
return commit_id
|
||||
return commit_id, tmp_results_path
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
commit_id = import_obj()
|
||||
commit_id, tmp_results_path = import_obj()
|
||||
if not commit_id:
|
||||
raise Exception("Can't create commit")
|
||||
if isinstance(commit_id, Exception):
|
||||
raise commit_id
|
||||
results = {"success": True, "commitId": commit_id}
|
||||
except Exception as ex:
|
||||
LOG.exception(ex)
|
||||
results = {"success": False, "error": str(ex)}
|
||||
|
||||
Path(TMP_RESULTS_PATH).write_text(json.dumps(results))
|
||||
Path(tmp_results_path).write_text(json.dumps(results))
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"private": true,
|
||||
"version": "2.5.4",
|
||||
"description": "Parse and import files of various types into a stream",
|
||||
"author": "Dimitrie Stefanescu <didimitrie@gmail.com>",
|
||||
"author": "Speckle Systems <hello@speckle.systems>",
|
||||
"homepage": "https://github.com/specklesystems/speckle-server#readme",
|
||||
"license": "SEE LICENSE IN readme.md",
|
||||
"main": "index.js",
|
||||
"main": "daemon.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/specklesystems/speckle-server.git"
|
||||
@@ -26,7 +26,7 @@
|
||||
"@speckle/shared": "workspace:^",
|
||||
"bcrypt": "^5.0.1",
|
||||
"crypto-random-string": "^3.3.1",
|
||||
"knex": "^2.4.1",
|
||||
"knex": "^2.5.1",
|
||||
"pg": "^8.7.3",
|
||||
"pino": "^8.7.0",
|
||||
"pino-http": "^8.0.0",
|
||||
@@ -34,7 +34,9 @@
|
||||
"prom-client": "^14.0.1",
|
||||
"undici": "^5.28.4",
|
||||
"valid-filename": "^3.1.0",
|
||||
"web-ifc": "^0.0.36"
|
||||
"web-ifc": "^0.0.36",
|
||||
"znv": "^0.4.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
|
||||
@@ -5,17 +5,21 @@ const bcrypt = require('bcrypt')
|
||||
const { chunk } = require('lodash')
|
||||
const { logger: parentLogger } = require('../observability/logging')
|
||||
|
||||
const knex = require('../knex')
|
||||
const Observability = require('@speckle/shared/dist/commonjs/observability/index.js')
|
||||
const Streams = () => knex('streams')
|
||||
const Branches = () => knex('branches')
|
||||
const Objects = () => knex('objects')
|
||||
const Closures = () => knex('object_children_closure')
|
||||
const ApiTokens = () => knex('api_tokens')
|
||||
const TokenScopes = () => knex('token_scopes')
|
||||
|
||||
const tables = (db) => ({
|
||||
objects: db('objects'),
|
||||
closures: db('object_children_closure'),
|
||||
branches: db('branches'),
|
||||
streams: db('streams'),
|
||||
apiTokens: db('api_tokens'),
|
||||
tokenScopes: db('token_scopes')
|
||||
})
|
||||
|
||||
module.exports = class ServerAPI {
|
||||
constructor({ streamId, logger }) {
|
||||
constructor({ db, streamId, logger }) {
|
||||
this.tables = tables(db)
|
||||
this.db = db
|
||||
this.streamId = streamId
|
||||
this.isSending = false
|
||||
this.buffer = []
|
||||
@@ -68,10 +72,10 @@ module.exports = class ServerAPI {
|
||||
totalChildrenCountByDepth
|
||||
)
|
||||
|
||||
await Objects().insert(insertionObject).onConflict().ignore()
|
||||
await this.tables.objects.insert(insertionObject).onConflict().ignore()
|
||||
|
||||
if (closures.length > 0) {
|
||||
await Closures().insert(closures).onConflict().ignore()
|
||||
await this.tables.closures.insert(closures).onConflict().ignore()
|
||||
}
|
||||
|
||||
return insertionObject.id
|
||||
@@ -123,7 +127,7 @@ module.exports = class ServerAPI {
|
||||
const batches = chunk(objsToInsert, objectsBatchSize)
|
||||
for (const [index, batch] of batches.entries()) {
|
||||
this.prepInsertionObjectBatch(batch)
|
||||
await Objects().insert(batch).onConflict().ignore()
|
||||
await this.tables.objects.insert(batch).onConflict().ignore()
|
||||
this.logger.info(
|
||||
{
|
||||
currentBatchCount: batch.length,
|
||||
@@ -141,7 +145,7 @@ module.exports = class ServerAPI {
|
||||
|
||||
for (const [index, batch] of batches.entries()) {
|
||||
this.prepInsertionClosureBatch(batch)
|
||||
await Closures().insert(batch).onConflict().ignore()
|
||||
await this.tables.closures.insert(batch).onConflict().ignore()
|
||||
this.logger.info(
|
||||
{
|
||||
currentBatchCount: batch.length,
|
||||
@@ -196,10 +200,10 @@ module.exports = class ServerAPI {
|
||||
}
|
||||
|
||||
async getBranchByNameAndStreamId({ streamId, name }) {
|
||||
const query = Branches()
|
||||
const query = this.tables.branches
|
||||
.select('*')
|
||||
.where({ streamId })
|
||||
.andWhere(knex.raw('LOWER(name) = ?', [name]))
|
||||
.andWhere(this.db.raw('LOWER(name) = ?', [name]))
|
||||
.first()
|
||||
return await query
|
||||
}
|
||||
@@ -212,10 +216,12 @@ module.exports = class ServerAPI {
|
||||
branch.name = name.toLowerCase()
|
||||
branch.description = description
|
||||
|
||||
await Branches().returning('id').insert(branch)
|
||||
await this.tables.branches.returning('id').insert(branch)
|
||||
|
||||
// update stream updated at
|
||||
await Streams().where({ id: streamId }).update({ updatedAt: knex.fn.now() })
|
||||
await this.tables.streams
|
||||
.where({ id: streamId })
|
||||
.update({ updatedAt: this.db.fn.now() })
|
||||
|
||||
return branch.id
|
||||
}
|
||||
@@ -244,14 +250,14 @@ module.exports = class ServerAPI {
|
||||
}
|
||||
const tokenScopes = scopes.map((scope) => ({ tokenId, scopeName: scope }))
|
||||
|
||||
await ApiTokens().insert(token)
|
||||
await TokenScopes().insert(tokenScopes)
|
||||
await this.tables.apiTokens.insert(token)
|
||||
await this.tables.tokenScopes.insert(tokenScopes)
|
||||
|
||||
return { id: tokenId, token: tokenId + tokenString }
|
||||
}
|
||||
|
||||
async revokeTokenById(tokenId) {
|
||||
const delCount = await ApiTokens()
|
||||
const delCount = await this.tables.apiTokens
|
||||
.where({ id: tokenId.slice(0, 10) })
|
||||
.del()
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
'use strict'
|
||||
|
||||
const Environment = require('@speckle/shared/dist/commonjs/environment/index.js')
|
||||
const {
|
||||
initPrometheusMetrics,
|
||||
metricDuration,
|
||||
metricInputFileSize,
|
||||
metricOperationErrors
|
||||
} = require('./prometheusMetrics')
|
||||
const knex = require('../knex')
|
||||
const FileUploads = () => knex('file_uploads')
|
||||
const getDbClients = require('../knex')
|
||||
|
||||
const { downloadFile } = require('./filesApi')
|
||||
const fs = require('fs')
|
||||
const { spawn } = require('child_process')
|
||||
|
||||
const ServerAPI = require('../ifc/api')
|
||||
const ServerAPI = require('./api')
|
||||
const objDependencies = require('./objDependencies')
|
||||
const { logger } = require('../observability/logging')
|
||||
const { Scopes } = require('@speckle/shared')
|
||||
const { Scopes, wait } = require('@speckle/shared')
|
||||
const { FF_FILEIMPORT_IFC_DOTNET_ENABLED } = Environment.getFeatureFlags()
|
||||
|
||||
const HEALTHCHECK_FILE_PATH = '/tmp/last_successful_query'
|
||||
|
||||
@@ -31,10 +32,10 @@ let TIME_LIMIT = 10 * 60 * 1000
|
||||
const providedTimeLimit = parseInt(process.env.FILE_IMPORT_TIME_LIMIT_MIN)
|
||||
if (providedTimeLimit) TIME_LIMIT = providedTimeLimit * 60 * 1000
|
||||
|
||||
async function startTask() {
|
||||
async function startTask(knex) {
|
||||
const { rows } = await knex.raw(`
|
||||
UPDATE file_uploads
|
||||
SET
|
||||
SET
|
||||
"convertedStatus" = 1,
|
||||
"convertedLastUpdate" = NOW()
|
||||
FROM (
|
||||
@@ -49,15 +50,16 @@ async function startTask() {
|
||||
return rows[0]
|
||||
}
|
||||
|
||||
async function doTask(task) {
|
||||
async function doTask(mainDb, regionName, taskDb, task) {
|
||||
const taskId = task.id
|
||||
|
||||
// Mark task as started
|
||||
await knex.raw(`NOTIFY file_import_started, '${task.id}'`)
|
||||
await mainDb.raw(`NOTIFY file_import_started, '${task.id}'`)
|
||||
|
||||
let taskLogger = logger.child({ taskId })
|
||||
let tempUserToken = null
|
||||
let serverApi = null
|
||||
let mainServerApi = null
|
||||
let taskServerApi = null
|
||||
let fileTypeForMetric = 'unknown'
|
||||
let fileSizeForMetric = 0
|
||||
|
||||
@@ -67,7 +69,7 @@ async function doTask(task) {
|
||||
|
||||
try {
|
||||
taskLogger.info("Doing task '{taskId}'.")
|
||||
const info = await FileUploads().where({ id: taskId }).first()
|
||||
const info = await taskDb('file_uploads').where({ id: taskId }).first()
|
||||
if (!info) {
|
||||
throw new Error('Internal error: DB inconsistent')
|
||||
}
|
||||
@@ -80,26 +82,38 @@ async function doTask(task) {
|
||||
fileName: info.fileName,
|
||||
fileSize: fileSizeForMetric,
|
||||
userId: info.userId,
|
||||
streamId: info.streamId,
|
||||
branchName: info.branchName
|
||||
projectId: info.streamId,
|
||||
modelName: info.branchName
|
||||
})
|
||||
fs.mkdirSync(TMP_INPUT_DIR, { recursive: true })
|
||||
|
||||
serverApi = new ServerAPI({ streamId: info.streamId, logger: taskLogger })
|
||||
mainServerApi = new ServerAPI({
|
||||
db: mainDb,
|
||||
streamId: info.streamId,
|
||||
logger: taskLogger
|
||||
})
|
||||
taskServerApi = new ServerAPI({
|
||||
db: taskDb,
|
||||
streamId: info.streamId,
|
||||
logger: taskLogger
|
||||
})
|
||||
|
||||
branchMetadata = {
|
||||
branchName: info.branchName,
|
||||
streamId: info.streamId
|
||||
}
|
||||
const existingBranch = await serverApi.getBranchByNameAndStreamId({
|
||||
const existingBranch = await taskServerApi.getBranchByNameAndStreamId({
|
||||
streamId: info.streamId,
|
||||
name: info.branchName
|
||||
})
|
||||
if (!existingBranch) {
|
||||
newBranchCreated = true
|
||||
}
|
||||
taskLogger = taskLogger.child({
|
||||
modelId: existingBranch?.id
|
||||
})
|
||||
|
||||
const { token } = await serverApi.createToken({
|
||||
const { token } = await mainServerApi.createToken({
|
||||
userId: info.userId,
|
||||
name: 'temp upload token',
|
||||
scopes: [Scopes.Streams.Write, Scopes.Streams.Read],
|
||||
@@ -115,24 +129,47 @@ async function doTask(task) {
|
||||
})
|
||||
|
||||
if (info.fileType.toLowerCase() === 'ifc') {
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
process.env['NODE_BINARY_PATH'] || 'node',
|
||||
[
|
||||
'--no-experimental-fetch',
|
||||
'./ifc/import_file.js',
|
||||
TMP_FILE_PATH,
|
||||
info.userId,
|
||||
info.streamId,
|
||||
info.branchName,
|
||||
`File upload: ${info.fileName}`,
|
||||
info.id
|
||||
],
|
||||
{
|
||||
USER_TOKEN: tempUserToken
|
||||
},
|
||||
TIME_LIMIT
|
||||
)
|
||||
if (FF_FILEIMPORT_IFC_DOTNET_ENABLED) {
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
process.env['DOTNET_BINARY_PATH'] || 'dotnet',
|
||||
[
|
||||
'/speckle-server/packages/fileimport-service/ifc-dotnet/ifc-converter.dll',
|
||||
TMP_FILE_PATH,
|
||||
TMP_RESULTS_PATH,
|
||||
info.streamId,
|
||||
`File upload: ${info.fileName}`,
|
||||
existingBranch?.id || '',
|
||||
regionName
|
||||
],
|
||||
{
|
||||
USER_TOKEN: tempUserToken
|
||||
},
|
||||
TIME_LIMIT
|
||||
)
|
||||
} else {
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
process.env['NODE_BINARY_PATH'] || 'node',
|
||||
[
|
||||
'--no-experimental-fetch',
|
||||
'./ifc/import_file.js',
|
||||
TMP_FILE_PATH,
|
||||
TMP_RESULTS_PATH,
|
||||
info.userId,
|
||||
info.streamId,
|
||||
info.branchName,
|
||||
`File upload: ${info.fileName}`,
|
||||
info.id,
|
||||
existingBranch?.id || '',
|
||||
regionName
|
||||
],
|
||||
{
|
||||
USER_TOKEN: tempUserToken
|
||||
},
|
||||
TIME_LIMIT
|
||||
)
|
||||
}
|
||||
} else if (info.fileType.toLowerCase() === 'stl') {
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
@@ -140,10 +177,14 @@ async function doTask(task) {
|
||||
[
|
||||
'./stl/import_file.py',
|
||||
TMP_FILE_PATH,
|
||||
TMP_RESULTS_PATH,
|
||||
info.userId,
|
||||
info.streamId,
|
||||
info.branchName,
|
||||
`File upload: ${info.fileName}`
|
||||
`File upload: ${info.fileName}`,
|
||||
info.id,
|
||||
existingBranch?.id || '',
|
||||
regionName
|
||||
],
|
||||
{
|
||||
USER_TOKEN: tempUserToken
|
||||
@@ -165,10 +206,14 @@ async function doTask(task) {
|
||||
'-u',
|
||||
'./obj/import_file.py',
|
||||
TMP_FILE_PATH,
|
||||
TMP_RESULTS_PATH,
|
||||
info.userId,
|
||||
info.streamId,
|
||||
info.branchName,
|
||||
`File upload: ${info.fileName}`
|
||||
`File upload: ${info.fileName}`,
|
||||
info.id,
|
||||
existingBranch?.id || '',
|
||||
regionName
|
||||
],
|
||||
{
|
||||
USER_TOKEN: tempUserToken
|
||||
@@ -185,7 +230,7 @@ async function doTask(task) {
|
||||
|
||||
const commitId = output.commitId
|
||||
|
||||
await knex.raw(
|
||||
await taskDb.raw(
|
||||
`
|
||||
UPDATE file_uploads
|
||||
SET
|
||||
@@ -199,7 +244,7 @@ async function doTask(task) {
|
||||
)
|
||||
} catch (err) {
|
||||
taskLogger.error(err)
|
||||
await knex.raw(
|
||||
await taskDb.raw(
|
||||
`
|
||||
UPDATE file_uploads
|
||||
SET
|
||||
@@ -208,12 +253,13 @@ async function doTask(task) {
|
||||
"convertedMessage" = ?
|
||||
WHERE "id" = ?
|
||||
`,
|
||||
[err.toString(), task.id]
|
||||
// DB only accepts a varchar 255
|
||||
[err.toString().substring(0, 254), task.id]
|
||||
)
|
||||
metricOperationErrors.labels(fileTypeForMetric).inc()
|
||||
} finally {
|
||||
const { streamId, branchName } = branchMetadata
|
||||
await knex.raw(
|
||||
await mainDb.raw(
|
||||
`NOTIFY file_import_update, '${task.id}:::${streamId}:::${branchName}:::${
|
||||
newBranchCreated ? 1 : 0
|
||||
}'`
|
||||
@@ -226,7 +272,7 @@ async function doTask(task) {
|
||||
if (fs.existsSync(TMP_RESULTS_PATH)) fs.unlinkSync(TMP_RESULTS_PATH)
|
||||
|
||||
if (tempUserToken) {
|
||||
await serverApi.revokeTokenById(tempUserToken)
|
||||
await mainServerApi.revokeTokenById(tempUserToken)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,42 +351,53 @@ function wrapLogLine(line, isErr, logger) {
|
||||
logger.info({ parserLogLine: line }, 'ParserLog: {parserLogLine}')
|
||||
}
|
||||
|
||||
async function tick() {
|
||||
if (shouldExit) {
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
try {
|
||||
const task = await startTask()
|
||||
|
||||
fs.writeFile(HEALTHCHECK_FILE_PATH, '' + Date.now(), () => {})
|
||||
|
||||
if (!task) {
|
||||
setTimeout(tick, 1000)
|
||||
return
|
||||
const doStuff = async () => {
|
||||
const dbClients = await getDbClients()
|
||||
const mainDb = dbClients.main.public
|
||||
const dbClientsIterator = infiniteDbClientsIterator(dbClients)
|
||||
while (!shouldExit) {
|
||||
const [regionName, taskDb] = dbClientsIterator.next().value
|
||||
try {
|
||||
const task = await startTask(taskDb)
|
||||
fs.writeFile(HEALTHCHECK_FILE_PATH, '' + Date.now(), () => {})
|
||||
if (!task) {
|
||||
await wait(1000)
|
||||
continue
|
||||
}
|
||||
await doTask(mainDb, regionName, taskDb, task)
|
||||
await wait(10)
|
||||
} catch (err) {
|
||||
metricOperationErrors.labels('main_loop').inc()
|
||||
logger.error(err, 'Error executing task')
|
||||
await wait(5000)
|
||||
}
|
||||
|
||||
await doTask(task)
|
||||
|
||||
// Check for another task very soon
|
||||
setTimeout(tick, 10)
|
||||
} catch (err) {
|
||||
metricOperationErrors.labels('main_loop').inc()
|
||||
logger.error(err, 'Error executing task')
|
||||
setTimeout(tick, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
logger.info('Starting FileUploads Service...')
|
||||
initPrometheusMetrics()
|
||||
await initPrometheusMetrics()
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
shouldExit = true
|
||||
logger.info('Shutting down...')
|
||||
})
|
||||
|
||||
tick()
|
||||
await doStuff()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
function* infiniteDbClientsIterator(dbClients) {
|
||||
let index = 0
|
||||
const dbClientEntries = [...Object.entries(dbClients)]
|
||||
const clientCount = dbClientEntries.length
|
||||
while (true) {
|
||||
// reset index
|
||||
if (index === clientCount) index = 0
|
||||
const [regionName, dbConnection] = dbClientEntries[index]
|
||||
index++
|
||||
yield [regionName, dbConnection.public]
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
const http = require('http')
|
||||
const prometheusClient = require('prom-client')
|
||||
const knex = require('../knex')
|
||||
const getDbClients = require('../knex')
|
||||
|
||||
let metricFree = null
|
||||
let metricUsed = null
|
||||
@@ -24,101 +24,105 @@ prometheusClient.collectDefaultMetrics()
|
||||
|
||||
let prometheusInitialized = false
|
||||
|
||||
function initKnexPrometheusMetrics() {
|
||||
metricFree = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_free',
|
||||
help: 'Number of free DB connections',
|
||||
collect() {
|
||||
this.set(knex.client.pool.numFree())
|
||||
}
|
||||
})
|
||||
const initDBPrometheusMetricsFactory =
|
||||
({ db }) =>
|
||||
() => {
|
||||
metricFree = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_free',
|
||||
help: 'Number of free DB connections',
|
||||
collect() {
|
||||
this.set(db.client.pool.numFree())
|
||||
}
|
||||
})
|
||||
|
||||
metricUsed = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_used',
|
||||
help: 'Number of used DB connections',
|
||||
collect() {
|
||||
this.set(knex.client.pool.numUsed())
|
||||
}
|
||||
})
|
||||
metricUsed = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_used',
|
||||
help: 'Number of used DB connections',
|
||||
collect() {
|
||||
this.set(db.client.pool.numUsed())
|
||||
}
|
||||
})
|
||||
|
||||
metricPendingAquires = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_pending',
|
||||
help: 'Number of pending DB connection aquires',
|
||||
collect() {
|
||||
this.set(knex.client.pool.numPendingAcquires())
|
||||
}
|
||||
})
|
||||
metricPendingAquires = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_pending',
|
||||
help: 'Number of pending DB connection aquires',
|
||||
collect() {
|
||||
this.set(db.client.pool.numPendingAcquires())
|
||||
}
|
||||
})
|
||||
|
||||
metricPendingCreates = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_pending_creates',
|
||||
help: 'Number of pending DB connection creates',
|
||||
collect() {
|
||||
this.set(knex.client.pool.numPendingCreates())
|
||||
}
|
||||
})
|
||||
metricPendingCreates = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_pending_creates',
|
||||
help: 'Number of pending DB connection creates',
|
||||
collect() {
|
||||
this.set(db.client.pool.numPendingCreates())
|
||||
}
|
||||
})
|
||||
|
||||
metricPendingValidations = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_pending_validations',
|
||||
help: 'Number of pending DB connection validations. This is a state between pending acquisition and acquiring a connection.',
|
||||
collect() {
|
||||
this.set(knex.client.pool.numPendingValidations())
|
||||
}
|
||||
})
|
||||
metricPendingValidations = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_pending_validations',
|
||||
help: 'Number of pending DB connection validations. This is a state between pending acquisition and acquiring a connection.',
|
||||
collect() {
|
||||
this.set(db.client.pool.numPendingValidations())
|
||||
}
|
||||
})
|
||||
|
||||
metricRemainingCapacity = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_remaining_capacity',
|
||||
help: 'Remaining capacity of the DB connection pool',
|
||||
collect() {
|
||||
const postgresMaxConnections =
|
||||
parseInt(process.env.POSTGRES_MAX_CONNECTIONS_FILE_IMPORT_SERVICE) || 1
|
||||
const demand =
|
||||
knex.client.pool.numUsed() +
|
||||
knex.client.pool.numPendingCreates() +
|
||||
knex.client.pool.numPendingValidations() +
|
||||
knex.client.pool.numPendingAcquires()
|
||||
metricRemainingCapacity = new prometheusClient.Gauge({
|
||||
name: 'speckle_server_knex_remaining_capacity',
|
||||
help: 'Remaining capacity of the DB connection pool',
|
||||
collect() {
|
||||
const postgresMaxConnections =
|
||||
parseInt(process.env.POSTGRES_MAX_CONNECTIONS_FILE_IMPORT_SERVICE) || 1
|
||||
const demand =
|
||||
db.client.pool.numUsed() +
|
||||
db.client.pool.numPendingCreates() +
|
||||
db.client.pool.numPendingValidations() +
|
||||
db.client.pool.numPendingAcquires()
|
||||
|
||||
this.set(Math.max(postgresMaxConnections - demand, 0))
|
||||
}
|
||||
})
|
||||
this.set(Math.max(postgresMaxConnections - demand, 0))
|
||||
}
|
||||
})
|
||||
|
||||
metricQueryDuration = new prometheusClient.Summary({
|
||||
name: 'speckle_server_knex_query_duration',
|
||||
help: 'Summary of the DB query durations in seconds'
|
||||
})
|
||||
metricQueryDuration = new prometheusClient.Summary({
|
||||
name: 'speckle_server_knex_query_duration',
|
||||
help: 'Summary of the DB query durations in seconds'
|
||||
})
|
||||
|
||||
metricQueryErrors = new prometheusClient.Counter({
|
||||
name: 'speckle_server_knex_query_errors',
|
||||
help: 'Number of DB queries with errors'
|
||||
})
|
||||
metricQueryErrors = new prometheusClient.Counter({
|
||||
name: 'speckle_server_knex_query_errors',
|
||||
help: 'Number of DB queries with errors'
|
||||
})
|
||||
|
||||
knex.on('query', (data) => {
|
||||
const queryId = data.__knexQueryUid + ''
|
||||
queryStartTime[queryId] = Date.now()
|
||||
})
|
||||
db.on('query', (data) => {
|
||||
const queryId = data.__knexQueryUid + ''
|
||||
queryStartTime[queryId] = Date.now()
|
||||
})
|
||||
|
||||
knex.on('query-response', (data, obj, builder) => {
|
||||
const queryId = obj.__knexQueryUid + ''
|
||||
const durationSec = (Date.now() - queryStartTime[queryId]) / 1000
|
||||
delete queryStartTime[queryId]
|
||||
if (!isNaN(durationSec)) metricQueryDuration.observe(durationSec)
|
||||
})
|
||||
db.on('query-response', (data, obj, builder) => {
|
||||
const queryId = obj.__knexQueryUid + ''
|
||||
const durationSec = (Date.now() - queryStartTime[queryId]) / 1000
|
||||
delete queryStartTime[queryId]
|
||||
if (!isNaN(durationSec)) metricQueryDuration.observe(durationSec)
|
||||
})
|
||||
|
||||
knex.on('query-error', (err, querySpec) => {
|
||||
const queryId = querySpec.__knexQueryUid + ''
|
||||
const durationSec = (Date.now() - queryStartTime[queryId]) / 1000
|
||||
delete queryStartTime[queryId]
|
||||
db.on('query-error', (err, querySpec) => {
|
||||
const queryId = querySpec.__knexQueryUid + ''
|
||||
const durationSec = (Date.now() - queryStartTime[queryId]) / 1000
|
||||
delete queryStartTime[queryId]
|
||||
|
||||
if (!isNaN(durationSec)) metricQueryDuration.observe(durationSec)
|
||||
metricQueryErrors.inc()
|
||||
})
|
||||
}
|
||||
if (!isNaN(durationSec)) metricQueryDuration.observe(durationSec)
|
||||
metricQueryErrors.inc()
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initPrometheusMetrics() {
|
||||
async initPrometheusMetrics() {
|
||||
if (prometheusInitialized) return
|
||||
prometheusInitialized = true
|
||||
|
||||
initKnexPrometheusMetrics()
|
||||
const db = (await getDbClients()).main.public
|
||||
|
||||
initDBPrometheusMetricsFactory({ db })()
|
||||
|
||||
// Define the HTTP server
|
||||
const server = http.createServer(async (req, res) => {
|
||||
|
||||
@@ -7,12 +7,21 @@ from specklepy.api import operations
|
||||
|
||||
import sys, os
|
||||
|
||||
TMP_RESULTS_PATH = "/tmp/import_result.json"
|
||||
DEFAULT_BRANCH = "uploads"
|
||||
|
||||
|
||||
def import_stl():
|
||||
file_path, _, stream_id, branch_name, commit_message = sys.argv[1:]
|
||||
(
|
||||
file_path,
|
||||
tmp_results_path,
|
||||
_,
|
||||
stream_id,
|
||||
branch_name,
|
||||
commit_message,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
) = sys.argv[1:]
|
||||
print(f"ImportSTL argv[1:]: {sys.argv[1:]}")
|
||||
|
||||
# Parse input
|
||||
@@ -61,18 +70,20 @@ def import_stl():
|
||||
source_application="STL",
|
||||
)
|
||||
|
||||
return commit_id
|
||||
return commit_id, tmp_results_path
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
commit_id = import_stl()
|
||||
commit_id, tmp_results_path = import_stl()
|
||||
if isinstance(commit_id, Exception):
|
||||
raise commit_id
|
||||
results = {"success": True, "commitId": commit_id}
|
||||
except Exception as ex:
|
||||
results = {"success": False, "error": str(ex)}
|
||||
print(ex)
|
||||
|
||||
print(results)
|
||||
Path(TMP_RESULTS_PATH).write_text(json.dumps(results))
|
||||
Path(tmp_results_path).write_text(json.dumps(results))
|
||||
|
||||
@@ -41,4 +41,4 @@ NUXT_PUBLIC_SURVICATE_WORKSPACE_KEY=
|
||||
NUXT_PUBLIC_ENABLE_DIRECT_PREVIEWS=true
|
||||
|
||||
# Ghost API
|
||||
NUXT_PUBLIC_GHOST_API_KEY=
|
||||
NUXT_PUBLIC_GHOST_API_KEY=
|
||||
@@ -34,14 +34,23 @@ RUN yarn workspaces foreach -W run build
|
||||
RUN find ./packages/frontend-2/.output/ -type f \( -name "*.js.map" -o -name "*.mjs.map" -o -name "*.cjs.map" \) -exec rm -f {} \;
|
||||
|
||||
ENV TINI_VERSION=v0.19.0
|
||||
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
|
||||
RUN chmod +x /tini
|
||||
RUN apt-get update -y \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
--no-install-recommends \
|
||||
ca-certificates=20230311 \
|
||||
curl=7.88.1-10+deb12u8 \
|
||||
&& curl -fsSL https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini -o /tini \
|
||||
&& chmod +x /tini \
|
||||
&& apt-get remove -y curl \
|
||||
&& apt-get autoremove -y \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM gcr.io/distroless/nodejs22-debian12:nonroot@sha256:ed26b3ab750110c51d9dbdfd6c697561dc40a01c296460c3494d47b550ef4126 AS production-stage
|
||||
ARG NODE_ENV=production
|
||||
ENV NODE_ENV=${NODE_ENV}
|
||||
|
||||
COPY --from=build-stage /tini /tini
|
||||
COPY --link --from=build-stage /tini /tini
|
||||
|
||||
ENTRYPOINT ["/tini", "--"]
|
||||
|
||||
@@ -53,7 +62,7 @@ ENV NUXT_PUBLIC_MIXPANEL_TOKEN_ID=acd87c5a50b56df91a795e999812a3a4
|
||||
ENV NUXT_PUBLIC_MIXPANEL_API_HOST=https://analytics.speckle.systems
|
||||
|
||||
WORKDIR /speckle-server
|
||||
COPY --from=build-stage /speckle-server/packages/frontend-2/.output .
|
||||
COPY --link --from=build-stage /speckle-server/packages/frontend-2/.output .
|
||||
|
||||
EXPOSE ${PORT}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from '~~/lib/core/composables/theme'
|
||||
import { useAuthManager } from '~~/lib/auth/composables/auth'
|
||||
import { useFixBraveSafariCookies } from '~~/lib/common/composables/reactiveCookie'
|
||||
|
||||
const { isDarkTheme } = useTheme()
|
||||
|
||||
@@ -30,6 +31,7 @@ useHead({
|
||||
|
||||
const { watchAuthQueryString } = useAuthManager()
|
||||
watchAuthQueryString()
|
||||
useFixBraveSafariCookies()
|
||||
</script>
|
||||
<style>
|
||||
.page-enter-active,
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
<svg width="38" height="37" viewBox="0 0 38 37" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.5695 7.84167C30.4459 7.87905 30.4465 8.05327 30.5704 8.08974L30.6008 8.09869C32.0949 8.53857 33.2612 9.70436 33.6956 11.1923C33.7314 11.3148 33.9059 11.3148 33.9425 11.1925C34.386 9.71452 35.5462 8.55207 37.0303 8.10336L37.0652 8.09282C37.1885 8.05553 37.1877 7.88161 37.064 7.84544L37.0283 7.835C35.5338 7.39799 34.3679 6.23128 33.938 4.74249C33.9028 4.62035 33.7287 4.62043 33.6922 4.7422C33.249 6.21893 32.0893 7.38202 30.6065 7.83048L30.5695 7.84167ZM31.4317 23.8166L31.4317 14.0863H31.4318V10.3168H31.4317V10.3162L26.998 10.3162V10.3168H17.2469L11.255 16.849V26.5794H11.2549V30.349H11.255V30.3494H15.6888V30.349L25.4397 30.349L31.4317 23.8166ZM26.998 26.5794L26.998 14.0863H15.6888L15.6888 26.5794H26.998Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 884 B |
@@ -5,7 +5,7 @@
|
||||
no-shadow
|
||||
class="mx-auto w-full"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div v-if="!workspaceInvite" class="flex flex-col items-center gap-y-2 pb-4">
|
||||
<h1 class="text-heading-xl text-center inline-block">
|
||||
{{ title }}
|
||||
@@ -22,23 +22,29 @@
|
||||
:app-id="appId"
|
||||
:newsletter-consent="false"
|
||||
/>
|
||||
<FormButton
|
||||
v-if="isSsoEnabled"
|
||||
color="outline"
|
||||
full-width
|
||||
size="lg"
|
||||
:to="ssoLoginRoute"
|
||||
>
|
||||
Continue with SSO
|
||||
</FormButton>
|
||||
|
||||
<div class="h-px w-full bg-outline-3 mt-2 shrink-0" />
|
||||
<div>
|
||||
<div
|
||||
v-if="hasLocalStrategy"
|
||||
class="text-center text-foreground-2 mb-2 text-body-2xs font-normal mt-2"
|
||||
>
|
||||
{{ hasThirdPartyStrategies ? 'Or login with your email' : '' }}
|
||||
</div>
|
||||
<AuthLoginWithEmailBlock
|
||||
v-if="hasLocalStrategy"
|
||||
:challenge="challenge"
|
||||
:workspace-invite="workspaceInvite || undefined"
|
||||
/>
|
||||
<div v-if="!forcedInviteEmail" class="text-center text-body-sm">
|
||||
<span class="mr-2">Don't have an account?</span>
|
||||
<CommonTextLink :to="finalRegisterRoute" :icon-right="ArrowRightIcon">
|
||||
Register
|
||||
</CommonTextLink>
|
||||
<div
|
||||
v-if="!forcedInviteEmail"
|
||||
class="text-center text-body-xs text-foreground-3 mt-2 select-none"
|
||||
>
|
||||
Don't have an account?
|
||||
<NuxtLink class="text-foreground" :to="finalRegisterRoute">Sign up</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,8 +56,7 @@ import { useQuery } from '@vue/apollo-composable'
|
||||
import { AuthStrategy } from '~~/lib/auth/helpers/strategies'
|
||||
import { useLoginOrRegisterUtils, useAuthManager } from '~~/lib/auth/composables/auth'
|
||||
import { LayoutDialog } from '@speckle/ui-components'
|
||||
import { ArrowRightIcon } from '@heroicons/vue/20/solid'
|
||||
import { registerRoute } from '~~/lib/common/helpers/route'
|
||||
import { registerRoute, ssoLoginRoute } from '~~/lib/common/helpers/route'
|
||||
import {
|
||||
authLoginPanelQuery,
|
||||
authLoginPanelWorkspaceInviteQuery
|
||||
@@ -65,8 +70,7 @@ const props = withDefaults(
|
||||
}>(),
|
||||
{
|
||||
dialogMode: false,
|
||||
title: 'Speckle login',
|
||||
subtitle: 'Connectivity, Collaboration and Automation for 3D'
|
||||
title: 'Speckle login'
|
||||
}
|
||||
)
|
||||
|
||||
@@ -75,6 +79,7 @@ const { isLoggedIn } = useActiveUser()
|
||||
const { inviteToken } = useAuthManager()
|
||||
const router = useRouter()
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const isSsoEnabled = useIsWorkspacesSsoEnabled()
|
||||
|
||||
const { result } = useQuery(authLoginPanelQuery)
|
||||
|
||||
|
||||
@@ -25,11 +25,6 @@
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<CommonTextLink :to="forgottenPasswordRoute">
|
||||
Forgot your password?
|
||||
</CommonTextLink>
|
||||
</div>
|
||||
<FormButton
|
||||
size="lg"
|
||||
submit
|
||||
@@ -39,6 +34,12 @@
|
||||
>
|
||||
Log in
|
||||
</FormButton>
|
||||
<div class="mt-1 text-center text-body-xs text-foreground-3 select-none">
|
||||
Forgot your password?
|
||||
<NuxtLink :to="forgottenPasswordRoute" class="text-foreground">
|
||||
Reset password
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,34 +1,28 @@
|
||||
<template>
|
||||
<form class="mx-auto w-full px-2" @submit="onSubmit">
|
||||
<h1 class="text-heading-xl text-center inline-block mb-4">Reset your password</h1>
|
||||
<h1 class="text-heading-xl text-center w-full inline-block mb-4">
|
||||
Reset your password
|
||||
</h1>
|
||||
|
||||
<div class="flex flex-col space-y-2 text-body-sm">
|
||||
<div class="mb-4">One step closer to resetting your password</div>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<p class="text-center text-body-xs text-foreground mb-2">
|
||||
Choose a new password for your Speckle account
|
||||
</p>
|
||||
<FormTextInput
|
||||
v-model="password"
|
||||
type="password"
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="New password"
|
||||
color="foundation"
|
||||
size="lg"
|
||||
:rules="passwordRules"
|
||||
:rules="[isRequired]"
|
||||
show-label
|
||||
show-required
|
||||
/>
|
||||
<FormTextInput
|
||||
type="password"
|
||||
name="password-repeat"
|
||||
label="Password (confirmation)"
|
||||
color="foundation"
|
||||
size="lg"
|
||||
:rules="passwordRepeatRules"
|
||||
placeholder="Confirm new password"
|
||||
show-label
|
||||
show-required
|
||||
/>
|
||||
<AuthPasswordChecks :password="password" />
|
||||
</div>
|
||||
|
||||
<FormButton class="mt-4" submit full-width size="lg" :disabled="loading">
|
||||
<FormButton class="mt-8" submit full-width size="lg" :disabled="loading">
|
||||
Reset password
|
||||
</FormButton>
|
||||
</form>
|
||||
@@ -36,11 +30,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from 'vee-validate'
|
||||
import { usePasswordReset } from '~~/lib/auth/composables/passwordReset'
|
||||
import { isRequired, isSameAs } from '~~/lib/common/helpers/validation'
|
||||
import { isRequired } from '~~/lib/common/helpers/validation'
|
||||
|
||||
type FormValues = {
|
||||
password: string
|
||||
repeatPassword: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -50,9 +43,8 @@ const props = defineProps<{
|
||||
const { handleSubmit } = useForm<FormValues>()
|
||||
const { finalize } = usePasswordReset()
|
||||
|
||||
const passwordRules = [isRequired]
|
||||
const passwordRepeatRules = [...passwordRules, isSameAs('password')]
|
||||
const loading = ref(false)
|
||||
const password = ref('')
|
||||
|
||||
const onSubmit = handleSubmit(
|
||||
async ({ password }) => await finalize(password, props.token)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<form class="mx-auto w-full px-2" @submit="onSubmit">
|
||||
<h1 class="text-heading-xl text-center inline-block mb-4">Reset your password</h1>
|
||||
<div class="flex flex-col space-y-4 text-body-xs">
|
||||
<div>
|
||||
Type in the email address you used, so we can verify your account. We will send
|
||||
you instructions on how to reset your password.
|
||||
<h1 class="text-heading-xl text-center inline-block mb-4 w-full">
|
||||
Reset your password
|
||||
</h1>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="text-body-xs text-foreground text-center mb-2">
|
||||
Enter your email address and we'll send you the password reset instructions.
|
||||
</div>
|
||||
<div>
|
||||
<FormTextInput
|
||||
@@ -18,12 +19,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-y-2 mt-4">
|
||||
<div class="flex flex-col gap-y-2 mt-8">
|
||||
<FormButton submit full-width size="lg" :disabled="loading">
|
||||
Send password reset email
|
||||
Send reset email
|
||||
</FormButton>
|
||||
<FormButton color="outline" size="lg" full-width :to="homeRoute">
|
||||
Go home
|
||||
Back to login
|
||||
</FormButton>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<FormCheckbox
|
||||
v-model="newsletterConsent"
|
||||
name="newsletter"
|
||||
label="Opt in for exclusive Speckle news and tips"
|
||||
class="text-body-2xs"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const newsletterConsent = ref<true | undefined>(undefined)
|
||||
</script>
|
||||
@@ -5,22 +5,22 @@
|
||||
<h1 class="text-heading-xl text-center inline-block">
|
||||
Create your Speckle account
|
||||
</h1>
|
||||
<h2 class="text-body-sm text-center text-foreground-2">
|
||||
Connectivity, Collaboration and Automation for 3D
|
||||
</h2>
|
||||
</div>
|
||||
<AuthWorkspaceInviteHeader v-else :invite="workspaceInvite" />
|
||||
<template v-if="isInviteOnly && !inviteToken">
|
||||
<div class="flex space-x-2 items-center">
|
||||
<ExclamationTriangleIcon class="h-8 w-8 text-warning" />
|
||||
<div>
|
||||
This server is invite only. If you have received an invitation email, please
|
||||
follow the instructions in it.
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!inviteEmail" class="flex space-x-2 items-center justify-center">
|
||||
<CommonAlert color="warning">
|
||||
<template #title>This server is invite only</template>
|
||||
<template #description>
|
||||
If you have received an invitation email, please follow the instructions in
|
||||
it.
|
||||
</template>
|
||||
</CommonAlert>
|
||||
<div
|
||||
v-if="!inviteEmail"
|
||||
class="flex gap-1 text-foregound-3 text-body-xs items-center justify-center"
|
||||
>
|
||||
<span>Already have an account?</span>
|
||||
<CommonTextLink :to="loginRoute">Log in</CommonTextLink>
|
||||
<NuxtLink class="text-foreground" :to="loginRoute">Log in</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -31,6 +31,7 @@
|
||||
:app-id="appId"
|
||||
:newsletter-consent="newsletterConsent"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div
|
||||
v-if="hasThirdPartyStrategies && hasLocalStrategy"
|
||||
@@ -55,25 +56,8 @@ import { useQuery } from '@vue/apollo-composable'
|
||||
import { AuthStrategy } from '~~/lib/auth/helpers/strategies'
|
||||
import { useLoginOrRegisterUtils } from '~~/lib/auth/composables/auth'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import { ExclamationTriangleIcon } from '@heroicons/vue/24/outline'
|
||||
import { loginRoute } from '~~/lib/common/helpers/route'
|
||||
|
||||
const registerPanelQuery = graphql(`
|
||||
query AuthRegisterPanel($token: String) {
|
||||
serverInfo {
|
||||
inviteOnly
|
||||
authStrategies {
|
||||
id
|
||||
}
|
||||
...AuthStategiesServerInfoFragment
|
||||
...ServerTermsOfServicePrivacyPolicyFragment
|
||||
}
|
||||
serverInviteByToken(token: $token) {
|
||||
id
|
||||
email
|
||||
}
|
||||
}
|
||||
`)
|
||||
import { authRegisterPanelQuery } from '~/lib/auth/graphql/queries'
|
||||
|
||||
const registerPanelWorkspaceInviteQuery = graphql(`
|
||||
query AuthRegisterPanelWorkspaceInvite($token: String) {
|
||||
@@ -86,7 +70,7 @@ const registerPanelWorkspaceInviteQuery = graphql(`
|
||||
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const { appId, challenge, inviteToken } = useLoginOrRegisterUtils()
|
||||
const { result } = useQuery(registerPanelQuery, () => ({
|
||||
const { result } = useQuery(authRegisterPanelQuery, () => ({
|
||||
token: inviteToken.value
|
||||
}))
|
||||
const { result: workspaceInviteResult } = useQuery(
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div
|
||||
class="mt-2 text-body-2xs text-foreground-2 text-center terms-of-service"
|
||||
v-html="serverInfo.termsOfService"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ServerTermsOfServicePrivacyPolicyFragmentFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
|
||||
defineProps<{
|
||||
serverInfo: ServerTermsOfServicePrivacyPolicyFragmentFragment
|
||||
}>()
|
||||
|
||||
graphql(`
|
||||
fragment ServerTermsOfServicePrivacyPolicyFragment on ServerInfo {
|
||||
termsOfService
|
||||
}
|
||||
`)
|
||||
</script>
|
||||
@@ -40,17 +40,8 @@
|
||||
/>
|
||||
</div>
|
||||
<AuthPasswordChecks :password="password" class="mt-2 h-12 sm:h-8" />
|
||||
<div class="mt-8 text-body-2xs flex px-2 text-foreground-2 space-x-2">
|
||||
<!--
|
||||
Note the newsletter consent box is here because i got very confused re layout of the panel
|
||||
and didn't figure out a better way to put it where i needed it to be
|
||||
-->
|
||||
<FormCheckbox
|
||||
v-model="newsletterConsent"
|
||||
name="newsletter"
|
||||
label="Opt in for exclusive Speckle news and tips"
|
||||
class="text-body-xs"
|
||||
/>
|
||||
<div class="mt-8 flex px-2">
|
||||
<AuthRegisterNewsletter v-model:newsletter-consent="newsletterConsent" />
|
||||
</div>
|
||||
<FormButton
|
||||
submit
|
||||
@@ -61,16 +52,10 @@
|
||||
>
|
||||
Sign up
|
||||
</FormButton>
|
||||
<div
|
||||
v-if="serverInfo.termsOfService"
|
||||
class="mt-2 text-body-2xs text-foreground-2 text-center terms-of-service"
|
||||
v-html="serverInfo.termsOfService"
|
||||
/>
|
||||
<div v-if="!inviteEmail" class="mt-2 sm:mt-4 text-center text-body-sm">
|
||||
<span class="mr-2">Already have an account?</span>
|
||||
<CommonTextLink :to="finalLoginRoute" :icon-right="ArrowRightIcon">
|
||||
Log in
|
||||
</CommonTextLink>
|
||||
<AuthRegisterTerms v-if="serverInfo.termsOfService" :server-info="serverInfo" />
|
||||
<div v-if="!inviteEmail" class="mt-2 sm:mt-4 text-center text-body-xs">
|
||||
<span class="mr-2 text-foreground-3">Already have an account?</span>
|
||||
<NuxtLink class="text-foreground" :to="finalLoginRoute">Log in</NuxtLink>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
@@ -84,7 +69,6 @@ import { loginRoute } from '~~/lib/common/helpers/route'
|
||||
import { passwordRules } from '~~/lib/auth/helpers/validation'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import type { ServerTermsOfServicePrivacyPolicyFragmentFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
import { ArrowRightIcon } from '@heroicons/vue/20/solid'
|
||||
import { useMounted } from '@vueuse/core'
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<form method="post" @submit="onSubmit">
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-heading-xl text-center mb-8">Speckle SSO login</h1>
|
||||
|
||||
<FormTextInput
|
||||
v-model="email"
|
||||
type="email"
|
||||
name="email"
|
||||
label="Your work email"
|
||||
placeholder="Enter your email"
|
||||
size="lg"
|
||||
color="foundation"
|
||||
:rules="[isEmail, isRequired]"
|
||||
:loading="isChecking"
|
||||
:help="helpText"
|
||||
:custom-error-message="errorMessage"
|
||||
show-label
|
||||
:disabled="loading"
|
||||
auto-focus
|
||||
@update:model-value="onEmailChange"
|
||||
/>
|
||||
|
||||
<AuthSsoWorkspaceSelect
|
||||
v-if="shouldShowWorkspaceSelector"
|
||||
:items="availableWorkspaces"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 space-y-4">
|
||||
<FormButton size="lg" submit full-width :loading="loading" :disabled="!isValid">
|
||||
{{ buttonText }}
|
||||
</FormButton>
|
||||
<FormButton size="lg" color="subtle" full-width :to="loginRoute">
|
||||
Back to login
|
||||
</FormButton>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useForm } from 'vee-validate'
|
||||
import { isEmail, isRequired } from '~/lib/common/helpers/validation'
|
||||
import { loginRoute } from '~/lib/common/helpers/route'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { workspaceSsoByEmailQuery } from '~/lib/workspaces/graphql/queries'
|
||||
import { useAuthManager, useLoginOrRegisterUtils } from '~/lib/auth/composables/auth'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import type { AuthSsoLogin_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { graphql } from '~/lib/common/generated/gql/gql'
|
||||
|
||||
type FormValues = {
|
||||
email: string
|
||||
workspace?: AuthSsoLogin_WorkspaceFragment
|
||||
}
|
||||
|
||||
enum EmailCheckState {
|
||||
Idle = 'idle',
|
||||
Checking = 'checking',
|
||||
Checked = 'checked'
|
||||
}
|
||||
|
||||
graphql(`
|
||||
fragment AuthSsoLogin_Workspace on LimitedWorkspace {
|
||||
id
|
||||
slug
|
||||
name
|
||||
logo
|
||||
}
|
||||
`)
|
||||
|
||||
const { meta, handleSubmit, setFieldValue, values } = useForm<FormValues>({
|
||||
initialValues: {
|
||||
email: '',
|
||||
workspace: undefined
|
||||
}
|
||||
})
|
||||
|
||||
const { challenge } = useLoginOrRegisterUtils()
|
||||
const { signInOrSignUpWithSso } = useAuthManager()
|
||||
const logger = useLogger()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
|
||||
const loading = ref(false)
|
||||
const email = ref('')
|
||||
const emailCheckState = ref<EmailCheckState>(EmailCheckState.Idle)
|
||||
|
||||
const {
|
||||
loading: isChecking,
|
||||
result,
|
||||
onResult
|
||||
} = useQuery(
|
||||
workspaceSsoByEmailQuery,
|
||||
() => ({ email: email.value }),
|
||||
() => ({
|
||||
enabled: emailCheckState.value === 'checking'
|
||||
})
|
||||
)
|
||||
|
||||
const helpText = computed(() => {
|
||||
if (isChecking.value) return 'Checking SSO availability...'
|
||||
if (availableWorkspaces.value.length === 0 && emailCheckState.value === 'checked') {
|
||||
return 'No SSO-enabled workspaces found for this email'
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const errorMessage = computed(() => {
|
||||
if (emailCheckState.value === 'checked' && availableWorkspaces.value.length === 0) {
|
||||
return 'This email is not associated with any SSO-enabled workspaces'
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const availableWorkspaces = computed(() => result.value?.workspaceSsoByEmail || [])
|
||||
|
||||
// Show workspace selector only if multiple options exist
|
||||
const shouldShowWorkspaceSelector = computed(
|
||||
() =>
|
||||
availableWorkspaces.value.length > 1 &&
|
||||
emailCheckState.value === EmailCheckState.Checked
|
||||
)
|
||||
|
||||
// Valid when email passes validation and has associated workspaces
|
||||
const isValid = computed(
|
||||
() =>
|
||||
meta.value.valid &&
|
||||
emailCheckState.value === 'checked' &&
|
||||
availableWorkspaces.value.length > 0
|
||||
)
|
||||
|
||||
const buttonText = computed(() => {
|
||||
if (isChecking.value) return 'Checking...'
|
||||
if (!isValid.value) return 'Single Sign-On'
|
||||
return values.workspace?.name ? `Sign in to ${values.workspace.name}` : 'Sign in'
|
||||
})
|
||||
|
||||
const debouncedCheckEmail = useDebounceFn((value: string) => {
|
||||
if (!value || !meta.value.valid) {
|
||||
emailCheckState.value = EmailCheckState.Idle
|
||||
return
|
||||
}
|
||||
emailCheckState.value = EmailCheckState.Checking
|
||||
}, 300)
|
||||
|
||||
const onEmailChange = (value: string) => {
|
||||
email.value = value
|
||||
emailCheckState.value = EmailCheckState.Idle
|
||||
setFieldValue('workspace', undefined)
|
||||
debouncedCheckEmail(value)
|
||||
}
|
||||
|
||||
const onSubmit = handleSubmit((values) => {
|
||||
if (!values.workspace) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
signInOrSignUpWithSso({
|
||||
challenge: challenge.value,
|
||||
workspaceSlug: values.workspace.slug
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('SSO login failed:', error)
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'SSO login failed',
|
||||
description:
|
||||
error instanceof Error ? error.message : 'An unexpected error occurred'
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onResult((res) => {
|
||||
if (!res.data) return
|
||||
emailCheckState.value = EmailCheckState.Checked
|
||||
const workspaces = res.data.workspaceSsoByEmail || []
|
||||
if (workspaces.length === 1) {
|
||||
setFieldValue('workspace', workspaces[0])
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<WorkspaceAvatar
|
||||
v-if="workspace"
|
||||
:name="workspace.name"
|
||||
:logo="workspace.logo"
|
||||
size="xl"
|
||||
/>
|
||||
|
||||
<h1 class="text-heading-xl text-center">
|
||||
Sign up to access {{ workspace?.name || 'your Workspace' }}
|
||||
</h1>
|
||||
|
||||
<div class="w-full max-w-xs">
|
||||
<AuthRegisterNewsletter v-model:newsletter-consent="newsletterConsent" />
|
||||
|
||||
<div class="my-4">
|
||||
<FormButton
|
||||
size="lg"
|
||||
full-width
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
@click="handleContinue"
|
||||
>
|
||||
Continue with {{ workspace?.ssoProviderName || 'SSO' }}
|
||||
</FormButton>
|
||||
</div>
|
||||
<AuthRegisterTerms v-if="serverInfo" :server-info="serverInfo" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthManager, useLoginOrRegisterUtils } from '~/lib/auth/composables/auth'
|
||||
import { useWorkspacePublicSsoCheck } from '~/lib/workspaces/composables/sso'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { authRegisterPanelQuery } from '~/lib/auth/graphql/queries'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(false)
|
||||
const newsletterConsent = ref<true | undefined>(undefined)
|
||||
|
||||
const { challenge } = useLoginOrRegisterUtils()
|
||||
const { signInOrSignUpWithSso } = useAuthManager()
|
||||
const mixpanel = useMixpanel()
|
||||
const logger = useLogger()
|
||||
const { result } = useQuery(authRegisterPanelQuery)
|
||||
|
||||
const serverInfo = computed(() => result.value?.serverInfo)
|
||||
const workspaceSlug = computed(() => route.params.slug?.toString() || '')
|
||||
const { workspace } = useWorkspacePublicSsoCheck(workspaceSlug)
|
||||
|
||||
const handleContinue = () => {
|
||||
if (!workspaceSlug.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
mixpanel.track('Workspace SSO Register Attempted', {
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_slug: workspaceSlug.value
|
||||
})
|
||||
|
||||
signInOrSignUpWithSso({
|
||||
challenge: challenge.value,
|
||||
workspaceSlug: workspaceSlug.value,
|
||||
newsletterConsent: !!newsletterConsent.value
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('SSO registration failed:', error)
|
||||
mixpanel.track('Workspace SSO Registration Failed', {
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_slug: workspaceSlug.value
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
mixpanel.track('Workspace SSO Registration Successful', {
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_slug: workspaceSlug.value
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<FormSelectBase
|
||||
v-bind="props"
|
||||
v-model="selectedValue"
|
||||
label="Select workspace"
|
||||
name="workspace"
|
||||
:rules="workspaceRules"
|
||||
help="You may need to authenticate separately for each workspace you want to access."
|
||||
>
|
||||
<template #option="{ item }">
|
||||
<div class="flex items-center gap-2">
|
||||
<WorkspaceAvatar :name="item.name" :logo="item.logo" size="xs" />
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #nothing-selected>Select a workspace</template>
|
||||
<template #something-selected="{ value }">
|
||||
<div v-if="!isArrayValue(value)" class="flex items-center gap-2">
|
||||
<WorkspaceAvatar :logo="value.logo" :name="value.name" size="xs" />
|
||||
<span>{{ value.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</FormSelectBase>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFormSelectChildInternals } from '@speckle/ui-components'
|
||||
import type { AuthSsoLogin_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { isRequired } from '~/lib/common/helpers/validation'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: AuthSsoLogin_WorkspaceFragment
|
||||
items: AuthSsoLogin_WorkspaceFragment[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: 'update:modelValue',
|
||||
v: AuthSsoLogin_WorkspaceFragment | AuthSsoLogin_WorkspaceFragment[] | undefined
|
||||
): void
|
||||
}>()
|
||||
|
||||
const workspaceRules = computed(() => [isRequired])
|
||||
|
||||
const { selectedValue, isArrayValue } =
|
||||
useFormSelectChildInternals<AuthSsoLogin_WorkspaceFragment>({
|
||||
props: toRefs(props),
|
||||
emit
|
||||
})
|
||||
</script>
|
||||
@@ -1,15 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4">
|
||||
<Component
|
||||
:is="getButtonComponent(strat)"
|
||||
v-for="strat in thirdPartyStrategies"
|
||||
:key="strat.id"
|
||||
to="javascript:;"
|
||||
:server-info="serverInfo"
|
||||
@click="() => onClick(strat)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<Component
|
||||
:is="getButtonComponent(strat)"
|
||||
v-for="strat in thirdPartyStrategies"
|
||||
:key="strat.id"
|
||||
to="javascript:;"
|
||||
:server-info="serverInfo"
|
||||
@click="() => onClick(strat)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<template>
|
||||
<NuxtLink
|
||||
<FormButton
|
||||
color="outline"
|
||||
size="lg"
|
||||
full-width
|
||||
class="group"
|
||||
:to="to"
|
||||
:class="[
|
||||
'transition grow px-3 inline-flex justify-center items-center outline-none h6 font-medium leading-7',
|
||||
'rounded shadow bg-foundation- text-foreground dark:text-foreground-on-primary',
|
||||
'border border-outline-2 hover:border-outline-3',
|
||||
noVerticalPadding ? '' : 'py-2'
|
||||
]"
|
||||
:external="true"
|
||||
:link-component="'a'"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<slot />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</FormButton>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
to: string
|
||||
noVerticalPadding?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<AuthThirdPartyLoginButtonBase :to="to" class="dark:border dark:border-[#475569]">
|
||||
<AuthThirdPartyLoginButtonBase :to="to">
|
||||
<img
|
||||
src="~/assets/images/auth/github_icon.svg"
|
||||
alt="GitHub Sign In"
|
||||
class="w-4 dark:invert"
|
||||
class="w-4 dark:invert grayscale group-hover:grayscale-0"
|
||||
/>
|
||||
<div class="ml-3">Github</div>
|
||||
<div class="ml-2">Continue with Github</div>
|
||||
</AuthThirdPartyLoginButtonBase>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<img
|
||||
src="~/assets/images/auth/google_icon_w_bg.svg"
|
||||
alt="Google Sign In"
|
||||
class="w-11"
|
||||
class="w-9 grayscale grayscale group-hover:grayscale-0"
|
||||
/>
|
||||
<div>Google</div>
|
||||
<div>Continue with Google</div>
|
||||
</AuthThirdPartyLoginButtonBase>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<AuthThirdPartyLoginButtonBase :to="to" class="dark:bg-[#2F2F2F]">
|
||||
<img src="~/assets/images/auth/ms_icon.svg" alt="Microsoft Sign In" class="w-4" />
|
||||
<div>Microsoft</div>
|
||||
<AuthThirdPartyLoginButtonBase :to="to">
|
||||
<img
|
||||
src="~/assets/images/auth/ms_icon.svg"
|
||||
alt="Microsoft Sign In"
|
||||
class="w-4 grayscale mr-2 group-hover:grayscale-0"
|
||||
/>
|
||||
<div>Continue with Microsoft</div>
|
||||
</AuthThirdPartyLoginButtonBase>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -9,45 +9,41 @@
|
||||
prevent-close-on-click-outside
|
||||
@fully-closed="reset"
|
||||
>
|
||||
<template v-if="isTestAutomation" #header>
|
||||
Create
|
||||
<span class="font-extrabold text-fancy-gradient">test</span>
|
||||
automation
|
||||
</template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<template v-if="isTestAutomation" #header>Create test automation</template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<CommonStepsNumber
|
||||
v-if="shouldShowStepsWidget"
|
||||
v-model="stepsWidgetModel"
|
||||
class="mb-2"
|
||||
class="my-2"
|
||||
:steps="stepsWidgetSteps"
|
||||
:go-vertical-below="TailwindBreakpoints.sm"
|
||||
non-interactive
|
||||
/>
|
||||
<CommonAlert v-if="isTestAutomation" color="info">
|
||||
<template #title>What is a "test automation"?</template>
|
||||
<template #description>
|
||||
<ul class="list-disc ml-4">
|
||||
<li>
|
||||
A test automation is a sandbox environment that allows you to connect your
|
||||
local development environment for testing purposes. It enables you to run
|
||||
your code against project data and submit results directly to the
|
||||
connected test automation.
|
||||
</li>
|
||||
<li>
|
||||
Unlike regular automations, test automations are not triggered by changes
|
||||
to project data. They cannot be started by pushing a new version to a
|
||||
model.
|
||||
</li>
|
||||
<li>Consequently, test automations do not execute published functions.</li>
|
||||
</ul>
|
||||
</template>
|
||||
</CommonAlert>
|
||||
<CommonCard v-if="isTestAutomation" class="bg-foundation py-3 px-4">
|
||||
<p class="text-body-xs font-medium text-foreground">
|
||||
What is a "test automation"?
|
||||
</p>
|
||||
<ul class="list-disc ml-4 text-body-xs text-foreground-2 mt-2">
|
||||
<li>
|
||||
A test automation is a sandbox environment that allows you to connect your
|
||||
local development environment for testing purposes. It enables you to run
|
||||
your code against project data and submit results directly to the connected
|
||||
test automation.
|
||||
</li>
|
||||
<li>
|
||||
Unlike regular automations, test automations are not triggered by changes to
|
||||
project data. They cannot be started by pushing a new version to a model.
|
||||
</li>
|
||||
<li>Consequently, test automations do not execute published functions.</li>
|
||||
</ul>
|
||||
</CommonCard>
|
||||
<AutomateAutomationCreateDialogSelectFunctionStep
|
||||
v-if="enumStep === AutomationCreateSteps.SelectFunction"
|
||||
v-model:selected-function="selectedFunction"
|
||||
:show-label="false"
|
||||
:show-required="false"
|
||||
:preselected-function="validatedPreselectedFunction"
|
||||
:workspace-id="workspaceId"
|
||||
/>
|
||||
<AutomateAutomationCreateDialogFunctionParametersStep
|
||||
v-else-if="
|
||||
@@ -65,12 +61,14 @@
|
||||
v-model:automation-name="automationName"
|
||||
:preselected-project="preselectedProject"
|
||||
:is-test-automation="isTestAutomation"
|
||||
:workspace-id="workspaceId"
|
||||
/>
|
||||
<AutomateAutomationCreateDialogSelectFunctionStep
|
||||
v-if="isTestAutomation"
|
||||
v-model:selected-function="selectedFunction"
|
||||
:preselected-function="validatedPreselectedFunction"
|
||||
:page-size="2"
|
||||
:workspace-id="workspaceId"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
@@ -83,11 +81,6 @@ import {
|
||||
TailwindBreakpoints,
|
||||
type LayoutDialogButton
|
||||
} from '@speckle/ui-components'
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
CodeBracketIcon
|
||||
} from '@heroicons/vue/24/outline'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import { Automate, type Optional } from '@speckle/shared'
|
||||
import type { CreateAutomationSelectableFunction } from '~/lib/automate/helpers/automations'
|
||||
@@ -134,6 +127,7 @@ graphql(`
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
workspaceId?: string
|
||||
preselectedFunction?: Optional<CreateAutomationSelectableFunction>
|
||||
preselectedProject?: Optional<FormSelectProjects_ProjectFragment>
|
||||
}>()
|
||||
@@ -215,8 +209,7 @@ const buttons = computed((): LayoutDialogButton[] => {
|
||||
id: 'createTestAutomation',
|
||||
text: 'Create test automation',
|
||||
props: {
|
||||
color: 'outline',
|
||||
iconLeft: CodeBracketIcon
|
||||
color: 'outline'
|
||||
},
|
||||
onClick: () => {
|
||||
isTestAutomation.value = true
|
||||
@@ -227,10 +220,10 @@ const buttons = computed((): LayoutDialogButton[] => {
|
||||
id: 'selectFnNext',
|
||||
text: 'Next',
|
||||
props: {
|
||||
iconRight: ChevronRightIcon,
|
||||
disabled: !selectedFunction.value
|
||||
},
|
||||
onClick: () => {
|
||||
mixpanel.track('Automate Select Function')
|
||||
step.value++
|
||||
}
|
||||
}
|
||||
@@ -241,17 +234,17 @@ const buttons = computed((): LayoutDialogButton[] => {
|
||||
id: 'fnParamsPrev',
|
||||
text: 'Previous',
|
||||
props: {
|
||||
color: 'outline',
|
||||
iconLeft: ChevronLeftIcon,
|
||||
class: '!text-primary'
|
||||
color: 'outline'
|
||||
},
|
||||
onClick: () => step.value--
|
||||
},
|
||||
{
|
||||
id: 'fnParamsNext',
|
||||
text: 'Next',
|
||||
onClick: () => {
|
||||
mixpanel.track('Automate Set Function Parameters ')
|
||||
},
|
||||
props: {
|
||||
iconRight: ChevronRightIcon,
|
||||
disabled: hasParameterErrors.value
|
||||
},
|
||||
submit: true
|
||||
@@ -263,14 +256,16 @@ const buttons = computed((): LayoutDialogButton[] => {
|
||||
id: 'detailsPrev',
|
||||
text: 'Previous',
|
||||
props: {
|
||||
color: 'outline',
|
||||
iconLeft: ChevronLeftIcon
|
||||
color: 'outline'
|
||||
},
|
||||
onClick: () => step.value--
|
||||
},
|
||||
{
|
||||
id: 'detailsCreate',
|
||||
text: 'Create',
|
||||
onClick: () => {
|
||||
mixpanel.track('Automate Set Automation Details')
|
||||
},
|
||||
submit: true,
|
||||
disabled: creationLoading.value
|
||||
}
|
||||
@@ -281,8 +276,7 @@ const buttons = computed((): LayoutDialogButton[] => {
|
||||
id: 'detailsPrev',
|
||||
text: 'Back',
|
||||
props: {
|
||||
color: 'outline',
|
||||
iconLeft: ChevronLeftIcon
|
||||
color: 'outline'
|
||||
},
|
||||
onClick: reset
|
||||
},
|
||||
@@ -445,7 +439,7 @@ const onDetailsSubmit = handleDetailsSubmit(async () => {
|
||||
return
|
||||
}
|
||||
|
||||
mixpanel.track('Automation created', {
|
||||
mixpanel.track('Automate Automation Created', {
|
||||
automationId: aId,
|
||||
name,
|
||||
projectId: project.id,
|
||||
|
||||
+2
@@ -12,6 +12,7 @@
|
||||
:allow-unset="false"
|
||||
validate-on-value-update
|
||||
owned-only
|
||||
:workspace-id="workspaceId"
|
||||
/>
|
||||
<FormSelectModels
|
||||
v-if="project?.id"
|
||||
@@ -51,6 +52,7 @@ import type {
|
||||
const props = defineProps<{
|
||||
preselectedProject?: Optional<FormSelectProjects_ProjectFragment>
|
||||
isTestAutomation: boolean
|
||||
workspaceId?: string
|
||||
}>()
|
||||
const project = defineModel<Optional<FormSelectProjects_ProjectFragment>>('project', {
|
||||
required: true
|
||||
|
||||
+21
-11
@@ -6,7 +6,7 @@
|
||||
:show-required="showRequired"
|
||||
name="search"
|
||||
color="foundation"
|
||||
placeholder="Search Functions..."
|
||||
placeholder="Search functions..."
|
||||
show-clear
|
||||
full-width
|
||||
v-bind="bind"
|
||||
@@ -43,13 +43,19 @@ import type { Optional } from '@speckle/shared'
|
||||
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
|
||||
|
||||
const searchQuery = graphql(`
|
||||
query AutomationCreateDialogFunctionsSearch($search: String, $cursor: String = null) {
|
||||
automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) {
|
||||
cursor
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
...AutomateAutomationCreateDialog_AutomateFunction
|
||||
query AutomationCreateDialogFunctionsSearch(
|
||||
$workspaceId: String!
|
||||
$search: String
|
||||
$cursor: String = null
|
||||
) {
|
||||
workspace(id: $workspaceId) {
|
||||
automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) {
|
||||
cursor
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
...AutomateAutomationCreateDialog_AutomateFunction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +63,7 @@ const searchQuery = graphql(`
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
workspaceId?: string
|
||||
preselectedFunction: Optional<CreateAutomationSelectableFunction>
|
||||
pageSize?: Optional<number>
|
||||
showLabel?: Optional<boolean>
|
||||
@@ -83,10 +90,11 @@ const {
|
||||
} = usePaginatedQuery({
|
||||
query: searchQuery,
|
||||
baseVariables: computed(() => ({
|
||||
search: search.value?.length ? search.value : null
|
||||
workspaceId: props.workspaceId ?? '',
|
||||
search: search.value?.length ? search.value : ''
|
||||
})),
|
||||
resolveKey: (vars) => [vars.search || ''],
|
||||
resolveCurrentResult: (res) => res?.automateFunctions,
|
||||
resolveCurrentResult: (res) => res?.workspace?.automateFunctions,
|
||||
resolveNextPageVariables: (baseVars, cursor) => ({
|
||||
...baseVars,
|
||||
cursor
|
||||
@@ -94,7 +102,9 @@ const {
|
||||
resolveCursorFromVariables: (vars) => vars.cursor
|
||||
})
|
||||
|
||||
const queryItems = computed(() => result.value?.automateFunctions.items)
|
||||
const queryItems = computed(() => {
|
||||
return result.value?.workspace?.automateFunctions.items
|
||||
})
|
||||
const items = computed(() => {
|
||||
const baseItems = (queryItems.value || []).slice(0, props.pageSize)
|
||||
const preselectedFn = props.preselectedFunction
|
||||
|
||||
@@ -2,80 +2,71 @@
|
||||
<Component
|
||||
:is="noButtons ? NuxtLink : 'div'"
|
||||
:class="classes"
|
||||
:to="noButtons ? automationFunctionRoute(fn.id) : undefined"
|
||||
:to="noButtons ? automateFunctionRoute(fn.id) : undefined"
|
||||
:external="externalMoreInfo"
|
||||
:target="externalMoreInfo ? '_blank' : undefined"
|
||||
class="rounded-lg border border-outline-3 bg-foundation overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="px-4 py-4 flex flex-col gap-3 rounded-lg border border-outline-3 bg-foundation relative h-full"
|
||||
>
|
||||
<div class="px-4 py-4 flex flex-col gap-3 relative h-full">
|
||||
<div class="flex gap-3 items-center" :class="{ 'w-4/5': hasLabel }">
|
||||
<AutomateFunctionLogo :logo="fn.logo" />
|
||||
<div class="flex flex-col truncate">
|
||||
<div
|
||||
:class="[
|
||||
'text-heading text-foreground truncate',
|
||||
'text-heading-sm text-foreground truncate',
|
||||
noButtons ? '' : 'hover:underline'
|
||||
]"
|
||||
>
|
||||
<Component
|
||||
:is="noButtons ? 'div' : NuxtLink"
|
||||
:to="automationFunctionRoute(fn.id)"
|
||||
:to="automateFunctionRoute(fn.id)"
|
||||
:target="externalMoreInfo ? '_blank' : undefined"
|
||||
class="truncate"
|
||||
>
|
||||
{{ fn.name }}
|
||||
</Component>
|
||||
</div>
|
||||
<div class="text-body-xs flex items-center space-x-1 -mt-1">
|
||||
<div class="text-body-2xs flex items-center text-foreground-2 space-x-0.5">
|
||||
<span>by</span>
|
||||
<Component
|
||||
:is="noButtons ? 'div' : CommonTextLink"
|
||||
external
|
||||
:to="fn.repo.url"
|
||||
>
|
||||
<Component :is="noButtons ? 'div' : NuxtLink" :to="fn.repo.url" external>
|
||||
{{ fn.repo.owner }}
|
||||
</Component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-body-xs text-foreground-2 line-clamp-3 h-18 whitespace-normal">
|
||||
<div class="text-body-xs text-foreground-2 line-clamp-3 h-18">
|
||||
{{ plaintextDescription }}
|
||||
</div>
|
||||
<div v-if="!noButtons" class="flex flex-col sm:flex-row sm:self-end gap-2">
|
||||
<div v-if="!noButtons" class="flex flex-col sm:flex-row gap-x-1">
|
||||
<template v-if="showEdit">
|
||||
<FormButton
|
||||
:icon-left="PencilIcon"
|
||||
full-width
|
||||
color="outline"
|
||||
@click="$emit('edit')"
|
||||
>
|
||||
Edit Details
|
||||
<FormButton full-width color="outline" @click="$emit('edit')">
|
||||
Edit details
|
||||
</FormButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<FormButton
|
||||
text
|
||||
:to="automationFunctionRoute(fn.id)"
|
||||
:external="externalMoreInfo"
|
||||
:target="externalMoreInfo ? '_blank' : undefined"
|
||||
>
|
||||
Learn More
|
||||
</FormButton>
|
||||
<FormButton
|
||||
:icon-left="selected ? CheckIcon : undefined"
|
||||
@click="$emit('use')"
|
||||
>
|
||||
{{ selected ? 'Selected' : 'Select' }}
|
||||
</FormButton>
|
||||
<FormButton
|
||||
color="subtle"
|
||||
:to="automateFunctionRoute(fn.id)"
|
||||
:external="externalMoreInfo"
|
||||
:target="externalMoreInfo ? '_blank' : undefined"
|
||||
>
|
||||
Learn more
|
||||
</FormButton>
|
||||
</template>
|
||||
</div>
|
||||
<div class="absolute top-0 right-0">
|
||||
<div
|
||||
v-if="hasLabel"
|
||||
class="rounded-bl-lg rounded-tr-[7px] text-body-2xs px-2 py-1"
|
||||
class="rounded-bl-md rounded-tr-lg font-medium text-body-3xs px-2 py-1"
|
||||
:class="{
|
||||
'bg-foundation-focus text-foreground': fn.isFeatured,
|
||||
'bg-warning text-foreground-on-primary': isOutdated
|
||||
'bg-info-lighter text-outline-4': fn.isFeatured,
|
||||
'bg-danger-lighter text-danger-darker': isOutdated
|
||||
}"
|
||||
>
|
||||
<template v-if="isOutdated">Outdated</template>
|
||||
@@ -88,10 +79,9 @@
|
||||
<script setup lang="ts">
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { AutomationsFunctionsCard_AutomateFunctionFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { CheckIcon, PencilIcon } from '@heroicons/vue/24/outline'
|
||||
import { automationFunctionRoute } from '~/lib/common/helpers/route'
|
||||
import { CheckIcon } from '@heroicons/vue/24/outline'
|
||||
import { automateFunctionRoute } from '~/lib/common/helpers/route'
|
||||
import { useMarkdown } from '~/lib/common/composables/markdown'
|
||||
import { CommonTextLink } from '@speckle/ui-components'
|
||||
|
||||
graphql(`
|
||||
fragment AutomationsFunctionsCard_AutomateFunction on AutomateFunction {
|
||||
@@ -134,9 +124,9 @@ const classes = computed(() => {
|
||||
const classParts = ['rounded-lg']
|
||||
|
||||
if (props.selected) {
|
||||
classParts.push('ring-2 ring-primary')
|
||||
classParts.push('border-primary')
|
||||
} else if (props.noButtons) {
|
||||
classParts.push('ring-outline-2 hover:ring-2 cursor-pointer')
|
||||
classParts.push('hover:border-outline-5 cursor-pointer')
|
||||
}
|
||||
|
||||
return classParts.join(' ')
|
||||
|
||||
@@ -52,14 +52,21 @@ import type {
|
||||
} from '~/lib/automate/helpers/functions'
|
||||
import {
|
||||
automateGithubAppAuthorizationRoute,
|
||||
automationFunctionRoute
|
||||
automateFunctionRoute
|
||||
} from '~/lib/common/helpers/route'
|
||||
import { useEnumSteps, useEnumStepsWidgetSetup } from '~/lib/form/composables/steps'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { useCreateAutomateFunction } from '~/lib/automate/composables/management'
|
||||
import {
|
||||
useCreateAutomateFunction,
|
||||
useUpdateAutomateFunction
|
||||
} from '~/lib/automate/composables/management'
|
||||
import { useMutationLoading } from '@vue/apollo-composable'
|
||||
import type { AutomateFunctionCreateDialogDoneStep_AutomateFunctionFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
import type {
|
||||
AutomateFunctionCreateDialogDoneStep_AutomateFunctionFragment,
|
||||
AutomateFunctionCreateDialog_WorkspaceFragment
|
||||
} from '~~/lib/common/generated/gql/graphql'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
|
||||
enum FunctionCreateSteps {
|
||||
Authorize,
|
||||
@@ -70,10 +77,19 @@ enum FunctionCreateSteps {
|
||||
|
||||
type DetailsFormValues = FunctionDetailsFormValues
|
||||
|
||||
graphql(`
|
||||
fragment AutomateFunctionCreateDialog_Workspace on Workspace {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
isAuthorized: boolean
|
||||
templates: CreatableFunctionTemplate[]
|
||||
githubOrgs: string[]
|
||||
workspace?: AutomateFunctionCreateDialog_WorkspaceFragment
|
||||
}>()
|
||||
const open = defineModel<boolean>('open', { required: true })
|
||||
|
||||
@@ -81,6 +97,7 @@ const mixpanel = useMixpanel()
|
||||
const logger = useLogger()
|
||||
const mutationLoading = useMutationLoading()
|
||||
const createFunction = useCreateAutomateFunction()
|
||||
const updateFunction = useUpdateAutomateFunction()
|
||||
const { handleSubmit: handleDetailsSubmit } = useForm<DetailsFormValues>()
|
||||
const onDetailsSubmit = handleDetailsSubmit(async (values) => {
|
||||
if (!selectedTemplate.value) {
|
||||
@@ -99,20 +116,37 @@ const onDetailsSubmit = handleDetailsSubmit(async (values) => {
|
||||
}
|
||||
})
|
||||
|
||||
if (res?.id) {
|
||||
mixpanel.track('Automate Function Created', {
|
||||
functionId: res.id,
|
||||
templateId: selectedTemplate.value.id,
|
||||
name: values.name
|
||||
})
|
||||
createdFunction.value = res
|
||||
step.value++
|
||||
if (!res?.id) {
|
||||
// TODO: Error toast with butter
|
||||
return
|
||||
}
|
||||
|
||||
mixpanel.track('Automate Function Created', {
|
||||
functionId: res.id,
|
||||
templateId: selectedTemplate.value.id,
|
||||
name: values.name,
|
||||
/* eslint-disable-next-line camelcase */
|
||||
workspace_id: props.workspace?.id
|
||||
})
|
||||
createdFunction.value = res
|
||||
step.value++
|
||||
|
||||
if (!props.workspace?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
await updateFunction({
|
||||
input: {
|
||||
id: res.id,
|
||||
workspaceIds: [props.workspace.id]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const onSubmit = computed(() => {
|
||||
switch (enumStep.value) {
|
||||
case FunctionCreateSteps.Details:
|
||||
mixpanel.track('Automate Configure Function Details')
|
||||
return onDetailsSubmit
|
||||
default:
|
||||
return noop
|
||||
@@ -162,7 +196,10 @@ const title = computed(() => {
|
||||
})
|
||||
|
||||
const authorizeGithubUrl = computed(() => {
|
||||
const redirectUrl = new URL(automateGithubAppAuthorizationRoute, apiBaseUrl)
|
||||
const redirectUrl = new URL(
|
||||
automateGithubAppAuthorizationRoute(props.workspace?.slug),
|
||||
apiBaseUrl
|
||||
)
|
||||
return redirectUrl.toString()
|
||||
})
|
||||
|
||||
@@ -182,6 +219,9 @@ const buttons = computed((): LayoutDialogButton[] => {
|
||||
{
|
||||
id: 'authorizeAuthorize',
|
||||
text: 'Authorize',
|
||||
onClick: () => {
|
||||
mixpanel.track('Automate Start Authorize GitHub App')
|
||||
},
|
||||
props: {
|
||||
fullWidth: true,
|
||||
to: authorizeGithubUrl.value,
|
||||
@@ -198,7 +238,10 @@ const buttons = computed((): LayoutDialogButton[] => {
|
||||
iconRight: ChevronRightIcon,
|
||||
disabled: !selectedTemplate.value
|
||||
},
|
||||
onClick: () => step.value++
|
||||
onClick: () => {
|
||||
mixpanel.track('Automate Select Function Template')
|
||||
step.value++
|
||||
}
|
||||
}
|
||||
]
|
||||
case FunctionCreateSteps.Details:
|
||||
@@ -238,7 +281,7 @@ const buttons = computed((): LayoutDialogButton[] => {
|
||||
iconRight: ArrowRightIcon,
|
||||
fullWidth: true,
|
||||
to: createdFunction.value?.id
|
||||
? automationFunctionRoute(createdFunction.value.id)
|
||||
? automateFunctionRoute(createdFunction.value.id)
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,20 @@ import { difference, differenceBy } from 'lodash-es'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { useUpdateAutomateFunction } from '~/lib/automate/composables/management'
|
||||
import type { FunctionDetailsFormValues } from '~/lib/automate/helpers/functions'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { AutomateFunctionEditDialog_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
graphql(`
|
||||
fragment AutomateFunctionEditDialog_Workspace on Workspace {
|
||||
id
|
||||
name
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
model: FunctionDetailsFormValues
|
||||
fnId: string
|
||||
workspaces?: AutomateFunctionEditDialog_WorkspaceFragment[]
|
||||
}>()
|
||||
const open = defineModel<boolean>('open', { required: true })
|
||||
const { handleSubmit, setValues } = useForm<FunctionDetailsFormValues>()
|
||||
@@ -53,6 +63,7 @@ const onSubmit = handleSubmit(async (values) => {
|
||||
values.description !== props.model.description ? values.description : null,
|
||||
logo: values.image !== props.model.image ? values.image : null,
|
||||
tags: difference(values.tags, props.model.tags || []).length ? values.tags : null,
|
||||
workspaceIds: values.workspace ? [values.workspace.id] : [],
|
||||
supportedSourceApps: differenceBy(
|
||||
values.allowedSourceApps,
|
||||
props.model.allowedSourceApps || [],
|
||||
|
||||
@@ -33,6 +33,30 @@
|
||||
:rules="descriptionRules"
|
||||
validate-on-value-update
|
||||
/>
|
||||
<FormSelectBase
|
||||
v-if="workspaces?.length"
|
||||
name="workspace"
|
||||
label="Workspace"
|
||||
placeholder="Select a workspace"
|
||||
show-label
|
||||
allow-unset
|
||||
clearable
|
||||
help="Allow automations in one of your workspaces to use this function."
|
||||
:items="workspaces"
|
||||
>
|
||||
<template #something-selected="{ value }">
|
||||
<div class="label label--light">
|
||||
{{ isArray(value) ? value[0].name : value.name }}
|
||||
</div>
|
||||
</template>
|
||||
<template #option="{ item, selected }">
|
||||
<div class="flex flex-col">
|
||||
<div :class="['label label--light', selected ? 'text-primary' : '']">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FormSelectBase>
|
||||
<FormSelectSourceApps
|
||||
name="allowedSourceApps"
|
||||
label="Supported source apps"
|
||||
@@ -85,9 +109,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ValidationHelpers } from '@speckle/ui-components'
|
||||
import { isArray } from 'lodash-es'
|
||||
import type { AutomateFunctionCreateDialog_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
defineProps<{
|
||||
githubOrgs?: string[]
|
||||
workspaces?: AutomateFunctionCreateDialog_WorkspaceFragment[]
|
||||
}>()
|
||||
|
||||
const avatarEditMode = ref(false)
|
||||
|
||||
@@ -1,54 +1,51 @@
|
||||
<template>
|
||||
<div class="pt-4 flex gap-4 flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<Portal to="navigation">
|
||||
<HeaderNavLink
|
||||
:to="automationFunctionsRoute"
|
||||
:name="'Automate functions'"
|
||||
></HeaderNavLink>
|
||||
<HeaderNavLink
|
||||
:to="automationFunctionRoute(fn.id)"
|
||||
:name="fn.name"
|
||||
></HeaderNavLink>
|
||||
<template v-if="fnWorkspace">
|
||||
<HeaderNavLink
|
||||
:to="workspaceRoute(fnWorkspace.slug)"
|
||||
:separator="false"
|
||||
:name="fnWorkspace.name"
|
||||
/>
|
||||
<HeaderNavLink
|
||||
:to="workspaceFunctionsRoute(fnWorkspace.slug)"
|
||||
name="Functions"
|
||||
/>
|
||||
<HeaderNavLink :to="automateFunctionRoute(fn.id)" :name="fn.name" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<HeaderNavLink
|
||||
:to="publicAutomateFunctionsRoute"
|
||||
:separator="false"
|
||||
name="Functions"
|
||||
/>
|
||||
<HeaderNavLink :to="automateFunctionRoute(fn.id)" :name="fn.name" />
|
||||
</template>
|
||||
</Portal>
|
||||
<div class="flex items-center gap-4">
|
||||
<AutomateFunctionLogo :logo="fn.logo" />
|
||||
<h1 class="text-heading-lg">{{ fn.name }}</h1>
|
||||
<FormButton v-if="isOwner" size="sm" text class="mt-1" @click="$emit('edit')">
|
||||
Edit
|
||||
</FormButton>
|
||||
</div>
|
||||
<div
|
||||
v-tippy="
|
||||
hasReleases ? undefined : 'Your function needs to have at least one release'
|
||||
"
|
||||
class="flex gap-2 shrink-0"
|
||||
>
|
||||
<FormButton
|
||||
:icon-left="BoltIcon"
|
||||
class="shrink-0"
|
||||
full-width
|
||||
:disabled="!hasReleases"
|
||||
@click="$emit('createAutomation')"
|
||||
>
|
||||
Use in an automation
|
||||
<div class="flex items-center align-center gap-2">
|
||||
<FormButton v-if="isOwner" color="outline" @click="$emit('edit')">
|
||||
Edit
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { BoltIcon } from '@heroicons/vue/24/outline'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { AutomateFunctionPageHeader_FunctionFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import type {
|
||||
AutomateFunctionPageHeader_FunctionFragment,
|
||||
AutomateFunctionPageHeader_WorkspaceFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import {
|
||||
automationFunctionRoute,
|
||||
automationFunctionsRoute
|
||||
automateFunctionRoute,
|
||||
publicAutomateFunctionsRoute,
|
||||
workspaceFunctionsRoute,
|
||||
workspaceRoute
|
||||
} from '~/lib/common/helpers/route'
|
||||
|
||||
defineEmits<{
|
||||
createAutomation: []
|
||||
edit: []
|
||||
}>()
|
||||
|
||||
graphql(`
|
||||
fragment AutomateFunctionPageHeader_Function on AutomateFunction {
|
||||
id
|
||||
@@ -63,13 +60,24 @@ graphql(`
|
||||
releases(limit: 1) {
|
||||
totalCount
|
||||
}
|
||||
workspaceIds
|
||||
}
|
||||
|
||||
fragment AutomateFunctionPageHeader_Workspace on Workspace {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
fn: AutomateFunctionPageHeader_FunctionFragment
|
||||
fnWorkspace?: AutomateFunctionPageHeader_WorkspaceFragment
|
||||
isOwner: boolean
|
||||
}>()
|
||||
|
||||
const hasReleases = computed(() => props.fn.releases.totalCount > 0)
|
||||
defineEmits<{
|
||||
createAutomation: []
|
||||
edit: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2">
|
||||
<AutomateFunctionPageInfoBlock :icon="CodeBracketIcon" title="Source">
|
||||
<AutomateFunctionPageInfoBlock title="Source">
|
||||
<div class="space-y-1">
|
||||
<CommonTextLink
|
||||
v-tippy="license"
|
||||
external
|
||||
:to="repoUrl"
|
||||
target="_blank"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
class="max-w-full"
|
||||
>
|
||||
<span class="truncate">{{ repo }}</span>
|
||||
@@ -19,7 +18,6 @@
|
||||
external
|
||||
:to="githubDetails.owner.html_url"
|
||||
target="_blank"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
class="max-w-full"
|
||||
>
|
||||
<span class="truncate">
|
||||
@@ -34,36 +32,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</AutomateFunctionPageInfoBlock>
|
||||
<AutomateFunctionPageInfoBlock :icon="InformationCircleIcon" title="Info">
|
||||
<div class="gap-y-2 text-body-sm">
|
||||
<AutomateFunctionPageInfoBlock title="Info">
|
||||
<div class="gap-y-2 text-body-xs">
|
||||
<div v-if="latestRelease">
|
||||
<span>Last published: </span>
|
||||
<CommonText class="font-medium" :text="publishedAt" />
|
||||
<span class="font-medium">Last published: </span>
|
||||
<CommonText :text="publishedAt" />
|
||||
</div>
|
||||
<div>
|
||||
<span>Used by: </span>
|
||||
<CommonText
|
||||
class="font-medium"
|
||||
:text="`${fn.automationCount} automations`"
|
||||
/>
|
||||
</div>
|
||||
<CommonTextLink
|
||||
v-if="latestRelease?.inputSchema"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
@click="onViewParameters"
|
||||
>
|
||||
<CommonTextLink v-if="latestRelease?.inputSchema" @click="onViewParameters">
|
||||
View parameters
|
||||
</CommonTextLink>
|
||||
</div>
|
||||
</AutomateFunctionPageInfoBlock>
|
||||
</div>
|
||||
<AutomateFunctionPageInfoBlock
|
||||
title="Description"
|
||||
:icon="ChatBubbleBottomCenterTextIcon"
|
||||
>
|
||||
<AutomateFunctionPageInfoBlock title="Description">
|
||||
<CommonProseMarkdownDescription :markdown="description" />
|
||||
</AutomateFunctionPageInfoBlock>
|
||||
<AutomateFunctionPageInfoBlock title="Readme" :icon="BookOpenIcon">
|
||||
<AutomateFunctionPageInfoBlock title="Readme">
|
||||
<CommonProseGithubReadme
|
||||
:readme-markdown="rawReadme || ''"
|
||||
:repo="repo || ''"
|
||||
@@ -86,12 +70,11 @@
|
||||
class="shrink-0"
|
||||
>
|
||||
<FormButton
|
||||
:icon-left="BoltIcon"
|
||||
class="shrink-0"
|
||||
:disabled="!hasReleases"
|
||||
@click="$emit('createAutomation')"
|
||||
>
|
||||
Use in an automation
|
||||
Use in automation
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,14 +86,6 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CodeBracketIcon,
|
||||
InformationCircleIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
BookOpenIcon,
|
||||
BoltIcon,
|
||||
ChatBubbleBottomCenterTextIcon
|
||||
} from '@heroicons/vue/24/outline'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
useGetGithubRepo,
|
||||
@@ -131,7 +106,6 @@ graphql(`
|
||||
owner
|
||||
name
|
||||
}
|
||||
automationCount
|
||||
description
|
||||
releases(limit: 1) {
|
||||
items {
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
<div
|
||||
class="bg-foundation basis-1/2 shrink-0 grow-0 border border-outline-3 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center gap-2 px-4 py-2 border-b border-outline-3 mb-4">
|
||||
<Component :is="icon" class="w-5 h-5" />
|
||||
<h3 class="text-heading">{{ title }}</h3>
|
||||
<div class="px-4 py-2 border-b border-outline-3 mb-4">
|
||||
<h3 class="text-heading-sm">{{ title }}</h3>
|
||||
</div>
|
||||
<div class="px-4 pb-4">
|
||||
<slot />
|
||||
@@ -12,10 +11,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { PropAnyComponent } from '@speckle/ui-components'
|
||||
|
||||
defineProps<{
|
||||
icon: PropAnyComponent
|
||||
title: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<LayoutDialog v-model:open="open" max-width="md" title="Function Parameters">
|
||||
<LayoutDialog
|
||||
v-model:open="open"
|
||||
:buttons="dialogButtons"
|
||||
max-width="md"
|
||||
title="Function parameters"
|
||||
>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<template v-if="finalParams">
|
||||
<FormJsonForm :schema="finalParams" />
|
||||
<LayoutDialogSection
|
||||
title="Parameter schema"
|
||||
:icon="BeakerIcon"
|
||||
border-t
|
||||
border-b
|
||||
>
|
||||
<LayoutDialogSection title="Parameter schema" border-t border-b>
|
||||
<FormTextArea
|
||||
name="actionYaml"
|
||||
readonly
|
||||
:model-value="JSON.stringify(finalParams, null, 2)"
|
||||
class="text-sm text-primary"
|
||||
textarea-classes="!bg-foundation !border border-outline-2 p-2 rounded-lg font-mono !font-body-2xs"
|
||||
rows="15"
|
||||
/>
|
||||
</LayoutDialogSection>
|
||||
@@ -27,11 +27,11 @@
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { BeakerIcon } from '@heroicons/vue/24/outline'
|
||||
import { LayoutDialogSection } from '@speckle/ui-components'
|
||||
import { formatVersionParams } from '~/lib/automate/helpers/jsonSchema'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { AutomateFunctionPageParametersDialog_AutomateFunctionReleaseFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
|
||||
graphql(`
|
||||
fragment AutomateFunctionPageParametersDialog_AutomateFunctionRelease on AutomateFunctionRelease {
|
||||
@@ -46,4 +46,12 @@ const props = defineProps<{
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true })
|
||||
const finalParams = computed(() => formatVersionParams(props.release.inputSchema))
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
{
|
||||
text: 'Close',
|
||||
props: { color: 'outline' },
|
||||
onClick: () => (open.value = false)
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="pt-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<Portal to="navigation">
|
||||
<HeaderNavLink :to="automationFunctionsRoute" :name="'Automate functions'" />
|
||||
</Portal>
|
||||
|
||||
<h1 class="text-heading-xl">Automate functions</h1>
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<Portal to="navigation">
|
||||
<template v-if="!!workspace?.slug && !!workspace?.name">
|
||||
<HeaderNavLink
|
||||
:to="workspaceRoute(workspace.slug)"
|
||||
:name="workspace.name"
|
||||
:separator="false"
|
||||
/>
|
||||
</template>
|
||||
<HeaderNavLink
|
||||
:to="workspaceFunctionsRoute(workspace?.slug!)"
|
||||
name="Functions"
|
||||
:separator="!!workspace"
|
||||
/>
|
||||
</Portal>
|
||||
<div class="flex flex-col md:flex-row gap-y-2 md:gap-x-4 md:justify-between">
|
||||
<div class="w-full flex flex-row justify-between gap-2">
|
||||
<FormTextInput
|
||||
name="search"
|
||||
placeholder="Search functions..."
|
||||
placeholder="Search..."
|
||||
show-clear
|
||||
color="foundation"
|
||||
class="grow"
|
||||
v-bind="bind"
|
||||
v-on="on"
|
||||
/>
|
||||
<FormButton
|
||||
v-if="canCreateFunction"
|
||||
:icon-left="PlusIcon"
|
||||
@click="() => (createDialogOpen = true)"
|
||||
>
|
||||
New Function
|
||||
<FormButton :disabled="!canCreateFunction" @click="createDialogOpen = true">
|
||||
New function
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,21 +35,27 @@
|
||||
:is-authorized="!!activeUser?.automateInfo.hasAutomateGithubApp"
|
||||
:github-orgs="activeUser?.automateInfo.availableGithubOrgs || []"
|
||||
:templates="availableTemplates"
|
||||
:workspace="workspace"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlusIcon } from '@heroicons/vue/24/outline'
|
||||
import type { Nullable, Optional } from '@speckle/shared'
|
||||
import { Roles, type Nullable, type Optional } from '@speckle/shared'
|
||||
import { useDebouncedTextInput } from '@speckle/ui-components'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { AutomateFunctionsPageHeader_QueryFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { automationFunctionsRoute } from '~/lib/common/helpers/route'
|
||||
import type {
|
||||
AutomateFunctionCreateDialog_WorkspaceFragment,
|
||||
AutomateFunctionsPageHeader_QueryFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { workspaceFunctionsRoute, workspaceRoute } from '~/lib/common/helpers/route'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
|
||||
graphql(`
|
||||
fragment AutomateFunctionsPageHeader_Query on Query {
|
||||
activeUser {
|
||||
id
|
||||
role
|
||||
automateInfo {
|
||||
hasAutomateGithubApp
|
||||
availableGithubOrgs
|
||||
@@ -64,6 +74,7 @@ graphql(`
|
||||
const props = defineProps<{
|
||||
activeUser: Optional<AutomateFunctionsPageHeader_QueryFragment['activeUser']>
|
||||
serverInfo: Optional<AutomateFunctionsPageHeader_QueryFragment['serverInfo']>
|
||||
workspace?: AutomateFunctionCreateDialog_WorkspaceFragment
|
||||
}>()
|
||||
const search = defineModel<string>('search')
|
||||
|
||||
@@ -71,15 +82,18 @@ const { on, bind } = useDebouncedTextInput({ model: search })
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const mixpanel = useMixpanel()
|
||||
|
||||
const createDialogOpen = ref(false)
|
||||
|
||||
const availableTemplates = computed(
|
||||
() => props.serverInfo?.automate.availableFunctionTemplates || []
|
||||
)
|
||||
const canCreateFunction = computed(
|
||||
() => !!props.activeUser?.id && !!availableTemplates.value.length
|
||||
)
|
||||
const canCreateFunction = computed(() => {
|
||||
return props.workspace
|
||||
? !!props.activeUser?.id && !!availableTemplates.value.length
|
||||
: props.activeUser?.role === Roles.Server.Admin
|
||||
})
|
||||
|
||||
if (import.meta.client) {
|
||||
watch(
|
||||
@@ -92,6 +106,7 @@ if (import.meta.client) {
|
||||
type: ToastNotificationType.Success,
|
||||
title: 'GitHub authorization successful'
|
||||
})
|
||||
mixpanel.track('Automate Finish Authorize GitHub App')
|
||||
createDialogOpen.value = true
|
||||
} else if (ghAuthVal === 'access_denied') {
|
||||
triggerNotification({
|
||||
@@ -114,5 +129,15 @@ if (import.meta.client) {
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
watch(
|
||||
() => route.query['automateBetaRedirect'] as Nullable<string>,
|
||||
(isRedirect) => {
|
||||
if (!isRedirect?.length) return
|
||||
mixpanel.track('Automate Beta Visit Redirected')
|
||||
const { automateBetaRedirect, ...query } = route.query
|
||||
void router.replace({ query })
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { AutomateFunctionsPageItems_QueryFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import type {
|
||||
AutomateAutomationCreateDialog_AutomateFunctionFragment,
|
||||
AutomationsFunctionsCard_AutomateFunctionFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import type { CreateAutomationSelectableFunction } from '~/lib/automate/helpers/automations'
|
||||
|
||||
defineEmits<{
|
||||
@@ -28,7 +31,7 @@ defineEmits<{
|
||||
|
||||
graphql(`
|
||||
fragment AutomateFunctionsPageItems_Query on Query {
|
||||
automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) {
|
||||
automateFunctions(limit: 6, filter: { search: $search }, cursor: $cursor) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
@@ -41,10 +44,11 @@ graphql(`
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
functions?: AutomateFunctionsPageItems_QueryFragment
|
||||
functions?: (AutomationsFunctionsCard_AutomateFunctionFragment &
|
||||
AutomateAutomationCreateDialog_AutomateFunctionFragment)[]
|
||||
search?: boolean
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const fns = computed(() => props.functions?.automateFunctions.items || [])
|
||||
const fns = computed(() => props.functions || [])
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<CommonBadge :color-classes="[runStatusClasses(run), 'shrink-0 grow-0'].join(' ')">
|
||||
<CommonBadge
|
||||
:color-classes="
|
||||
[runStatusClasses(run), 'shrink-0 grow-0 text-foreground'].join(' ')
|
||||
"
|
||||
>
|
||||
{{ run.status.toUpperCase() }}
|
||||
</CommonBadge>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<LayoutTable
|
||||
:columns="[
|
||||
{ id: 'status', header: 'status', classes: 'col-span-2' },
|
||||
{ id: 'status', header: 'Status', classes: 'col-span-2' },
|
||||
{ id: 'runId', header: 'Run ID', classes: 'col-span-3' },
|
||||
{ id: 'modelVersion', header: 'Model version', classes: 'col-span-3' },
|
||||
{ id: 'date', header: 'Date', classes: 'col-span-2' },
|
||||
@@ -13,8 +13,7 @@
|
||||
{
|
||||
icon: EyeIcon,
|
||||
label: 'View',
|
||||
action: onView,
|
||||
class: '!text-primary'
|
||||
action: onView
|
||||
}
|
||||
]"
|
||||
empty-message="Automation does not have any runs"
|
||||
|
||||
@@ -3,17 +3,15 @@
|
||||
<template #header>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center space-x-2 max-w-full w-full">
|
||||
<div class="mt-[6px] shrink-0">
|
||||
<AutomateRunsTriggerStatusIcon
|
||||
:summary="summary"
|
||||
class="h-6 w-6 sm:h-10 sm:w-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-col gap-1">
|
||||
<h4 :class="[`h6 sm:h5 font-medium whitespace-normal`, summary.titleColor]">
|
||||
<AutomateRunsTriggerStatusIcon
|
||||
:summary="summary"
|
||||
class="h-6 w-6 sm:h-10 sm:w-10"
|
||||
/>
|
||||
<div class="flex min-w-0 flex-col gap-0.5">
|
||||
<h4 class="text-heading-sm" :class="[summary.titleColor]">
|
||||
{{ summary.title }}
|
||||
</h4>
|
||||
<div class="caption text-foreground-2 whitespace-normal">
|
||||
<div class="text-body-2xs text-foreground-2">
|
||||
{{ summary.longSummary }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,7 +38,7 @@
|
||||
to="https://speckle.systems/blog/automate-with-speckle/"
|
||||
class="order-2 sm:order-1"
|
||||
>
|
||||
Learn more about Automate here!
|
||||
Learn more about Automate
|
||||
</FormButton>
|
||||
<div
|
||||
class="flex w-full justify-between order-1 sm:order-2 sm:justify-normal sm:w-auto sm:space-x-1"
|
||||
|
||||
+80
-76
@@ -1,95 +1,99 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="flex flex-col gap-y-2 sm:flex-row sm:gap-y-0 sm:gap-x-2 sm:items-center my-2 py-1 px-2 border border-blue-500/10 rounded-md sm:h-12"
|
||||
class="flex flex-col lg:flex-row gap-y-1 my-2 py-2 px-2 border border-outline-2 rounded-md lg:items-center"
|
||||
>
|
||||
<div class="flex gap-x-2 items-center truncate">
|
||||
<div>
|
||||
<div class="flex flex-col lg:flex-row gap-2 flex-1">
|
||||
<div class="flex items-center gap-x-1.5">
|
||||
<Component
|
||||
:is="statusMetaData.icon"
|
||||
v-tippy="functionRun.status"
|
||||
:class="['h-5 w-5 outline-none', statusMetaData.iconColor]"
|
||||
:class="['h-4 w-4 outline-none', statusMetaData.iconColor]"
|
||||
/>
|
||||
<AutomateFunctionLogo :logo="functionRun.function?.logo" size="xs" />
|
||||
</div>
|
||||
<AutomateFunctionLogo :logo="functionRun.function?.logo" size="xs" />
|
||||
|
||||
<div class="font-medium text-sm truncate">
|
||||
{{ automationName ? automationName + ' / ' : ''
|
||||
}}{{ functionRun.function?.name || 'Unknown function' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col gap-y-1">
|
||||
<div class="font-medium text-body-2xs truncate">
|
||||
{{ automationName ? automationName + ' / ' : ''
|
||||
}}{{ functionRun.function?.name || 'Unknown function' }}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-2 sm:items-center truncate">
|
||||
<div class="sm:truncate">
|
||||
<div
|
||||
v-if="
|
||||
[
|
||||
AutomateRunStatus.Initializing,
|
||||
AutomateRunStatus.Running,
|
||||
AutomateRunStatus.Pending
|
||||
].includes(functionRun.status)
|
||||
"
|
||||
class="text-sm text-foreground-2 italic whitespace-normal sm:truncate"
|
||||
>
|
||||
Function is {{ functionRun.status.toLowerCase() }}.
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-sm text-foreground-2 italic whitespace-normal sm:truncate"
|
||||
>
|
||||
{{ functionRun.statusMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-grow text-right flex-shrink-0 bg-pink-300/0 justify-end space-x-2 items-center"
|
||||
>
|
||||
<div
|
||||
v-if="attachments && attachments.length !== 0"
|
||||
class="flex space-x-1 shrink items-center"
|
||||
>
|
||||
<div v-if="attachments.length === 1">
|
||||
<AutomateRunsAttachmentButton
|
||||
:blob-id="attachments[0]"
|
||||
:project-id="projectId"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
<FormButton
|
||||
v-if="attachments.length > 1"
|
||||
size="sm"
|
||||
color="outline"
|
||||
class="mt-1"
|
||||
@click="showAttachmentDialog = true"
|
||||
>
|
||||
{{ attachments.length }} attachments
|
||||
</FormButton>
|
||||
<LayoutDialog
|
||||
v-model:open="showAttachmentDialog"
|
||||
:title="`${functionRun.function?.name || 'Unknown function'} attachments`"
|
||||
max-width="sm"
|
||||
>
|
||||
<div v-for="id in attachments" :key="id">
|
||||
<AutomateRunsAttachmentButton
|
||||
:blob-id="id"
|
||||
:restrict-width="false"
|
||||
:project-id="projectId"
|
||||
size="xs"
|
||||
/>
|
||||
<div class="flex flex-col sm:flex-row gap-2 sm:items-center truncate">
|
||||
<div class="sm:truncate">
|
||||
<div
|
||||
v-if="
|
||||
[
|
||||
AutomateRunStatus.Initializing,
|
||||
AutomateRunStatus.Running,
|
||||
AutomateRunStatus.Pending
|
||||
].includes(functionRun.status)
|
||||
"
|
||||
class="text-body-2xs text-foreground-2 italic whitespace-normal sm:truncate"
|
||||
>
|
||||
Function is {{ functionRun.status.toLowerCase() }}.
|
||||
</div>
|
||||
</LayoutDialog>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<FormButton
|
||||
v-if="functionRun.contextView"
|
||||
size="sm"
|
||||
:to="functionRun.contextView"
|
||||
target="_blank"
|
||||
<div
|
||||
v-else
|
||||
class="text-body-2xs text-foreground-2 italic whitespace-normal sm:truncate"
|
||||
>
|
||||
{{ functionRun.statusMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-grow text-right flex-shrink-0 bg-pink-300/0 justify-end space-x-2 items-center"
|
||||
>
|
||||
View Results
|
||||
</FormButton>
|
||||
<div
|
||||
v-if="attachments && attachments.length !== 0"
|
||||
class="flex space-x-1 shrink items-center"
|
||||
>
|
||||
<div v-if="attachments.length === 1">
|
||||
<AutomateRunsAttachmentButton
|
||||
:blob-id="attachments[0]"
|
||||
:project-id="projectId"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
<FormButton
|
||||
v-if="attachments.length > 1"
|
||||
size="sm"
|
||||
color="outline"
|
||||
class="mt-1"
|
||||
@click="showAttachmentDialog = true"
|
||||
>
|
||||
{{ attachments.length }} attachments
|
||||
</FormButton>
|
||||
<LayoutDialog
|
||||
v-model:open="showAttachmentDialog"
|
||||
:title="`${
|
||||
functionRun.function?.name || 'Unknown function'
|
||||
} attachments`"
|
||||
max-width="sm"
|
||||
>
|
||||
<div v-for="id in attachments" :key="id">
|
||||
<AutomateRunsAttachmentButton
|
||||
:blob-id="id"
|
||||
:restrict-width="false"
|
||||
:project-id="projectId"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
</LayoutDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormButton
|
||||
v-if="functionRun.contextView"
|
||||
size="sm"
|
||||
color="outline"
|
||||
:to="functionRun.contextView"
|
||||
target="_blank"
|
||||
>
|
||||
View results
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,15 +3,13 @@
|
||||
<ViewerLayoutPanel @close="$emit('close')">
|
||||
<template #title>Automate</template>
|
||||
|
||||
<div class="flex items-center space-x-2 w-full pl-3 mt-2">
|
||||
<div class="mt-[6px] shrink-0">
|
||||
<AutomateRunsTriggerStatusIcon :summary="summary" class="h-6 w-6" />
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-col gap-1">
|
||||
<h4 :class="[`label font-medium whitespace-normal`, summary.titleColor]">
|
||||
<div class="flex items-center space-x-2 w-full pl-3 mt-3 mb-1">
|
||||
<AutomateRunsTriggerStatusIcon :summary="summary" class="h-6 w-6" />
|
||||
<div class="flex min-w-0 flex-col gap-0.5">
|
||||
<h4 class="text-heading-sm" :class="[summary.titleColor]">
|
||||
{{ summary.title }}
|
||||
</h4>
|
||||
<div class="caption text-foreground-2 whitespace-normal">
|
||||
<div class="text-body-2xs text-foreground-2">
|
||||
{{ summary.longSummary }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
:class="`border border-blue-500/10 rounded-md space-y-2 overflow-hidden ${
|
||||
expanded ? 'shadow' : ''
|
||||
}`"
|
||||
class="border border-outline-2 rounded-md space-y-2 overflow-hidden bg-foundation shadow-sm"
|
||||
>
|
||||
<button
|
||||
class="flex space-x-1 items-center max-w-full w-full px-1 py-1 h-8 transition hover:bg-primary-muted"
|
||||
class="flex space-x-1.5 items-center max-w-full w-full px-2 py-1 h-8 transition hover:bg-primary-muted bg-foundation"
|
||||
@click="expanded = !expanded"
|
||||
>
|
||||
<div>
|
||||
@@ -16,7 +14,7 @@
|
||||
/>
|
||||
</div>
|
||||
<AutomateFunctionLogo :logo="functionRun.function?.logo" size="xs" />
|
||||
<div class="font-medium text-xs truncate">
|
||||
<div class="font-medium text-body-2xs truncate">
|
||||
{{ automationName ? automationName + ' / ' : ''
|
||||
}}{{ functionRun.function?.name || 'Unknown function' }}
|
||||
</div>
|
||||
@@ -31,33 +29,21 @@
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
<div v-if="expanded" class="px-2 pb-2 space-y-4">
|
||||
<div v-if="expanded" class="px-3 pb-2 space-y-4">
|
||||
<!-- Status message -->
|
||||
<div class="space-y-1">
|
||||
<div class="text-xs font-medium text-foreground-2">Status</div>
|
||||
<div
|
||||
v-if="
|
||||
[
|
||||
AutomateRunStatus.Initializing,
|
||||
AutomateRunStatus.Running,
|
||||
AutomateRunStatus.Pending
|
||||
].includes(functionRun.status)
|
||||
"
|
||||
class="text-xs text-foreground-2 italic"
|
||||
>
|
||||
Function is {{ functionRun.status.toLowerCase() }}.
|
||||
</div>
|
||||
<div v-else class="text-xs text-foreground-2 italic">
|
||||
{{ functionRun.statusMessage || 'No status message' }}
|
||||
<div class="text-body-2xs font-medium text-foreground">Status</div>
|
||||
<div class="text-body-2xs text-foreground-2 whitespace-pre-wrap">
|
||||
{{ statusMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attachments -->
|
||||
<div
|
||||
v-if="attachments.length !== 0"
|
||||
class="border-t pt-2 border-foreground-2 space-y-1"
|
||||
class="border-t pt-2 border-outline-2 space-y-1"
|
||||
>
|
||||
<div class="text-xs font-medium text-foreground-2">Attachments</div>
|
||||
<div class="text-body-2xs font-medium text-foreground-2">Attachments</div>
|
||||
<div class="ml-[2px] justify-start">
|
||||
<AutomateRunsAttachmentButton
|
||||
v-for="id in attachments"
|
||||
@@ -71,11 +57,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- Context view -->
|
||||
<div v-if="hasValidContextView" class="border-t pt-2 border-foreground-2">
|
||||
<div v-if="hasValidContextView" class="border-t pt-2 border-outline-2">
|
||||
<div>
|
||||
<FormButton
|
||||
size="sm"
|
||||
link
|
||||
color="outline"
|
||||
class="truncate max-w-full"
|
||||
:to="functionRun.contextView || ''"
|
||||
>
|
||||
@@ -86,9 +72,9 @@
|
||||
<!-- Results -->
|
||||
<div
|
||||
v-if="!!results?.values.objectResults.length"
|
||||
class="border-t pt-2 border-foreground-2"
|
||||
class="border-t pt-2 border-outline-2"
|
||||
>
|
||||
<div class="text-xs font-medium text-foreground-2 mb-2">Results</div>
|
||||
<div class="text-body-2xs font-medium text-foreground-2 mb-2">Results</div>
|
||||
<div class="space-y-1">
|
||||
<AutomateViewerPanelFunctionRunRowObjectResult
|
||||
v-for="(result, index) in results.values.objectResults.slice(
|
||||
@@ -168,4 +154,16 @@ const hasValidContextView = computed(() => {
|
||||
const currentPath = route.fullPath
|
||||
return !doesRouteFitTarget(ctxView, currentPath)
|
||||
})
|
||||
|
||||
const statusMessage = computed(() => {
|
||||
const isFinished = ![
|
||||
AutomateRunStatus.Initializing,
|
||||
AutomateRunStatus.Running,
|
||||
AutomateRunStatus.Pending
|
||||
].includes(props.functionRun.status)
|
||||
|
||||
return isFinished
|
||||
? props.functionRun.statusMessage ?? 'No status message'
|
||||
: `Function is ${props.functionRun.status.toLowerCase()}.`
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div :class="`overflow-hidden`">
|
||||
<div class="overflow-hidden">
|
||||
<button
|
||||
:class="`block transition text-left hover:bg-primary-muted hover:shadow-md rounded-md p-1 cursor-pointer border-l-2 ${
|
||||
:class="`block w-full transition text-left hover:bg-primary-muted hover:shadow-md rounded-md p-1 cursor-pointer border-l-2 ${
|
||||
isIsolated || metadataGradientIsSet
|
||||
? 'border-primary bg-primary-muted shadow-md'
|
||||
: 'border-transparent'
|
||||
@@ -30,6 +30,7 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CheckIcon,
|
||||
XMarkIcon,
|
||||
InformationCircleIcon,
|
||||
ExclamationTriangleIcon
|
||||
@@ -154,6 +155,11 @@ const setOrUnsetGradient = () => {
|
||||
|
||||
const iconAndColor = computed(() => {
|
||||
switch (props.result.level) {
|
||||
case 'SUCCESS':
|
||||
return {
|
||||
icon: CheckIcon,
|
||||
color: 'text-success font-medium'
|
||||
}
|
||||
case 'ERROR':
|
||||
return {
|
||||
icon: XMarkIcon,
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="!hasValidPlan">
|
||||
<div
|
||||
v-if="condensed"
|
||||
class="flex items-center justify-between rounded-md p-2 pl-3 text-body-3xs font-medium bg-info-lighter text-primary-focus gap-x-2"
|
||||
>
|
||||
{{ title }}
|
||||
<FormButton
|
||||
v-if="actions.length > 0"
|
||||
size="sm"
|
||||
:disabled="actions[0].disabled"
|
||||
@click="actions[0].onClick"
|
||||
>
|
||||
{{ actions[0].title }}
|
||||
</FormButton>
|
||||
</div>
|
||||
<CommonAlert v-else :color="alertColor" :actions="actions">
|
||||
<template #title>
|
||||
{{ title }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ description }}
|
||||
</template>
|
||||
</CommonAlert>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import {
|
||||
type BillingAlert_WorkspaceFragment,
|
||||
WorkspacePlanStatuses
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { useBillingActions } from '~/lib/billing/composables/actions'
|
||||
import type { AlertAction, AlertColor } from '@speckle/ui-components'
|
||||
|
||||
graphql(`
|
||||
fragment BillingAlert_Workspace on Workspace {
|
||||
id
|
||||
plan {
|
||||
name
|
||||
status
|
||||
createdAt
|
||||
}
|
||||
subscription {
|
||||
billingInterval
|
||||
currentBillingCycleEnd
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
workspace: BillingAlert_WorkspaceFragment
|
||||
actions?: Array<AlertAction>
|
||||
condensed?: boolean
|
||||
}>()
|
||||
|
||||
const { billingPortalRedirect } = useBillingActions()
|
||||
|
||||
const planStatus = computed(() => props.workspace.plan?.status)
|
||||
// If there is no plan status, we assume it's a trial
|
||||
const isTrial = computed(
|
||||
() => !planStatus.value || planStatus.value === WorkspacePlanStatuses.Trial
|
||||
)
|
||||
const isPaymentFailed = computed(
|
||||
() => planStatus.value === WorkspacePlanStatuses.PaymentFailed
|
||||
)
|
||||
const isScheduledForCancelation = computed(
|
||||
() => planStatus.value === WorkspacePlanStatuses.CancelationScheduled
|
||||
)
|
||||
const trialDaysLeft = computed(() => {
|
||||
const createdAt = props.workspace.plan?.createdAt
|
||||
const trialEndDate = dayjs(createdAt).add(31, 'days')
|
||||
const diffDays = trialEndDate.diff(dayjs(), 'day')
|
||||
return Math.max(0, diffDays)
|
||||
})
|
||||
const title = computed(() => {
|
||||
if (isTrial.value) {
|
||||
if (trialDaysLeft.value === 0) {
|
||||
return 'Final day of free trial'
|
||||
}
|
||||
if (props.condensed) {
|
||||
return `${trialDaysLeft.value} day${
|
||||
trialDaysLeft.value !== 1 ? 's' : ''
|
||||
} left in trial`
|
||||
} else
|
||||
return `You have ${trialDaysLeft.value} day${
|
||||
trialDaysLeft.value !== 1 ? 's' : ''
|
||||
} left on your free trial`
|
||||
}
|
||||
switch (planStatus.value) {
|
||||
case WorkspacePlanStatuses.CancelationScheduled:
|
||||
return `Your workspace subscription is scheduled for cancellation`
|
||||
case WorkspacePlanStatuses.Canceled:
|
||||
return `Your workspace subscription has been cancelled`
|
||||
case WorkspacePlanStatuses.Expired:
|
||||
return `Your free trial has ended`
|
||||
case WorkspacePlanStatuses.PaymentFailed:
|
||||
return "Your last payment didn't go through"
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
const description = computed(() => {
|
||||
if (isTrial.value) {
|
||||
return trialDaysLeft.value === 0
|
||||
? 'Upgrade to a paid plan to continue using your workspace'
|
||||
: 'Upgrade to a paid plan to start your subscription'
|
||||
}
|
||||
switch (planStatus.value) {
|
||||
case WorkspacePlanStatuses.CancelationScheduled:
|
||||
return 'Once the current billing cycle ends your workspace will enter read-only mode. Renew your subscription to undo.'
|
||||
case WorkspacePlanStatuses.Canceled:
|
||||
return 'Your workspace has been cancelled and is in read-only mode. Subscribe to a plan to regain full access.'
|
||||
case WorkspacePlanStatuses.Expired:
|
||||
return "The workspace is in a read-only locked state until there's an active subscription. Subscribe to a plan to regain full access."
|
||||
case WorkspacePlanStatuses.PaymentFailed:
|
||||
return "Update your payment information now to ensure your workspace doesn't go into maintenance mode."
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
const hasValidPlan = computed(() => planStatus.value === WorkspacePlanStatuses.Valid)
|
||||
|
||||
const alertColor = computed<AlertColor>(() => {
|
||||
switch (planStatus.value) {
|
||||
case WorkspacePlanStatuses.PaymentFailed:
|
||||
case WorkspacePlanStatuses.Canceled:
|
||||
return 'danger'
|
||||
case WorkspacePlanStatuses.CancelationScheduled:
|
||||
case WorkspacePlanStatuses.Expired:
|
||||
return 'warning'
|
||||
default:
|
||||
return 'neutral'
|
||||
}
|
||||
})
|
||||
|
||||
const actions = computed((): AlertAction[] => {
|
||||
const actions: Array<AlertAction> = props.actions ?? []
|
||||
|
||||
if (isPaymentFailed.value) {
|
||||
actions.push({
|
||||
title: 'Update payment information',
|
||||
onClick: () => billingPortalRedirect(props.workspace.id),
|
||||
disabled: !props.workspace.id
|
||||
})
|
||||
} else if (isScheduledForCancelation.value) {
|
||||
actions.push({
|
||||
title: 'Renew subscription',
|
||||
onClick: () => billingPortalRedirect(props.workspace.id),
|
||||
disabled: !props.workspace.id
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
})
|
||||
</script>
|
||||
@@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<ul>
|
||||
<li
|
||||
v-for="(item, index) in billingItems"
|
||||
:key="item.name"
|
||||
class="text-body-xs flex"
|
||||
:class="[index === billingItems.length - 1 ? 'border-b border-outline-3' : null]"
|
||||
>
|
||||
<p
|
||||
class="text-foreground flex-1 py-2 px-3"
|
||||
:class="[index < billingItems.length - 1 ? 'border-b border-outline-3' : null]"
|
||||
>
|
||||
{{ item.label }}
|
||||
<span class="text-foreground-2">x</span>
|
||||
£{{ item.cost }}
|
||||
</p>
|
||||
<p
|
||||
class="text-right text-foreground ml-4 w-32 md:w-40 py-2 px-3"
|
||||
:class="[index < billingItems.length - 1 ? 'border-b border-outline-3' : null]"
|
||||
>
|
||||
£{{ item.count * item.cost }} / month
|
||||
</p>
|
||||
</li>
|
||||
<li class="flex justify-between text-foreground font-medium">
|
||||
<p class="flex-1 p-3">Total</p>
|
||||
<p class="text-right w-32 md:w-40 ml-4 p-3">
|
||||
£{{ workspaceCost.subTotal }} / month
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { BillingSummary_WorkspaceCostFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
|
||||
graphql(`
|
||||
fragment BillingSummary_WorkspaceCost on WorkspaceCost {
|
||||
items {
|
||||
cost
|
||||
count
|
||||
name
|
||||
label
|
||||
}
|
||||
discount {
|
||||
amount
|
||||
name
|
||||
}
|
||||
subTotal
|
||||
total
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
workspaceCost: BillingSummary_WorkspaceCostFragment
|
||||
}>()
|
||||
|
||||
const billingItems = computed(() => props.workspaceCost.items)
|
||||
</script>
|
||||
@@ -4,7 +4,7 @@
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div v-if="title || description" class="flex-1">
|
||||
<div v-if="title" class="flex items-center gap-2">
|
||||
<p class="text-heading-sm text-foreground">{{ title }}</p>
|
||||
<CommonBadge v-if="badge" rounded>{{ badge }}</CommonBadge>
|
||||
@@ -21,20 +21,25 @@
|
||||
v-if="buttons"
|
||||
class="flex flex-col flex-wrap md:flex-row gap-y-2 md:gap-x-2 gap-y-0 mt-3"
|
||||
>
|
||||
<FormButton
|
||||
<div
|
||||
v-for="(button, index) in buttons"
|
||||
:key="button.id || index"
|
||||
v-bind="button.props || {}"
|
||||
:disabled="button.props?.disabled || button.disabled"
|
||||
:submit="button.props?.submit || button.submit"
|
||||
target="_blank"
|
||||
external
|
||||
size="sm"
|
||||
color="outline"
|
||||
@click="($event) => button.onClick?.($event)"
|
||||
v-tippy="button.disabledMessage"
|
||||
class="shrink-0"
|
||||
>
|
||||
{{ button.text }}
|
||||
</FormButton>
|
||||
<FormButton
|
||||
v-bind="button.props || {}"
|
||||
:disabled="button.props?.disabled || button.disabled"
|
||||
:submit="button.props?.submit || button.submit"
|
||||
target="_blank"
|
||||
external
|
||||
size="sm"
|
||||
color="outline"
|
||||
@click="($event) => button.onClick?.($event)"
|
||||
>
|
||||
{{ button.text }}
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
:icon-left="ClipboardDocumentIcon"
|
||||
hide-text
|
||||
@click="onCopy"
|
||||
></FormButton>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div
|
||||
v-if="cleanReadmeHtml.length"
|
||||
:class="proseClasses"
|
||||
v-html="cleanReadmeHtml"
|
||||
></div>
|
||||
<div v-else class="italic text-center">No readme found</div>
|
||||
<div v-if="cleanReadmeHtml.length" :class="proseClasses" v-html="cleanReadmeHtml" />
|
||||
<div v-else class="text-foreground-2 text-body-xs text-center italic">
|
||||
No readme found
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { Nullable } from '@speckle/shared'
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<div class="bg-foundation text-foreground rounded shadow-md p-2">
|
||||
<CommonTiptapMentionListItem
|
||||
v-if="existingUser"
|
||||
:item="existingUser"
|
||||
is-selected
|
||||
@click="enterHandler"
|
||||
/>
|
||||
<FormButton v-else @click="enterHandler">Invite {{ query }}</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
SuggestionCommandProps,
|
||||
SuggestionKeyDownProps
|
||||
} from '~~/lib/core/tiptap/email-mention/suggestion'
|
||||
import type { SuggestionOptionsItem } from '~~/lib/core/tiptap/mentionExtension'
|
||||
|
||||
const props = defineProps<{
|
||||
query: string
|
||||
items: SuggestionOptionsItem[]
|
||||
command: (mention: SuggestionCommandProps) => void
|
||||
}>()
|
||||
|
||||
const existingUser = computed(() => props.items[0] || null)
|
||||
|
||||
const enterHandler = () => {
|
||||
if (existingUser.value) {
|
||||
// Create a mention of the existing user
|
||||
props.command({
|
||||
mention: { id: existingUser.value.id, label: existingUser.value.name },
|
||||
email: null
|
||||
})
|
||||
} else {
|
||||
// Trigger invite and close popup
|
||||
props.command({
|
||||
email: props.query,
|
||||
mention: null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onKeyDown = (params: SuggestionKeyDownProps) => {
|
||||
const { event } = params
|
||||
if (event.key === 'Enter') {
|
||||
enterHandler()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
onKeyDown
|
||||
})
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
v-show="(query?.length || 0) >= 3"
|
||||
v-show="(query?.length || 0) >= 1"
|
||||
class="bg-foundation text-foreground rounded shadow-md p-2"
|
||||
>
|
||||
<ul>
|
||||
@@ -14,7 +14,7 @@
|
||||
</li>
|
||||
</template>
|
||||
<template v-else>
|
||||
<li>Couldn't find anything 🤷</li>
|
||||
<li>Couldn't find anyone</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -44,13 +44,9 @@ const props = defineProps<{
|
||||
placeholder?: string
|
||||
readonly?: boolean
|
||||
/**
|
||||
* Used to invite users to project when their emails are mentioned
|
||||
* Used to scope things like user mentions to project collaborators etc.
|
||||
*/
|
||||
projectId?: string
|
||||
/**
|
||||
* Disable invitation CTA, e.g. if user doesn't have the required accesses
|
||||
*/
|
||||
disableInvitationCta?: boolean
|
||||
}>()
|
||||
|
||||
const editorContentRef = ref(null as Nullable<HTMLElement>)
|
||||
@@ -65,8 +61,7 @@ const editor = new Editor({
|
||||
editable: isEditable.value,
|
||||
extensions: getEditorExtensions(props.schemaOptions, {
|
||||
placeholder: props.placeholder,
|
||||
projectId:
|
||||
props.projectId && !props.disableInvitationCta ? props.projectId : undefined
|
||||
projectId: props.projectId
|
||||
}),
|
||||
onUpdate: () => {
|
||||
const data = getData()
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
>
|
||||
<WorkspaceAvatar
|
||||
:logo="project.workspace.logo"
|
||||
:default-logo-index="project.workspace.defaultLogoIndex"
|
||||
:name="project.workspace.name"
|
||||
size="sm"
|
||||
/>
|
||||
<p class="text-body-2xs text-foreground ml-2">
|
||||
@@ -77,7 +77,7 @@ graphql(`
|
||||
id
|
||||
slug
|
||||
name
|
||||
...WorkspaceAvatar_Workspace
|
||||
logo
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -58,40 +58,44 @@
|
||||
v-if="isWorkspacesEnabled"
|
||||
collapsible
|
||||
title="Workspaces"
|
||||
:plus-click="isNotGuest ? handlePlusClick : undefined"
|
||||
plus-text="Create workspace"
|
||||
:icon-click="isNotGuest ? handlePlusClick : undefined"
|
||||
icon-text="Create workspace"
|
||||
>
|
||||
<NuxtLink :to="workspacesRoute" @click="handleIntroducingWorkspacesClick">
|
||||
<LayoutSidebarMenuGroupItem
|
||||
v-if="!hasWorkspaces || route.path === workspacesRoute"
|
||||
label="Introducing workspaces"
|
||||
:active="isActive(workspacesRoute)"
|
||||
tag="BETA"
|
||||
>
|
||||
<template #icon>
|
||||
<IconWorkspaces class="size-4 text-foreground-2" />
|
||||
</template>
|
||||
</LayoutSidebarMenuGroupItem>
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
v-for="(item, key) in workspacesItems"
|
||||
:key="key"
|
||||
:to="item.to"
|
||||
@click="isOpenMobile = false"
|
||||
>
|
||||
<LayoutSidebarMenuGroupItem
|
||||
:label="item.label"
|
||||
:active="isActive(item.to)"
|
||||
class="!pl-1"
|
||||
<template v-for="(item, key) in workspacesItems" :key="key">
|
||||
<NuxtLink
|
||||
v-if="item.creationState.completed !== false"
|
||||
:to="item.to"
|
||||
@click="isOpenMobile = false"
|
||||
>
|
||||
<template #icon>
|
||||
<WorkspaceAvatar
|
||||
:logo="item.logo"
|
||||
:default-logo-index="item.defaultLogoIndex"
|
||||
size="sm"
|
||||
/>
|
||||
</template>
|
||||
</LayoutSidebarMenuGroupItem>
|
||||
</NuxtLink>
|
||||
<LayoutSidebarMenuGroupItem
|
||||
:label="item.label"
|
||||
:active="isActive(item.to)"
|
||||
:tag="
|
||||
item.plan.status === WorkspacePlanStatuses.Trial ||
|
||||
item.plan.status === WorkspacePlanStatuses.Expired ||
|
||||
!item.plan.status
|
||||
? 'TRIAL'
|
||||
: undefined
|
||||
"
|
||||
class="!pl-1"
|
||||
>
|
||||
<template #icon>
|
||||
<WorkspaceAvatar :name="item.name" :logo="item.logo" size="sm" />
|
||||
</template>
|
||||
</LayoutSidebarMenuGroupItem>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</LayoutSidebarMenuGroup>
|
||||
|
||||
<LayoutSidebarMenuGroup title="Resources" collapsible>
|
||||
@@ -152,32 +156,17 @@
|
||||
</NuxtLink>
|
||||
</LayoutSidebarMenuGroup>
|
||||
</LayoutSidebarMenu>
|
||||
<template #promo>
|
||||
<LayoutSidebarPromo
|
||||
title="SpeckleCon 2024"
|
||||
text="Join us in London on Nov 13-14 for the ultimate community event."
|
||||
button-text="Get tickets"
|
||||
@on-click="onPromoClick"
|
||||
/>
|
||||
</template>
|
||||
</LayoutSidebar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<FeedbackDialog v-model:open="showFeedbackDialog" />
|
||||
|
||||
<WorkspaceCreateDialog
|
||||
v-model:open="showWorkspaceCreateDialog"
|
||||
navigate-on-success
|
||||
event-source="sidebar"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
FormButton,
|
||||
LayoutSidebar,
|
||||
LayoutSidebarPromo,
|
||||
LayoutSidebarMenu,
|
||||
LayoutSidebarMenuGroup,
|
||||
LayoutSidebarMenuGroupItem
|
||||
@@ -189,13 +178,30 @@ import {
|
||||
projectsRoute,
|
||||
workspaceRoute,
|
||||
workspacesRoute,
|
||||
downloadManagerUrl
|
||||
downloadManagerUrl,
|
||||
workspaceCreateRoute
|
||||
} from '~/lib/common/helpers/route'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
|
||||
import { HomeIcon } from '@heroicons/vue/24/outline'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import { WorkspacePlanStatuses } from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
graphql(`
|
||||
fragment Sidebar_User on User {
|
||||
id
|
||||
automateFunctions {
|
||||
items {
|
||||
id
|
||||
name
|
||||
description
|
||||
logo
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const { isLoggedIn } = useActiveUser()
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
@@ -205,7 +211,6 @@ const { activeUser: user } = useActiveUser()
|
||||
const mixpanel = useMixpanel()
|
||||
|
||||
const isOpenMobile = ref(false)
|
||||
const showWorkspaceCreateDialog = ref(false)
|
||||
const showFeedbackDialog = ref(false)
|
||||
|
||||
const { result: workspaceResult, onResult: onWorkspaceResult } = useQuery(
|
||||
@@ -223,18 +228,24 @@ const isActive = (...routes: string[]): boolean => {
|
||||
const isNotGuest = computed(
|
||||
() => Roles.Server.Admin || user.value?.role === Roles.Server.User
|
||||
)
|
||||
|
||||
const workspacesItems = computed(() =>
|
||||
workspaceResult.value?.activeUser
|
||||
? workspaceResult.value.activeUser.workspaces.items.map((workspace) => ({
|
||||
label: workspace.name,
|
||||
name: workspace.name,
|
||||
id: workspace.id,
|
||||
to: workspaceRoute(workspace.slug),
|
||||
logo: workspace.logo,
|
||||
defaultLogoIndex: workspace.defaultLogoIndex
|
||||
plan: {
|
||||
status: workspace.plan?.status
|
||||
},
|
||||
creationState: {
|
||||
completed: workspace.creationState?.completed
|
||||
}
|
||||
}))
|
||||
: []
|
||||
)
|
||||
const hasWorkspaces = computed(() => workspacesItems.value.length > 0)
|
||||
|
||||
onWorkspaceResult((result) => {
|
||||
if (result.data?.activeUser) {
|
||||
@@ -248,22 +259,13 @@ onWorkspaceResult((result) => {
|
||||
}
|
||||
})
|
||||
|
||||
const onPromoClick = () => {
|
||||
mixpanel.track('Promo Banner Clicked', {
|
||||
source: 'sidebar',
|
||||
campaign: 'specklecon2024'
|
||||
})
|
||||
|
||||
window.open('https://conf.speckle.systems/', '_blank')
|
||||
}
|
||||
|
||||
const openFeedbackDialog = () => {
|
||||
showFeedbackDialog.value = true
|
||||
isOpenMobile.value = false
|
||||
}
|
||||
|
||||
const openWorkspaceCreateDialog = () => {
|
||||
showWorkspaceCreateDialog.value = true
|
||||
navigateTo(workspaceCreateRoute())
|
||||
mixpanel.track('Create Workspace Button Clicked', {
|
||||
source: 'sidebar'
|
||||
})
|
||||
|
||||
@@ -1,41 +1,38 @@
|
||||
<template>
|
||||
<NuxtLink :to="tutorial.url" target="_blank">
|
||||
<NuxtLink :to="tutorialItem.url" target="_blank" @click="trackClick">
|
||||
<div
|
||||
class="bg-foundation border border-outline-3 rounded-xl flex flex-col overflow-hidden hover:border-outline-5 transition"
|
||||
>
|
||||
<div
|
||||
:style="{ backgroundImage: `url(${tutorial.featureImage})` }"
|
||||
class="bg-foundation-page bg-cover bg-center w-full h-32"
|
||||
<NuxtImg
|
||||
:src="tutorialItem.image"
|
||||
:alt="tutorialItem.title"
|
||||
class="h-32 w-full object-cover"
|
||||
width="400"
|
||||
height="225"
|
||||
/>
|
||||
<div class="p-5 pb-4">
|
||||
<h3 v-if="tutorial.title" class="text-body-2xs text-foreground truncate">
|
||||
{{ tutorial.title }}
|
||||
<div class="p-5">
|
||||
<h3 class="text-body-2xs text-foreground truncate">
|
||||
{{ tutorialItem.title }}
|
||||
</h3>
|
||||
<p class="text-body-3xs text-foreground-2 mt-2">
|
||||
<span v-tippy="updatedAt.full">
|
||||
{{ updatedAt.relative }}
|
||||
</span>
|
||||
<template v-if="tutorial.readingTime">
|
||||
<span class="pl-1 pr-2">•</span>
|
||||
{{ tutorial.readingTime }}m read
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TutorialItem } from '~~/lib/dashboard/helpers/types'
|
||||
import type { TutorialItem } from '~/lib/dashboard/helpers/types'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
|
||||
const mixpanel = useMixpanel()
|
||||
|
||||
const props = defineProps<{
|
||||
tutorial: TutorialItem
|
||||
tutorialItem: TutorialItem
|
||||
}>()
|
||||
|
||||
const updatedAt = computed(() => {
|
||||
return {
|
||||
full: formattedFullDate(props.tutorial.publishedAt),
|
||||
relative: formattedRelativeDate(props.tutorial.publishedAt, { capitalize: true })
|
||||
}
|
||||
})
|
||||
const trackClick = () => {
|
||||
mixpanel.track('Tutorial clicked', {
|
||||
title: props.tutorialItem.title,
|
||||
url: props.tutorialItem.url
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
<template>
|
||||
<LayoutDialog
|
||||
v-model:open="isOpen"
|
||||
title="Give us feedback"
|
||||
:title="dialogTitle"
|
||||
:buttons="dialogButtons"
|
||||
:on-submit="onSubmit"
|
||||
max-width="md"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-body-xs text-foreground font-medium">
|
||||
How can we improve Speckle? If you have a feature request, please also share how
|
||||
you would use it and why it's important to you
|
||||
{{ dialogIntro }}
|
||||
</p>
|
||||
<FormTextArea
|
||||
v-model="feedback"
|
||||
@@ -18,6 +17,13 @@
|
||||
label="Feedback"
|
||||
color="foundation"
|
||||
/>
|
||||
<p v-if="!hideSuppport" class="text-body-xs !leading-4">
|
||||
Need help? For support, head over to our
|
||||
<FormButton to="https://speckle.community/" target="_blank" link text>
|
||||
community forum
|
||||
</FormButton>
|
||||
where we can chat and solve problems together.
|
||||
</p>
|
||||
</div>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
@@ -30,9 +36,24 @@ import { useZapier } from '~/lib/core/composables/zapier'
|
||||
import { useGlobalToast, ToastNotificationType } from '~~/lib/common/composables/toast'
|
||||
import { isRequired } from '~/lib/common/helpers/validation'
|
||||
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
|
||||
import { defaultZapierWebhookUrl } from '~/lib/common/helpers/route'
|
||||
|
||||
type FeedbackType = 'general' | 'gendo'
|
||||
type FormValues = { feedback: string }
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type?: FeedbackType
|
||||
title?: string
|
||||
intro?: string
|
||||
hideSuppport?: boolean
|
||||
metadata?: Record<string, unknown>
|
||||
}>(),
|
||||
{
|
||||
type: 'general'
|
||||
}
|
||||
)
|
||||
|
||||
const isOpen = defineModel<boolean>('open', { required: true })
|
||||
|
||||
const { activeUser: user } = useActiveUser()
|
||||
@@ -52,6 +73,14 @@ const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
}
|
||||
])
|
||||
|
||||
const dialogTitle = computed(() => props.title || 'Give us feedback')
|
||||
|
||||
const dialogIntro = computed(
|
||||
() =>
|
||||
props.intro ||
|
||||
'How can we improve Speckle? If you have a feature request, please also share how you would use it and why its important to you'
|
||||
)
|
||||
|
||||
const onSubmit = handleSubmit(async () => {
|
||||
if (!feedback.value) return
|
||||
|
||||
@@ -63,10 +92,12 @@ const onSubmit = handleSubmit(async () => {
|
||||
})
|
||||
|
||||
mixpanel.track('Feedback Sent', {
|
||||
message: feedback.value
|
||||
message: feedback.value,
|
||||
feedbackType: props.type,
|
||||
...props.metadata
|
||||
})
|
||||
|
||||
await sendWebhook('https://hooks.zapier.com/hooks/catch/12120532/2m4okri/', {
|
||||
await sendWebhook(defaultZapierWebhookUrl, {
|
||||
userId: user.value?.id ?? '',
|
||||
feedback: feedback.value
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<FormSelectBase
|
||||
v-model="selectedValue"
|
||||
:items="roles"
|
||||
:multiple="multiple"
|
||||
:clearable="clearable"
|
||||
name="projectRoles"
|
||||
label="Project roles"
|
||||
@@ -14,9 +13,7 @@
|
||||
:allow-unset="allowUnset"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template #nothing-selected>
|
||||
{{ multiple ? 'Select roles' : 'Select role' }}
|
||||
</template>
|
||||
<template #nothing-selected>Select role</template>
|
||||
<template #something-selected="{ value }">
|
||||
<template v-if="isMultiItemArrayValue(value)">
|
||||
<div ref="elementToWatchForChanges" class="flex items-center space-x-0.5">
|
||||
@@ -63,7 +60,6 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
multiple?: boolean
|
||||
modelValue?: ValueType
|
||||
clearable?: boolean
|
||||
disabledItems?: StreamRoles[]
|
||||
|
||||
@@ -133,6 +133,12 @@ const props = defineProps({
|
||||
ownedOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* Whether to only return projects within a specific workspace
|
||||
*/
|
||||
workspaceId: {
|
||||
type: String as PropType<Optional<string>>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -155,10 +161,11 @@ const invokeSearch = async (search: string) => {
|
||||
if (!isLoggedIn.value) return []
|
||||
const results = await apollo.query({
|
||||
query: searchProjectsQuery,
|
||||
variables: {
|
||||
variables: computed(() => ({
|
||||
search: search.trim().length ? search : null,
|
||||
onlyWithRoles: props.ownedOnly ? [Roles.Stream.Owner] : null
|
||||
}
|
||||
onlyWithRoles: props.ownedOnly ? [Roles.Stream.Owner] : null,
|
||||
...(props.workspaceId && { workspaceId: props.workspaceId })
|
||||
})).value
|
||||
})
|
||||
return results.data.activeUser?.projects.items || []
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user