32 Commits

Author SHA1 Message Date
Alan Rynne b9a7ec5c8f Merge pull request #2 from specklesystems/iain/update-speckle-domains
chore(domains): update speckle.xyz to app.speckle.systems
2024-08-12 11:46:12 +02:00
Iain Sproat 4775b26d31 chore(domains): update speckle.xyz to app.speckle.systems
- remove mention of deprecated DO 1-click deployment mechanism
2024-07-18 17:25:53 +01:00
Alan Rynne 2d65a5d667 Hotfix navbar order screwup 2021-10-22 12:32:58 +02:00
Alan Rynne 1986aacfae Caching alpha disclaimer acceptance 2021-10-22 12:28:46 +02:00
Alan Rynne 83bed8e2df Updated gitignore 2021-10-22 12:28:46 +02:00
Alan Rynne 6441351f62 Package update and cleanup 2021-10-22 12:28:46 +02:00
Alan Rynne 4ea10664ea Selection Details and other minor fixes 2021-10-22 12:28:46 +02:00
Matteo Cominetti 83d700a362 Create close-issue.yml 2021-10-02 17:14:52 +01:00
Matteo Cominetti 0963d89fa2 Create open-issue.yml 2021-10-02 17:14:39 +01:00
Alan Rynne 12d6529520 Fixes viewer resize problem 2021-10-01 14:31:41 +02:00
Alan Rynne 73da6c0e32 Part of matteos fixes in #1 2021-09-30 18:12:00 +02:00
Alan Rynne 3a5a9b5e76 Page titles 2021-09-30 13:41:07 +02:00
Alan Rynne cfdaf73ae9 Favicons 2021-09-30 13:36:10 +02:00
Alan Rynne a96af3cda8 Redirect fix! 2021-09-30 13:27:42 +02:00
Alan Rynne 07280e1bc4 Redirect file for netlify 2021-09-30 13:20:35 +02:00
Alan Rynne f685e5d8e0 Readme.md added 2021-09-30 13:09:20 +02:00
Alan Rynne 207bb423e9 Cleanup and progress report 2021-09-30 12:54:19 +02:00
Alan Rynne e384eba256 CSS fix 2021-09-30 11:49:06 +02:00
Alan Rynne 50f2bebea4 All linked up! 2021-09-30 11:16:35 +02:00
Alan Rynne debc7b1e44 Working linked charts with shared data 2021-09-30 09:30:52 +02:00
Alan Rynne 2b3c5750e0 Synched up charts 2021-09-29 12:40:45 +02:00
Alan Rynne 5c926af166 Unused methods cleanup 2021-09-29 10:47:33 +02:00
Alan Rynne ba3ce711e4 After review polish 2021-09-29 10:36:31 +02:00
Alan Rynne 28e43687d2 UI tweaks and improvements 2021-09-28 17:37:45 +02:00
Alan Rynne 4f68c8ac8f Minor tweaks 2021-09-28 17:17:05 +02:00
Alan Rynne e215ead6e6 Commit latest changes and obj loader. 2021-09-21 12:08:45 +02:00
Alan Rynne da3a823c2f Qucik push with current state 2021-09-13 13:05:10 +02:00
Alan Rynne bcff97c8ee Half way through app re-organisation 2021-09-12 18:40:00 +02:00
Alan Rynne bf6d2bb681 Merge pull request #3 from specklesystems/alan/cleanup
Cleanup: Removed promises in favour of async/await
2021-05-18 13:36:43 +02:00
Alan Rynne 95c4410bf2 cleanup: Added minor comments in speckleUtils.js 2021-05-18 13:23:24 +02:00
Alan Rynne a8a9145846 cleanup: Removed all promises in favour of async/await 2021-05-18 13:03:31 +02:00
Alan Rynne 270fc5bd5a feat: Added gif to readme 2021-04-30 14:28:08 +02:00
73 changed files with 2364 additions and 416 deletions
+4 -4
View File
@@ -1,7 +1,7 @@
# Replace these with the app id and app secret that you get once registering
# an application on the Speckle Server!
VUE_APP_SPECKLE_ID=c2c12aaad2
VUE_APP_SPECKLE_SECRET=3b4109dc32
VUE_APP_SPECKLE_ID=daf0e0546d
VUE_APP_SPECKLE_SECRET=3c92fff425
# Make sure you change this to use the server of your choice!
VUE_APP_SERVER_URL=https://speckle.xyz
VUE_APP_SPECKLE_NAME="Speckle Demo App"
VUE_APP_SERVER_URL=https://latest.speckle.dev
VUE_APP_SPECKLE_NAME="Speckle — Revit Dashboard"
+78
View File
@@ -0,0 +1,78 @@
name: Update issue Status
on:
issues:
types: [closed]
jobs:
update_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ORGANIZATION: specklesystems
PROJECT_NUMBER: 9
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectNext(number: $number) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo "$PROJECT_ID"
echo "$STATUS_FIELD_ID"
echo 'DONE_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .settings | fromjson | .options[] | select(.name== "Done") | .id' project_data.json) >> $GITHUB_ENV
echo "$DONE_ID"
- name: Add Issue to project #it's already in the project, but we do this to get its node id!
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $id:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
- name: Update Status
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $status:ID!, $id:ID!, $value:String!) {
set_status: updateProjectNextItemField(
input: {
projectId: $project
itemId: $id
fieldId: $status
value: $value
}
) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
+50
View File
@@ -0,0 +1,50 @@
name: Move new issues into Project
on:
issues:
types: [opened]
jobs:
track_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ORGANIZATION: specklesystems
PROJECT_NUMBER: 9
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectNext(number: $number) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
- name: Add Issue to project
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $id:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
+3
View File
@@ -21,3 +21,6 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?
.graphqlconfig
schema.graphql
+58 -24
View File
@@ -1,39 +1,73 @@
# Dev Guide: Create your own App
<h1 align="center">
<img src="https://user-images.githubusercontent.com/2679513/131189167-18ea5fe1-c578-47f6-9785-3748178e4312.png" width="150px"/><br/>
Speckle | Revit Dashboard
</h1>
<h3 align="center">
Speckle App to display Revit commits
</h3>
<p align="center"><b>Speckle</b> is data infrastructure for the AEC industry.</p><br/>
![Create your own app](./app-guide-main-img.jpg)
<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&amp;style=flat-square&amp;logo=discourse&amp;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&amp;logo=read-the-docs&amp;logoColor=white" alt="docs"></a></p>
<p align="center"><a href="https://circleci.com/gh/specklesystems/speckle-sharp"><img src="https://circleci.com/gh/specklesystems/speckle-sharp.svg?style=svg" alt=".NET Core"></a></p>
This repo contains all the code related to the [Create your own App guide](https://speckle.guide/dev/apps.html), which is part of our [Developer Docs](https://speckle.guide/dev)
# About Speckle
> You can find the published app [HERE](https://hardcore-einstein-829a53.netlify.app/)
>
> [![Netlify Status](https://api.netlify.com/api/v1/badges/45628834-7477-42f8-9e2a-e8da5b4d2de1/deploy-status)](https://app.netlify.com/sites/hardcore-einstein-829a53/deploys)
What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube/views/B9humiSpHzM?label=Speckle%20in%201%20minute%20video&style=social)
Currently, only _Part I_ of the guide has been released. You can find a `tag` with the name `part-i` in the repo, which points to the final code for the corresponding parts. Future releases will follow the same pattern.
### Features
For instructions on how to install dependencies and basic setup, please consult the guide.
- **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
- **Interoperability:** 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 got 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 and more!
## Environment variables
### Try Speckle now!
You'll find an `.env.local` file already populated with some values. **Please remember to modify these values with your own, following the guide linked above.**
Give Speckle a try in no time by:
## Project setup
- [![app.speckle.systems](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://app.speckle.systems) ⇒ creating an account on our public server
The setup scripts are just like any other `Vue.js` app:
### Resources
### Install packages
- [![Community forum users](https://img.shields.io/badge/community-forum-green?style=for-the-badge&logo=discourse&logoColor=white)](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/dev/) reference on almost any end-user and developer functionality
```
npm install
```
![Untitled](https://user-images.githubusercontent.com/2679513/132021739-15140299-624d-4410-98dc-b6ae6d9027ab.png)
### Compiles and hot-reloads for development
# Repo structure
```
npm run serve
```
### Compiles and minifies for production
### Other repos
```
npm run build
```
Make sure to also check and ⭐️ these other Speckle repositories:
- [`speckle-sharp`](https://github.com/specklesystems/speckle-sharp): .NET SDK, tooling, schema and Connectors
- [`speckle-server`](https://github.com/specklesystems/speckle-server): Server and Web packages
- [`specklepy`](https://github.com/specklesystems/specklepy): Python SDK 🐍
- [`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
- [`speckle-unreal`](https://github.com/specklesystems/speckle-unreal): Unreal Engine Connector
- [`speckle-qgis`](https://github.com/specklesystems/speckle-qgis): QGIS connectod
- [`speckle-powerbi`](https://github.com/specklesystems/speckle-powerbi): PowerBi connector
- and more [connectos & tooling](https://github.com/specklesystems/)!
## Developing and Debugging
This app uses Vue.js 2. In order to run it locally
### Security
For any security vulnerabilities or concerns, please contact us directly at security[at]speckle.systems.
### License
Unless otherwise described, the code in this repository is licensed under the Apache-2.0 License. Please note that some modules, extensions or code herein might be otherwise licensed. This is indicated either in the root of the containing folder under a different license file, or in the respective file's header. If you have any questions, don't hesitate to get in touch with us via [email](mailto:hello@speckle.systems).
Binary file not shown.

After

Width:  |  Height:  |  Size: 834 KiB

+3 -2
View File
@@ -1,5 +1,6 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
presets: ['@vue/cli-plugin-babel/preset'],
exclude: [
/(Speckle.js\.). /
]
}
+233 -100
View File
@@ -1,5 +1,5 @@
{
"name": "speckle-demo-app",
"name": "speckle-revit-dashboard-app",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
@@ -1201,18 +1201,18 @@
"dev": true
},
"@speckle/objectloader": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@speckle/objectloader/-/objectloader-2.0.0.tgz",
"integrity": "sha512-s1vH5gGzB5nJgrMFESaQqGJZuzVOAViKP1Xk9ZZV/zSP2b4vpkINMDC1WQvP9j2kqzCjQcQfkl+KqM6IuGvHgg=="
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@speckle/objectloader/-/objectloader-2.1.1.tgz",
"integrity": "sha512-EdMBYixK//8OrL/8aLvOkjNbEJab59FZyMETcPW9hgwo/M4a0pX6ao9LTrHyBMCGpjx5iyidk8SDpExebAtARw=="
},
"@speckle/viewer": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@speckle/viewer/-/viewer-2.0.1.tgz",
"integrity": "sha512-wZizgEf548vbhxOcuOq2E2lAbVYV7JaUhWI9f/u4tV4GDS9DMoDsZ+WFDUFyXtERZ8soaj65YL7vmvChS6vcug==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@speckle/viewer/-/viewer-2.1.1.tgz",
"integrity": "sha512-OQWLfVIb79AJJYEl90kk9Ra/WcmpmDaloTgbkaRQOC0jMNmhFSFfrsFvfNX5G3BdhuDGKpbSwDx2j81xJucKJQ==",
"requires": {
"camera-controls": "^1.26.0",
"camera-controls": "^1.28.0",
"lodash.debounce": "^4.0.8",
"three": "^0.124.0"
"three": "0.124.0"
}
},
"@types/anymatch": {
@@ -1231,6 +1231,14 @@
"@types/node": "*"
}
},
"@types/chart.js": {
"version": "2.9.34",
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.34.tgz",
"integrity": "sha512-CtZVk+kh1IN67dv+fB0CWmCLCRrDJgqOj15qPic2B1VCMovNO6B7Vhf/TgPpNscjhAL1j+qUntDMWb9A4ZmPTg==",
"requires": {
"moment": "^2.10.2"
}
},
"@types/connect": {
"version": "3.4.34",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
@@ -1744,16 +1752,6 @@
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"optional": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"cacache": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz",
@@ -1780,34 +1778,6 @@
"unique-filename": "^1.1.1"
}
},
"chalk": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"optional": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"optional": true
},
"fs-extra": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
@@ -1819,25 +1789,6 @@
"universalify": "^0.1.0"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"optional": true
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -1854,16 +1805,6 @@
"minipass": "^3.1.1"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"terser-webpack-plugin": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
@@ -1880,18 +1821,6 @@
"terser": "^4.6.12",
"webpack-sources": "^1.4.3"
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.2.0",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.2.0.tgz",
"integrity": "sha512-TitGhqSQ61RJljMmhIGvfWzJ2zk9m1Qug049Ugml6QP3t0e95o0XJjk29roNEiPKJQBEi8Ord5hFuSuELzSp8Q==",
"dev": true,
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
}
}
}
},
@@ -2229,6 +2158,11 @@
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true
},
"almost-equal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/almost-equal/-/almost-equal-1.1.0.tgz",
"integrity": "sha1-+FHGMROHV5lCdqou++jfowZszN0="
},
"alphanum-sort": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz",
@@ -3126,9 +3060,9 @@
"dev": true
},
"camera-controls": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-1.28.0.tgz",
"integrity": "sha512-V9HIwjcdD843RpToJjW2fUZfZUQaFIA2/Wx87+zq781/tJGzjE1g1PBeMTOV8oTGl8cskS5yV8AqtBn9+HlrUQ=="
"version": "1.33.1",
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-1.33.1.tgz",
"integrity": "sha512-+l0LgV+w0G4GAtwiSahHLj7rCqzVMSa+/BHE3+aE7vdku2B9QF8CXgDSlCH9cBt2TZeg+WQSkRPeIYwjvtjdPw=="
},
"caniuse-api": {
"version": "3.0.0",
@@ -3177,6 +3111,32 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true
},
"chart.js": {
"version": "2.9.4",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
"requires": {
"chartjs-color": "^2.1.0",
"moment": "^2.10.2"
}
},
"chartjs-color": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
"requires": {
"chartjs-color-string": "^0.6.0",
"color-convert": "^1.9.3"
}
},
"chartjs-color-string": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
"requires": {
"color-name": "^1.0.0"
}
},
"check-types": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz",
@@ -3262,6 +3222,11 @@
"safe-buffer": "^5.0.1"
}
},
"clamp": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz",
"integrity": "sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ="
},
"class-utils": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
@@ -3505,16 +3470,42 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-interpolate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/color-interpolate/-/color-interpolate-1.0.5.tgz",
"integrity": "sha512-EcWwYtBJdbeHyYq/y5QwVWLBUm4s7+8K37ycgO9OdY6YuAEa0ywAY+ItlAxE1UO5bXW4ugxNhStTV3rsTZ35ZA==",
"requires": {
"clamp": "^1.0.1",
"color-parse": "^1.2.0",
"color-space": "^1.14.3",
"lerp": "^1.0.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"color-parse": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/color-parse/-/color-parse-1.4.2.tgz",
"integrity": "sha512-RI7s49/8yqDj3fECFZjUI1Yi0z/Gq1py43oNJivAIIDSyJiOZLfYCRQEgn8HEVAj++PcRe8AnL2XF0fRJ3BTnA==",
"requires": {
"color-name": "^1.0.0"
}
},
"color-space": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/color-space/-/color-space-1.16.0.tgz",
"integrity": "sha512-A6WMiFzunQ8KEPFmj02OnnoUnqhmSaHaZ/0LVFcPTdlvm8+3aMJ5x1HRHy3bDHPkovkf4sS0f4wsVvwk71fKkg==",
"requires": {
"hsluv": "^0.0.3",
"mumath": "^3.3.4"
}
},
"color-string": {
"version": "1.5.5",
@@ -3532,6 +3523,21 @@
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
"dev": true
},
"colorjs.io": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.0.3.tgz",
"integrity": "sha512-JEqrQTV5eWOlDCgw+B3pPwaQuS7JG0il0SfMg2ucFt69Le5TtKJ/ENE2/NAsCBJYiEnACAEjQmf7VHedwlbMQw==",
"requires": {
"acorn": "^7.3.1"
},
"dependencies": {
"acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
}
}
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -5867,10 +5873,21 @@
"dev": true
},
"graphql-tag": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.11.0.tgz",
"integrity": "sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA==",
"dev": true
"version": "2.12.5",
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.5.tgz",
"integrity": "sha512-5xNhP4063d16Pz3HBtKprutsPrmHZi5IdUGOWRxA2B6VF7BIRGOHZ5WQvDmJXZuPcBg7rYwaFxvQYjqkSdR3TQ==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"gzip-size": {
"version": "5.1.1",
@@ -6074,6 +6091,11 @@
"integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=",
"dev": true
},
"hsluv": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/hsluv/-/hsluv-0.0.3.tgz",
"integrity": "sha1-gpEH2vtKn4tSoYCe0C4JHq3mdUw="
},
"html-entities": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz",
@@ -7190,6 +7212,11 @@
"launch-editor": "^2.2.1"
}
},
"lerp": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/lerp/-/lerp-1.0.3.tgz",
"integrity": "sha1-oYyJaPkXiW3hXM/MKNVaa3Med24="
},
"levn": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
@@ -7297,8 +7324,7 @@
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash._reinterpolate": {
"version": "3.0.0",
@@ -7354,6 +7380,11 @@
"lodash._reinterpolate": "^3.0.0"
}
},
"lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
},
"lodash.transform": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz",
@@ -7716,6 +7747,11 @@
"minimist": "^1.2.5"
}
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -7752,6 +7788,14 @@
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
"dev": true
},
"mumath": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/mumath/-/mumath-3.3.4.tgz",
"integrity": "sha1-SNSg8P2MrU57Mglu6JsWGmPTC78=",
"requires": {
"almost-equal": "^1.1.0"
}
},
"mute-stream": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
@@ -11408,6 +11452,14 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
},
"vue-chartjs": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-3.5.1.tgz",
"integrity": "sha512-foocQbJ7FtveICxb4EV5QuVpo6d8CmZFmAopBppDIGKY+esJV8IJgwmEW0RexQhxqXaL/E1xNURsgFFYyKzS/g==",
"requires": {
"@types/chart.js": "^2.7.55"
}
},
"vue-cli-plugin-vuetify": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.3.1.tgz",
@@ -11498,6 +11550,87 @@
}
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.8.1",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.1.tgz",
"integrity": "sha512-V53TJbHmzjBhCG5OYI2JWy/aYDspz4oVHKxS43Iy212GjGIG1T3EsB3+GWXFm/1z5VwjdjLmdZUFYM70y77vtQ==",
"dev": true,
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"optional": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"optional": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"optional": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"optional": true
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"vue-router": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.1.tgz",
+10 -4
View File
@@ -1,5 +1,5 @@
{
"name": "speckle-demo-app",
"name": "speckle-revit-dashboard-app",
"version": "0.1.0",
"private": true,
"scripts": {
@@ -8,12 +8,18 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@speckle/objectloader": "^2.0.0",
"@speckle/viewer": "^2.0.1",
"@speckle/objectloader": "^2.1.1",
"@speckle/viewer": "^2.1.1",
"chart.js": "^2.9.4",
"color-interpolate": "^1.0.5",
"colorjs.io": "^0.0.3",
"core-js": "^3.6.5",
"debounce": "^1.2.1",
"lodash": "^4.17.21",
"lodash.throttle": "^4.1.1",
"register-service-worker": "^1.7.1",
"vue": "^2.6.11",
"vue-chartjs": "^3.5.1",
"vue-router": "^3.2.0",
"vue-timeago": "^5.1.3",
"vuetify": "^2.4.11",
@@ -31,7 +37,7 @@
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"graphql-tag": "^2.11.0",
"graphql-tag": "^2.12.5",
"sass": "^1.32.0",
"sass-loader": "^10.0.0",
"vue-cli-plugin-vuetify": "~2.3.1",
+1
View File
@@ -0,0 +1 @@
/* /index.html 200
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+41
View File
@@ -0,0 +1,41 @@
{
"name": "App",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 215 B

+126 -19
View File
@@ -4,51 +4,158 @@
app
color="primary"
dark
dense
>
<div class="d-flex align-center">
<v-img
alt="Speckle Logo"
class="shrink mr-2"
contain
:src="require(`@/assets/img.png`)"
transition="scale-transition"
width="40"
height="24"
/>
<h3>Speckle Demo App</h3>
</div>
<v-btn text link to="/" small>
<v-img class="mr-2" src="@/assets/img.png" height="14" width="14"/>
<v-badge offset-y="7px" offset-x="0px" color="error" content="alpha">
<h3 class="text--white">Revit Dash</h3>
</v-badge>
</v-btn>
<v-spacer></v-spacer>
<div v-if="isAuthenticated" class="mr-1">
Welcome <b>{{ this.$store.state.user.name }}</b>!
<stream-search v-if="isAuthenticated" @selected="$router.push(`/streams/${$event.id}`)"/>
<v-spacer></v-spacer>
<div>
<v-tooltip bottom max-width="500" color="warning" v-if="alertAccepted">
<template v-slot:activator="{ on, attrs }">
<v-icon v-bind="attrs" v-on="on" class="mr-3" color="warning">mdi-alert-outline</v-icon>
</template>
<span>This app is still in <b>ALPHA</b> stage; meaning some things may not work as expected.</span>
</v-tooltip>
</div>
<div>
<v-tooltip left>
<template v-slot:activator="{ on, attrs }">
<v-btn x-small v-bind="attrs" v-on="on" icon link href="https://speckle.community" target="_blank" class="mr-3">
<v-icon size="x-large">mdi-help-circle-outline</v-icon>
</v-btn>
</template>
<span>Have any questions? <b>Join our Community!</b></span>
</v-tooltip>
</div>
<v-btn
class="ma-2"
small
outlined
v-if="!isAuthenticated"
@click="$store.dispatch('redirectToAuth')"
>
<span>Login with Speckle</span>
</v-btn>
<v-btn outlined v-else @click="$store.dispatch('logout')">
Log out
<v-img class="mr-2" src="@/assets/img.png" height="14" width="14"/>
<span>Login/Register</span>
</v-btn>
<v-menu v-else offset-y open-on-hover>
<template v-slot:activator="{ on, attrs }">
<v-avatar v-bind="attrs" v-on="on" size="32" color="grey lighten-3">
<v-img v-if="$store.state.user.avatar" :src="$store.state.user.avatar"/>
<v-img v-else :src="`https://robohash.org/${$store.user.id}.png?size=32x32`"/>
</v-avatar>
</template>
<v-list dense nav subheader id="login-menu">
<v-subheader class="caption">Logged in as:</v-subheader>
<p class="caption ml-3 mb-1">{{ $store.state.user.name }} <span
v-if="$store.state.user.email">({{ $store.state.user.email }})</span></p>
<v-divider class="ma-1"></v-divider>
<v-list-item link :href="`${serverUrl}/profile`" target="_blank">
<v-list-item-title>Go to profile</v-list-item-title>
<v-list-item-icon>
<v-icon small>mdi-account</v-icon>
</v-list-item-icon>
</v-list-item>
<v-list-item link href="https://speckle.systems/tutorials/revit-dash/" target="_blank">
<v-list-item-title>Feedback</v-list-item-title>
<v-list-item-icon>
<v-icon small>mdi-message-alert-outline</v-icon>
</v-list-item-icon>
</v-list-item>
<v-list-item link @click="$store.dispatch('logout')">
<v-list-item-title class="error--text">Log out</v-list-item-title>
<v-list-item-icon>
<v-icon small color="error">mdi-logout</v-icon>
</v-list-item-icon>
</v-list-item>
</v-list>
</v-menu>
</v-app-bar>
<div class="floating d-flex justify-end align-end">
<v-alert prominent v-if="alert" color="error" max-width="60%">
<v-row align="center">
<v-col class="grow white--text">
This app is still in <b>ALPHA</b> stage; meaning some things may not work as expected. You can provide feedback on our <a src="https://speckle.community" class="font-weight-bold white--text text-decoration-underline">Community Forum</a>
</v-col>
<v-col class="shrink">
<v-btn outlined color="white" @click="alertOk">OK</v-btn>
</v-col>
</v-row>
</v-alert>
</div>
<v-main>
<router-view/>
<transition name="fade">
<router-view/>
</transition>
</v-main>
</v-app>
</template>
<script>
import StreamSearch from "@/components/StreamSearch";
export default {
name: 'App',
components: {StreamSearch},
data() {
return {
serverUrl: process.env.VUE_APP_SERVER_URL,
alert: true,
alertAccepted: false
}
},
mounted() {
let seen = localStorage.getItem(process.env.VUE_APP_SPECKLE_NAME + ".alphaDisclaimerSeen")
if(seen == "true"){
this.alert = false
this.alertAccepted = true
}
},
computed: {
isAuthenticated() {
return this.$store.getters.isAuthenticated
},
},
methods: {
alertOk(){
console.log("alert ok'd")
localStorage.setItem(process.env.VUE_APP_SPECKLE_NAME + ".alphaDisclaimerSeen", true)
this.alert = false
this.alertAccepted = true
}
}
};
</script>
<style lang="scss">
$heading-font-family: 'Space Grotesk';
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */
{
opacity: 0;
}
.floating {
position: fixed;
top: 4em;
right: 2em;
z-index: 1;
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

+112
View File
@@ -0,0 +1,112 @@
<template>
<v-card :class="`my-1 mb-0 pa-0 pb-2 ${localExpand ? 'elevation-3' : 'elevation-0'} my-0`">
<v-card-title>
<v-chip @click="toggleLoadExpand">
<v-icon small class="mr-2">mdi-code-array</v-icon>
{{ keyName }}
<span class="caption ml-2">List ({{ value.length }} elements)</span>
<v-icon class="ml-2" small>
{{ localExpand ? 'mdi-minus' : 'mdi-plus' }}
</v-icon>
</v-chip>
</v-card-title>
<v-card-text v-if="localExpand" class="pb-0 pr-0 pl-3">
<component
:is="entry.type"
v-for="(entry, index) in rangeEntries"
:key="index"
:key-name="entry.key"
:value="entry.value"
:stream-id="streamId"
></component>
</v-card-text>
<v-card-text v-if="localExpand && currentLimit < value.length">
<v-btn small @click="loadMore">Show more</v-btn>
</v-card-text>
</v-card>
</template>
<script>
export default {
name: 'ObjectListViewer',
components: {
ObjectSpeckleViewer: () => import('./ObjectSpeckleViewer'),
ObjectSimpleViewer: () => import('./ObjectSimpleViewer'),
ObjectValueViewer: () => import('./ObjectValueViewer')
},
props: {
value: {
type: Array,
default: () => []
},
keyName: {
type: String,
default: null
},
streamId: {
type: String,
default: null
}
},
data() {
return {
localExpand: false,
itemsPerLoad: 5,
currentLimit: 5
}
},
computed: {
rangeEntries() {
let arr = []
let index = 0
for (let val of this.range) {
index++
if (Array.isArray(val)) {
arr.push({
key: `${index}`,
value: val,
type: 'ObjectListViewer'
})
} else if (typeof val === 'object' && val !== null) {
if (val.speckle_type && val.speckle_type === 'reference') {
arr.push({
key: `${index}`,
value: val,
type: 'ObjectSpeckleViewer'
})
} else {
arr.push({
key: `${index}`,
value: val,
type: 'ObjectSimpleViewer'
})
}
} else {
arr.push({
key: `${index}`,
value: val,
type: 'ObjectValueViewer'
})
}
}
arr.sort((a, b) => {
if (a.type === b.type) return 0
if (a.type === 'ObjectValueViewer') return -1
return 0
})
return arr
},
range() {
return this.value.slice(0, this.currentLimit)
}
},
methods: {
toggleLoadExpand() {
this.localExpand = !this.localExpand
},
loadMore() {
this.currentLimit += this.itemsPerLoad
}
}
}
</script>
+132
View File
@@ -0,0 +1,132 @@
<template>
<v-card elevation="0" :class="`my-1 pa-0 my-0`">
<v-card-title>
<v-chip color="" @click="toggleLoadExpand">
<v-icon class="mr-2" small>mdi-code-braces</v-icon>
{{ keyName }}
<span class="caption ml-2">
{{ value.speckle_type ? parsedType : 'Object' }}
</span>
<v-icon small class="ml-2">
{{ localExpand ? 'mdi-minus' : 'mdi-plus' }}
</v-icon>
</v-chip>
<v-btn
v-show="forceShowOpenInNew || value.referencedId"
icon
small
target="_blank"
:href="`${serverUrl}/streams/${streamId}/objects/${value.id || value.referencedId}`"
>
<v-icon small>mdi-open-in-new</v-icon>
</v-btn>
</v-card-title>
<v-card-text v-if="localExpand" class="pr-0 pl-3">
<component
:is="entry.type"
v-for="(entry, index) in objectEntries"
:key="index"
:key-name="entry.key"
:value="entry.value"
:stream-id="streamId"
></component>
</v-card-text>
</v-card>
</template>
<script>
export default {
name: 'ObjectSimpleViewer',
components: {
ObjectListViewer: () => import('./ObjectListViewer.vue'),
ObjectSpeckleViewer: () => import('./ObjectSpeckleViewer'),
ObjectValueViewer: () => import('./ObjectValueViewer')
},
props: {
value: {
type: Object,
default: () => {}
},
streamId: {
type: String,
default: null
},
keyName: {
type: String,
default: null
},
forceShowOpenInNew: {
type: Boolean,
default: false
},
forceExpand: {
type: Boolean,
default: false
}
},
data() {
return {
localExpand: this.forceExpand,
serverUrl: process.env.VUE_APP_SERVER_URL
}
},
computed: {
objectEntries() {
if (!this.value) return []
let entries = Object.entries(this.value)
let arr = []
for (let [key, val] of entries) {
if (key.startsWith('__')) continue
if (key[0] === '@') key = key.substring(1)
if (key === 'totalChildrenCount') key = 'total children count'
if (key === 'speckle_type') key = 'speckle type'
if (Array.isArray(val)) {
arr.push({
key,
value: val,
type: 'ObjectListViewer',
description: `List (${val.length} elements)`
})
// TODO -> list value template displayer
} else if (typeof val === 'object' && val !== null) {
if (val.speckle_type && val.speckle_type === 'reference') {
arr.push({
key,
value: val,
type: 'ObjectSpeckleViewer'
})
} else {
arr.push({
key,
value: val,
type: 'ObjectSimpleViewer'
})
}
} else {
arr.push({
key,
value: val,
type: 'ObjectValueViewer'
})
}
}
arr.sort((a, b) => {
if (a.type === b.type) return 0
if (a.type === 'ObjectValueViewer') return -1
return 0
})
return arr
},
parsedType() {
if (!this.value.speckle_type) return 'Object'
let sections = this.value.speckle_type.split(':').map((s) => s.split('.').reverse()[0])
return sections.join('/')
}
},
methods: {
toggleLoadExpand() {
this.localExpand = !this.localExpand
}
}
}
</script>
+157
View File
@@ -0,0 +1,157 @@
<template>
<v-card :class="`my-1 pa-0 ${localExpand ? 'elevation-3' : 'elevation-0'} my-0`">
<v-card-title v-if="object">
<v-chip color="" @click="toggleLoadExpand">
<v-icon small class="mr-2">mdi-code-braces</v-icon>
{{ keyName }}
<span class="caption ml-2">
{{ object.data.speckle_type ? object.data.speckle_type : 'Referenced Object' }}
</span>
<v-icon small class="ml-2">
{{ localExpand ? 'mdi-minus' : 'mdi-plus' }}
</v-icon>
</v-chip>
<v-btn
icon
small
:href="`${serverUrl}/${streamId}/objects/${value.referencedId}`"
target="_blank"
>
<v-icon small>mdi-open-in-new</v-icon>
</v-btn>
</v-card-title>
<v-card-title v-if="!object">
<v-chip color="" @click="toggleLoadExpand">
<v-icon small class="mr-2">mdi-code-braces</v-icon>
{{ keyName }}
<span class="caption ml-2">Referenced Object</span>
<v-icon small class="ml-2">
{{ localExpand ? 'mdi-minus' : 'mdi-plus' }}
</v-icon>
</v-chip>
<v-btn
icon
small
target="_blank"
:href="`${serverUrl}/streams/${streamId}/objects/${value.referencedId}`"
>
<v-icon small>mdi-open-in-new</v-icon>
</v-btn>
</v-card-title>
<v-card-text v-if="localExpand" class="pr-0 pl-3">
<v-skeleton-loader
v-if="loading"
type="list-item-three-line, list-item-three-line"
></v-skeleton-loader>
<component
:is="entry.type"
v-for="(entry, index) in objectEntries"
:key="index"
:key-name="entry.key"
:value="entry.value"
:stream-id="streamId"
></component>
</v-card-text>
</v-card>
</template>
<script>
import {getObject} from "@/speckleUtils";
export default {
name: 'ObjectSpeckleViewer',
components: {
ObjectListViewer: () => import('./ObjectListViewer'),
ObjectSimpleViewer: () => import('./ObjectSimpleViewer'),
ObjectValueViewer: () => import('./ObjectValueViewer')
},
props: {
expand: {
type: Boolean,
default: false
},
value: {
type: Object,
default: null
},
keyName: {
type: String,
default: null
},
streamId: {
type: String,
default: null
}
},
data() {
return {
localExpand: false,
loading: true,
object: null,
serverUrl: process.env.VUE_APP_SERVER_URL
}
},
computed: {
objectEntries() {
if (!this.object) return []
let entries = Object.entries(this.object.data)
let arr = []
for (let [key, val] of entries) {
if (key.startsWith('__')) continue
if (key[0] === '@') key = key.substring(1)
if (key === 'totalChildrenCount') key = 'total children count'
if (key === 'speckle_type') key = 'speckle type'
if (Array.isArray(val)) {
arr.push({
key,
value: val,
type: 'ObjectListViewer',
description: `List (${val.length} elements)`
})
} else if (typeof val === 'object' && val !== null) {
if (val.speckle_type && val.speckle_type === 'reference') {
arr.push({
key,
value: val,
type: 'ObjectSpeckleViewer'
})
} else {
arr.push({
key,
value: val,
type: 'ObjectSimpleViewer'
})
}
} else {
arr.push({
key,
value: val,
type: 'ObjectValueViewer'
})
}
}
arr.sort((a, b) => {
if (a.type === b.type) return 0
if (a.type === 'ObjectValueViewer') return -1
return 0
})
return arr
}
},
async mounted() {
this.loading = true
this.localExpand = this.expand
if(!this.localExpand){
var res = await getObject(this.streamId,this.value.referencedId)
delete res.data.stream.object.data.__closure
this.object = res.data.stream.object
}
this.loading = false
},
methods: {
toggleLoadExpand() {
this.localExpand = !this.localExpand
}
}
}
</script>
+26
View File
@@ -0,0 +1,26 @@
<template>
<div class="ml-4 my-2">
<b>{{ keyName }}</b>
<code class="ml-4">{{ value }}</code>
</div>
</template>
<script>
export default {
name: "ObjectsValueViewer",
components: {},
props: {
value: {
type: [Number, String, Boolean],
default: null
},
streamId: {
type: String,
default: null
},
keyName: {
type: String,
default: null
}
}
}
</script>
+434
View File
@@ -0,0 +1,434 @@
<template lang="html">
<v-sheet style="height: 100%; position: relative" class="transparent">
<v-alert
v-show="showAlert"
text
type="warning"
dismissible
dense
style="position: absolute; z-index: 20; width: 100%"
class="caption"
>
{{ alertMessage }}
</v-alert>
<div
id="rendererparent"
ref="rendererparent"
:class="`${fullScreen ? 'fullscreen' : ''} ${darkMode ? 'dark' : ''}`"
>
<v-fade-transition>
<div v-show="!hasLoadedModel" class="overlay cover-all">
<transition name="fade">
<div v-show="hasImg" ref="cover" class="overlay-abs bg-img"></div>
</transition>
<div class="overlay-abs radial-bg"></div>
<div class="overlay-abs" style="pointer-events: none">
<v-btn
color="primary"
class="vertical-center"
style="pointer-events: all"
small
@click="load()"
>
<v-icon dense>mdi-play</v-icon>
</v-btn>
</div>
</div>
</v-fade-transition>
<v-progress-linear
v-if="hasLoadedModel && loadProgress < 99"
v-model="loadProgress"
height="4"
rounded
class="vertical-center elevation-10"
style="position: absolute; width: 80%; left: 10%; opacity: 0.5"
></v-progress-linear>
<v-card
elevation="0"
v-show="hasLoadedModel && loadProgress >= 99"
style="position: absolute; bottom: 0; z-index: 2; width: 100%"
class="pa-0 text-center transparent elevation-0 pb-3"
>
<v-btn-toggle class="elevation-0" style="z-index: 100">
<v-btn
:disabled="selectedObjects.length === 0"
v-if="(showSelectionHelper || fullScreen)"
small
color="primary"
@click="showObjectDetails = !showObjectDetails"
>
<span v-if="!isSmall">Selection Details</span>
<v-icon v-else small>mdi-cube</v-icon>
({{ selectedObjects.length }})
</v-btn>
<v-menu top close-on-click offset-y style="z-index: 100">
<template #activator="{ on: onMenu, attrs: menuAttrs }">
<v-tooltip top>
<template #activator="{ on: onTooltip, attrs: tooltipAttrs }">
<v-btn
small
v-bind="{ ...tooltipAttrs, ...menuAttrs }"
v-on="{ ...onTooltip, ...onMenu }"
>
<v-icon small>mdi-camera</v-icon>
</v-btn>
</template>
Select view
</v-tooltip>
</template>
<v-list dense>
<v-list-item @click="setView('top')">
<v-list-item-title>Top</v-list-item-title>
</v-list-item>
<v-list-item @click="setView('front')">
<v-list-item-title>Front</v-list-item-title>
</v-list-item>
<v-list-item @click="setView('back')">
<v-list-item-title>Back</v-list-item-title>
</v-list-item>
<v-list-item @click="setView('left')">
<v-list-item-title>Left</v-list-item-title>
</v-list-item>
<v-list-item @click="setView('right')">
<v-list-item-title>Right</v-list-item-title>
</v-list-item>
<v-divider v-if="namedViews.length !== 0"></v-divider>
<v-list-item v-for="view in namedViews" :key="view.id" @click="setNamedView(view.id)">
<v-list-item-title>{{ view.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-tooltip top>
<template #activator="{ on, attrs }">
<v-btn v-bind="attrs" small v-on="on" @click="zoomEx()">
<v-icon small>mdi-cube-scan</v-icon>
</v-btn>
</template>
Focus entire model
</v-tooltip>
<v-tooltip top>
<template #activator="{ on, attrs }">
<v-btn v-bind="attrs" small @click="sectionToggle()" v-on="on">
<v-icon small>mdi-scissors-cutting</v-icon>
</v-btn>
</template>
Show / Hide Section plane
</v-tooltip>
<v-tooltip top>
<template #activator="{ on, attrs }">
<v-btn small v-bind="attrs" @click="fullScreen = !fullScreen" v-on="on">
<v-icon small>{{ fullScreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen' }}</v-icon>
</v-btn>
</template>
Full screen
</v-tooltip>
<v-tooltip top>
<template #activator="{ on, attrs }">
<v-btn v-bind="attrs" small @click="showHelp = !showHelp" v-on="on">
<v-icon small>mdi-help</v-icon>
</v-btn>
</template>
Show viewer help
</v-tooltip>
<v-dialog
v-model="showObjectDetails"
width="500"
:fullscreen="$vuetify.breakpoint.smAndDown"
>
<v-card>
<v-toolbar elevation="0">
<v-toolbar-title>Selection Details</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon @click="showObjectDetails = false"><v-icon>mdi-close</v-icon></v-btn>
</v-toolbar>
<v-sheet>
<div v-if="selectedObjects.length !== 0">
<object-simple-viewer
v-for="(obj, ind) in selectedObjects"
:key="obj.id + ind"
:value="obj"
:stream-id="$route.params.id"
:key-name="`Selected Object ${ind + 1}`"
force-expand
/>
</div>
</v-sheet>
</v-card>
</v-dialog>
<v-dialog v-model="showHelp" max-width="290">
<v-card>
<v-card-text class="pt-7">
<v-icon class="mr-2">mdi-rotate-orbit</v-icon>
Use your
<b>left mouse button</b>
to rotate the view.
<br />
<br />
<v-icon class="mr-2">mdi-pan</v-icon>
Use your
<b>right mouse button</b>
to pan the view.
<br />
<br />
<v-icon class="mr-2">mdi-cursor-default-click</v-icon>
<b>Double clicking an object</b>
focus it in the camera view.
<br />
<br />
<v-icon class="mr-2">mdi-cursor-default-click-outline</v-icon>
<b>Double clicking on the background</b>
will focus again the entire scene.
</v-card-text>
</v-card>
</v-dialog>
</v-btn-toggle>
</v-card>
</div>
</v-sheet>
</template>
<script>
import throttle from 'lodash.throttle'
import {Viewer} from '@speckle/viewer'
import {TOKEN} from "@/speckleUtils";
import ObjectSimpleViewer from "@/components/ObjectSimpleViewer";
export default {
components: {ObjectSimpleViewer},
props: {
autoLoad: {
type: Boolean,
default: false
},
objectUrls: {
type: Array,
default: null
},
unloadTrigger: {
type: Number,
default: 0
},
showSelectionHelper: {
type: Boolean,
default: false
},
embeded: {
type: Boolean,
default: false
}
},
data() {
return {
hasLoadedModel: false,
loadProgress: 0,
fullScreen: false,
showHelp: false,
alertMessage: null,
showAlert: false,
selectedObjects: [],
showObjectDetails: false,
hasImg: false,
namedViews: []
}
},
computed: {
isSmall() {
return this.$vuetify.breakpoint.name === 'xs' || this.$vuetify.breakpoint.name === 'sm'
},
darkMode() {
return this.$vuetify.theme.dark
}
},
watch: {
unloadTrigger() {
this.unloadData()
},
fullScreen() {
setTimeout(() => window.__viewer.onWindowResize(), 20)
},
loadProgress(newVal) {
if (newVal >= 99) {
let views = window.__viewer.interactions.getViews()
this.namedViews.push(...views)
}
},
objectUrls(){
this.unloadData()
this.load()
}
},
// TODO: pause rendering on destroy, reinit on mounted.
async mounted() {
// NOTE: we're doing some globals and dom shennanigans in here for the purpose
// of having a unique global renderer and it's container dom element. The principles
// are simple enough:
// - create a single 'renderer' container div
// - initialise the actual renderer **once** (per app lifecycle, on refresh it's fine)
// - juggle the container div out of this component's dom when the component is managed out by vue
// - juggle the container div back in of this component's dom when it's back.
let renderDomElement = document.getElementById('renderer')
if (!renderDomElement) {
renderDomElement = document.createElement('div')
renderDomElement.id = 'renderer'
}
this.domElement = renderDomElement
this.$refs.rendererparent.appendChild(renderDomElement)
if (!window.__viewer) {
window.__viewer = new Viewer({ container: renderDomElement })
}
window.__viewer.onWindowResize()
this.setupEvents()
},
beforeDestroy() {
// NOTE: here's where we juggle the container div out, and do cleanup on the
// viewer end.
// hide renderer dom element.
this.domElement.style.display = 'none'
// move renderer dom element outside this component so it doesn't get deleted.
document.body.appendChild(this.domElement)
},
methods: {
zoomEx() {
window.__viewer.interactions.zoomExtents()
},
setView(view) {
window.__viewer.interactions.rotateTo(view)
},
setNamedView(id) {
window.__viewer.interactions.setView(id)
},
sectionToggle() {
window.__viewer.interactions.toggleSectionBox()
},
setupEvents() {
window.__viewer.on('load-warning', ({ message }) => {
this.alertMessage = message
this.showAlert = true
})
window.__viewer.on(
'load-progress',
throttle(
function (args) {
this.loadProgress = args.progress * 100
this.zoomEx()
}.bind(this),
200
)
)
window.__viewer.on('select', (objects) => {
this.selectedObjects.splice(0, this.selectedObjects.length)
this.selectedObjects.push(...objects)
this.$emit('selection', this.selectedObjects)
})
},
load() {
if (!this.objectUrls || this.objectUrls.length === 0) return
window.__viewer.onWindowResize()
this.objectUrls?.forEach(url => {
window.__viewer.loadObject(url, localStorage.getItem(TOKEN))
window.__viewerLastLoadedUrl = url
})
this.setupEvents()
this.hasLoadedModel = true
},
unloadData() {
window.__viewer.sceneManager.removeAllObjects()
this.hasLoadedModel = false
this.loadProgress = 0
this.namedViews.splice(0, this.namedViews.length)
}
}
}
</script>
<style>
#rendererparent {
position: relative;
display: inline-block;
width: 100%;
height: 100%;
}
.fullscreen {
position: fixed !important;
top: 0;
left: 0;
z-index: 10;
/*background-color: rgb(58, 59, 60);*/
background-color: rgb(238, 238, 238);
}
.dark {
background-color: rgb(58, 59, 60) !important;
}
#renderer {
position: absolute;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.overlay {
position: relative;
z-index: 2;
text-align: center;
}
.overlay-abs {
position: absolute;
z-index: 2;
text-align: center;
width: 100%;
height: 100%;
}
.bg-img {
background-position: center;
background-repeat: no-repeat;
/*background-attachment: fixed;*/
}
.cover-all {
position: relative;
width: 100%;
height: 100%;
text-align: center;
}
.radial-bg {
transition: all 0.5s ease-out;
background: radial-gradient(
circle,
rgba(60, 94, 128, 0.8519782913165266) 0%,
rgba(63, 123, 135, 0.13489145658263302) 100%
);
opacity: 1;
}
.radial-bg:hover {
background: radial-gradient(
circle,
rgba(60, 94, 128, 0.8519782913165266) 0%,
rgba(63, 123, 135, 0.13489145658263302) 100%
);
opacity: 0.5;
}
.vertical-center {
margin: 0;
top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
z-index: 2;
}
</style>
+358
View File
@@ -0,0 +1,358 @@
<template>
<div>
<revit-project-info :info="info" :stream="stream" :totals="totals"/>
<v-row>
<v-col cols="12">
<v-card max-height="500px" outlined>
<v-card-title>Objects by category</v-card-title>
<v-card-text>
<doughnut-chart ref="doughnut" v-if="objsByCategoryData" :chart-data="objsByCategoryData"
:options="doughnutOptions"></doughnut-chart>
</v-card-text>
</v-card>
</v-col>
<v-col lg="6" sm="12" xs="12">
<v-card max-height="500px" min-height="500px" outlined>
<v-card-title>Elements Per Level/Category</v-card-title>
<v-card-text>
<horizontal-barchart v-if="objsByLevelData" :chart-data="objsByLevelData"
:options="options"></horizontal-barchart>
</v-card-text>
</v-card>
</v-col>
<v-col lg="6" sm="12" xs="12">
<v-card max-height="500px" min-height="500px" outlined>
<v-card-title>Families<v-badge inline :content="totals.families"></v-badge> and Types <v-badge inline :content="totals.types"></v-badge></v-card-title>
<v-card-subtitle>
<v-text-field dense clearable prepend-icon="mdi-filter" placeholder="Filter all family types"
v-model="typeFilter"></v-text-field>
</v-card-subtitle>
<v-card-text>
<v-treeview dense :items="familyTypeTree" :search="typeFilter"></v-treeview>
</v-card-text>
</v-card>
</v-col>
</v-row>
</div>
</template>
<script>
import ObjectLoader from '@speckle/objectloader'
import HorizontalBarchart from "@/components/charts/HorizontalBarchart";
import interpolate from "color-interpolate";
import DoughnutChart from "@/components/charts/DoughnutChart";
import RevitProjectInfo from "@/components/RevitProjectInfo";
import {TOKEN} from "@/speckleUtils";
function debounce(callback, wait) {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => {
callback(...args);
}, wait);
};
}
export default {
name: "RevitDashboard",
components: {RevitProjectInfo, DoughnutChart, HorizontalBarchart},
props: ["streamId", "objectId", "revitData", "info", "stream"],
data() {
return {
familyTypeTree: [],
typeFilter: "",
loader: null,
objsPerLevel: null,
colorRange: interpolate(["#047EFB", "#4caf50"]),
availableLevels: {},
availableFamTypes: null,
totals: {
levels: 0,
elements: 0,
views: 0,
families: 0,
types: 0
},
options: {
responsive: true,
maintainAspectRatio: false,
legend: {
display: false
},
scales: {
xAxes: [{
stacked: true,
categoryPercentage: 0.5,
barPercentage: 1
}],
yAxes: [{
stacked: true
}]
}
},
doughnutOptions: {
responsive: true,
maintainAspectRatio: false,
legend: {
position: 'left',
onClick: (e, legendItem) => {
this.$emit("legend-clicked", e, legendItem)
}
},
pieceLabel: {
mode: 'value',
precision: 0,
fontSize: 18,
fontColor: '#fff',
fontStyle: 'bold',
}
},
}
},
computed: {
objsByLevelData() {
// Fast exit if no object has been set yet
if (!this.objsPerLevel) return null
var labels = []
var dataSets = []
var count = 0
// Loop through categories
for (const [key, value] of Object.entries(this.objsPerLevel)) {
// Create empty data set
let dataSet = {
label: key,
backgroundColor: this.colorRange(count / Object.keys(this.objsPerLevel).length),
data: []
}
count++
// Loop through levels on each category
for (const [lvlKey, lvlValue] of Object.entries(value)) {
// Append level name to labels if it hasn't already
if (labels.indexOf(lvlKey) === -1) labels.push(lvlKey)
// Push category count per level to dataset data
dataSet.data.push(Object.keys(lvlValue).length)
}
// Add dataset to list
dataSets.push(dataSet)
}
return {
labels,
datasets: dataSets,
}
},
objsByCategoryData() {
// Fast exit if no object has been set yet
if (!this.objsPerLevel) return null
var labels = []
var count = 0
let dataSet = {
backgroundColor: [],
data: []
}
// Loop through categories
for (const [key, value] of Object.entries(this.objsPerLevel)) {
// Loop through levels on each category
labels.push(key)
dataSet.backgroundColor.push(this.colorRange(count / Object.keys(this.objsPerLevel).length))
var total = 0
for (const [_, lvlValue] of Object.entries(value)) {
// Push category count per level to dataset data
total += Object.keys(lvlValue).length
}
dataSet.data.push(total)
count++
}
return {
labels,
datasets: [dataSet],
}
}
},
async mounted() {
this.processStreamObjects()
},
watch: {
streamId: {
handler: function (val, oldVal) {
this.processStreamObjects()
}
},
objectId: {
handler: function (val, oldVal) {
this.processStreamObjects()
}
},
availableFamTypes: {
handler: function(val, oldVal) {
this.familyTypeTree = this.famTypeTree();
}
}
},
methods: {
famTypeTree() {
var totalFams = 0
var totalTypes = 0
if (!this.availableFamTypes) return []
var id = 0;
var items = []
// Iterate through the categories
Object.entries(this.availableFamTypes).forEach(([key, val]) => {
var children = []
// Iterate through the families
Object.entries(val).forEach(([childKey, childVal]) => {
var grandChilds = []
// Iterate through available types
Object.entries(childVal).forEach(([grandKey, grandVal]) => {
grandChilds.push({
id: id++,
name: grandKey
})
totalTypes++
})
children.push({
id: id++,
name: childKey,
children: grandChilds
})
totalFams++
})
items.push({
id: id++,
name: key,
children: children
})
})
this.totals.families = totalFams
this.totals.types = totalTypes
return items
},
async processStreamObjects() {
this.$emit("loaded", false)
this.$emit("progress", 0)
this.loader = new ObjectLoader({
serverUrl: process.env.VUE_APP_SERVER_URL,
streamId: this.stream.id,
objectId: this.objectId,
token: localStorage.getItem(TOKEN)
})
function shouldIgnore(obj) {
return obj.speckle_type.startsWith("Objects.Geometry") ||
obj.speckle_type.endsWith("DataChunk") ||
obj.speckle_type.endsWith("GridLine") ||
obj.speckle_type.endsWith("ElementType") ||
obj.speckle_type === "Base" ||
obj.speckle_type.endsWith("ProjectInfo")
}
// Initialize placeholders
const typeCategoryMap = {}
const objectsPerLevel = {}
const availableCategoriesAndTypes = {}
const availableLevels = {}
var totalViews = 0
var totalElements = 0
var total = 0
var count = 0
var d = debounce(() => {
}, 10)
for await (let obj of this.loader.getObjectIterator()) {
if (!total) total = obj.totalChildrenCount
var progress = Math.floor((count * 100) / total)
this.$emit("progress", progress)
count++
// Get all types in the document
if (obj.speckle_type.endsWith("ElementType")) {
typeCategoryMap[obj.type] = obj.category // Map type to category
// Ensure structure exists
if (!availableCategoriesAndTypes[obj.category])
availableCategoriesAndTypes[obj.category] = {}
if (!availableCategoriesAndTypes[obj.category][obj.family])
availableCategoriesAndTypes[obj.category][obj.family] = {}
// Assign
availableCategoriesAndTypes[obj.category][obj.family][obj.type] = obj
continue
}
// Get all views in the document
if (obj.speckle_type === "Objects.BuiltElements.View" || obj.speckle_type === "Objects.BuiltElements.View:Objects.BuiltElements.View3D") {
totalViews++
continue
}
// Should we ignore this object?
if (shouldIgnore(obj)) continue
// Increase element count
totalElements++
var rvtType = obj.type || "Other"
var lvl = obj.level?.name || "No level"
// If object has level, cache it.
if (lvl !== "No level" && !availableLevels[lvl]) {
availableLevels[lvl] = obj.level
}
// Make sure nested structure exists
if (!objectsPerLevel[rvtType])
objectsPerLevel[rvtType] = {}
if (!objectsPerLevel[rvtType][lvl])
objectsPerLevel[rvtType][lvl] = {}
// Assign obj
objectsPerLevel[rvtType][lvl][obj.elementId] = obj
}
// Load has finished, post-process data
// Organize categories per level
var catsPerLevel = {}
Object.keys(objectsPerLevel).forEach(fam => {
var value = objectsPerLevel[fam]
var cat = typeCategoryMap[fam]
if (!catsPerLevel[cat]) catsPerLevel[cat] = {}
Object.keys(value).forEach(levelKey => {
if (!catsPerLevel[cat][levelKey]) catsPerLevel[cat][levelKey] = {}
catsPerLevel[cat][levelKey] = {...catsPerLevel[cat][levelKey], ...objectsPerLevel[fam][levelKey]}
})
})
this.objsPerLevel = catsPerLevel
this.availableFamTypes = availableCategoriesAndTypes
this.availableLevels = availableLevels
this.totals.levels = Object.keys(this.availableLevels).length
this.totals.views = totalViews
this.totals.elements = totalElements
this.$emit("loaded", true)
}
}
}
</script>
<style scoped>
.scroll-box {
overflow: scroll;
padding: 1em;
}
.v-card {
display: flex !important;
flex-direction: column;
}
.v-card__text {
flex-grow: 1;
overflow: auto;
}
</style>
+88
View File
@@ -0,0 +1,88 @@
<template>
<v-row>
<v-col lg="6" sm="12" xs="12">
<v-card outlined class="d-flex flex-column fill-height">
<span class="d-flex justify-center grey--text body-2 mb-0 mt-1">Stream details</span>
<v-card-title>
{{ stream.name }}
<v-btn outlined color="primary" x-small class="ml-3" :href="serverUrl+'/streams/'+stream.id" target="_blank">View in Speckle.xyz</v-btn>
</v-card-title>
<v-card-subtitle>
You are viewing the latest commit on this stream
</v-card-subtitle>
<v-card-text style="min-height: 180px; height:100%">
<renderer show-selection-helper :object-urls="objectUrl"/>
</v-card-text>
</v-card>
</v-col>
<v-col lg="6" sm="12" xs="12" class="d-flex flex-column">
<v-card v-if="projectInfo" outlined class="d-flex flex-column fill-height">
<span class="d-flex justify-center grey--text body-2 mb-0 mt-1">Revit project overview</span>
<v-card-title>{{ projectInfo.name }} {{ projectInfo.number }}<span
class="d-flex align-center text-body-2 grey--text border pl-2">
<v-icon size="medium" color="grey"
class="pr-1">mdi-home-circle-outline</v-icon>{{ projectInfo.address }}</span>
</v-card-title>
<v-card-subtitle class="d-flex align-center">
<v-icon size="small" class="pr-1">mdi-account-circle-outline</v-icon>
{{ projectInfo.author }}
<v-icon size="small" class="pr-1 pl-2">mdi-calendar-check-outline</v-icon>
{{ projectInfo.status }}
</v-card-subtitle>
<v-row wrap dense class="d-flex align-center justify-center ma-3">
<v-col v-for="(item, key) in totals" :key="key" class="col-4 d-flex flex-column justify-center align-center flex-fill">
<div class="d-flex flex-column justify-center align-center flex-fill">
<p class="text-md-h2 mb-0 primary--text">{{ item }}</p>
<p class="pb-0 ma-0 primary--text caption">{{ key.toUpperCase() }}</p>
</div>
</v-col>
</v-row>
</v-card>
<v-card v-else outlined class="d-flex justify-center align-center" height="100%">
<v-card-subtitle>No project info was sent on this commit</v-card-subtitle>
</v-card>
</v-col>
</v-row>
</template>
<script>
import {getStreamObject} from "@/speckleUtils";
import Renderer from "@/components/Renderer";
export default {
name: "RevitProjectInfo",
components: {Renderer},
props: ["info", "stream", "totals"],
data() {
return {
projectInfo: null,
serverUrl: process.env.VUE_APP_SERVER_URL,
}
},
computed: {
objectUrl() {
return [`${this.serverUrl}/streams/${this.stream.id}/objects/${this.stream.commits.items[0].referencedObject}`]
}
},
watch: {
info: {
deep: true,
immediate: true,
handler: async function (val, oldVal) {
if (!val) return
var id = this.$route.params.id
var res = await getStreamObject(id, this.info[0].referencedId)
this.projectInfo = res
}
}
}
}
</script>
<style scoped>
.renderer-parent {
height: 200px;
width: 400px;
background-color: antiquewhite;
}
</style>
+5 -7
View File
@@ -25,10 +25,10 @@
<v-row class="pa-0 ma-0">
{{ item.name }}
<v-spacer></v-spacer>
<span class="streamid">{{ item.id }}</span>
<span class="primary rounded white--text pl-1 pr-1 caption">{{ item.id }}</span>
</v-row>
</v-list-item-title>
<v-list-item-subtitle class="caption">
<v-list-item-subtitle class="caption primary--text">
Updated
<timeago :datetime="item.updatedAt"></timeago>
</v-list-item-subtitle>
@@ -57,12 +57,10 @@ export default {
}
},
methods: {
fetchSearchResults(e) {
async fetchSearchResults(e) {
if (!e || e?.length < 3) return
return searchStreams(e)
.then(json => {
this.streams = json.data.streams
})
var json = await searchStreams(e)
this.streams = json.data.streams
},
debounceInput: debounce(function (e) {
this.fetchSearchResults(e)
-15
View File
@@ -1,15 +0,0 @@
<template lang="html">
<v-container fill-height class="home flex-column justify-center align-center primary--text">
<h1>Welcome to the Speckle Demo App!</h1>
<h3>This app part of our developer guide on how to create your very own Speckle App.</h3>
<v-alert type="info" text color="primary" class="my-5 dark">
Check out <a href="https://speckle.guide/dev/apps" target="_blank">the tutorial</a>!
</v-alert>
<p>Please log in to access you Speckle data.</p>
</v-container>
</template>
<script>
export default {
name: 'WelcomeView'
}
</script>
+19
View File
@@ -0,0 +1,19 @@
import { Doughnut, mixins } from 'vue-chartjs'
import Chart from "chart.js"
const { reactiveProp } = mixins
export default {
extends: Doughnut,
mixins: [reactiveProp],
props: ['options'],
data(){
return {
oldClick: Chart.defaults.global.legend.onClick
}
},
mounted () {
// this.chartData is created in the mixin.
// If you want to pass options please create a local options object
this.renderChart(this.chartData, this.options)
}
}
@@ -0,0 +1,13 @@
import { HorizontalBar, mixins } from 'vue-chartjs'
const { reactiveProp } = mixins
export default {
extends: HorizontalBar,
mixins: [reactiveProp],
props: ['options'],
mounted () {
// this.chartData is created in the mixin.
// If you want to pass options please create a local options object
this.renderChart(this.chartData, this.options)
}
}
+8
View File
@@ -0,0 +1,8 @@
query {
user {
avatar
id
name
email
}
}
+57 -15
View File
@@ -2,6 +2,8 @@ import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import store from '../store/index.js'
import WelcomeView from "@/views/WelcomeView";
import RevitStream from "@/views/RevitStream";
Vue.use(VueRouter)
@@ -9,7 +11,40 @@ const routes = [
{
path: '/',
name: 'Home',
component: Home
component: Home,
meta: {
requiresAuth: true,
title: 'Speckle Revit Dashboard',
metaTags: [
{
name: 'description',
content: 'The speckle Revit Dashboard homepage'
},
{
property: 'og:description',
content: 'The speckle Revit Dashboard homepage'
}
]
}
},
{
path: '/login',
name: 'Login',
component: WelcomeView,
meta: {
requiresNoAuth: true,
title: "Login | Speckle Revit Dashboard"
}
},
{
path: '/streams/:id',
name: 'Streams',
component: RevitStream,
meta: {
requiresAuth: true,
title: "Stream | Speckle Revit Dashboard"
}
}
]
@@ -19,22 +54,29 @@ const router = new VueRouter({
routes
})
router.beforeEach( async (to, from, next) => {
if(to.query.access_code){
// If the route contains an access code, exchange it and go home.
store.dispatch('exchangeAccessCode', to.query.access_code)
.then(() => next("/"))
.catch(err => {
console.warn("exchange failed", err);
next("/")
})
router.beforeEach(async (to, from, next) => {
if(to.meta.title){
document.title = to.meta.title
}
else {
// Check on every route change if you still have access.
store.dispatch("getUser")
.then(to => next(to))
.catch(err => next("/"))
if (to.query.access_code) {
// If the route contains an access code, exchange it
try {
await store.dispatch('exchangeAccessCode', to.query.access_code)
} catch (err) {
console.warn("exchange failed", err);
}
// Whatever happens, go home.
return next("/")
}
// Fetch if user is authenticated
await store.dispatch("getUser")
var isAuth = store.getters.isAuthenticated
if (to.meta.requiresAuth && !isAuth)
return next({name: "Login"})
else if (to.meta.requiresNoAuth && isAuth)
return next("/")
// Any other page
next()
})
export default router
+68 -32
View File
@@ -1,39 +1,75 @@
export const userInfoQuery = () => `query {
user {
name
},
serverInfo {
name
company
}
}`
export const streamCommitsQuery = (streamId, itemsPerPage, cursor) => `query {
stream(id: "${streamId}"){
commits(limit: ${itemsPerPage}, cursor: ${cursor ? '"' + cursor + '"' : null}) {
totalCount
cursor
items{
id
message
branchName
sourceApplication
referencedObject
authorName
createdAt
}
}
}
}`
export const streamSearchQuery = (search) => `query {
streams(query: "${search}") {
export const userInfoQuery = `
query {
user {
name
id
avatar
email
},
serverInfo {
name
company
}
}`
export const streamCommitsQuery = `
query($id: String!, $limit: Int, $cursor: String) {
stream(id: $id){
name
updatedAt
id
commits(limit: $limit, cursor: $cursor) {
totalCount
cursor
items {
items{
id
name
updatedAt
message
branchName
sourceApplication
referencedObject
authorName
createdAt
}
}
}`
}
}`
export const streamSearchQuery = `
query($searchText: String!) {
streams(query: $searchText) {
totalCount
cursor
items {
id
name
updatedAt
}
}
}`
export const streamObjectQuery = `query($streamId: String!, $objectId: String!) {
stream(id: $streamId){
object(id: $objectId){
totalChildrenCount
id
speckleType
data
}
}
}`
export const latestStreamsQuery = `query {
streams(limit: 10){
cursor
totalCount
items {
id
name
description
createdAt
updatedAt
}
}
}`
+53 -27
View File
@@ -1,4 +1,10 @@
import {streamCommitsQuery, streamSearchQuery, userInfoQuery} from "@/speckleQueries";
import {
latestStreamsQuery,
streamCommitsQuery,
streamObjectQuery,
streamSearchQuery,
userInfoQuery
} from "@/speckleQueries";
export const APP_NAME = process.env.VUE_APP_SPECKLE_NAME
export const SERVER_URL = process.env.VUE_APP_SERVER_URL
@@ -6,6 +12,7 @@ export const TOKEN = `${APP_NAME}.AuthToken`
export const REFRESH_TOKEN = `${APP_NAME}.RefreshToken`
export const CHALLENGE = `${APP_NAME}.Challenge`
// Redirects to the Speckle server authentication page, using a randomly generated challenge. Challenge will be stored to compare with when exchanging the access code.
export function goToSpeckleAuthPage() {
// Generate random challenge
var challenge = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
@@ -15,13 +22,16 @@ export function goToSpeckleAuthPage() {
window.location = `${SERVER_URL}/authn/verify/${process.env.VUE_APP_SPECKLE_ID}/${challenge}`
}
export function speckleLogOut(){
// Log out the current user. This removes the token/refreshToken pair.
export function speckleLogOut() {
// Remove both token and refreshToken from localStorage
localStorage.removeItem(TOKEN)
localStorage.removeItem(REFRESH_TOKEN)
}
export function exchangeAccessCode(accessCode){
return fetch(`${SERVER_URL}/auth/token/`, {
// Exchanges the provided access code with a token/refreshToken pair, and saves them to local storage.
export async function exchangeAccessCode(accessCode) {
var res = await fetch(`${SERVER_URL}/auth/token/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -32,38 +42,54 @@ export function exchangeAccessCode(accessCode){
appSecret: process.env.VUE_APP_SPECKLE_SECRET,
challenge: localStorage.getItem(CHALLENGE)
})
}).then(res => res.json()).then(data => {
if (data.token) {
localStorage.removeItem(CHALLENGE)
localStorage.setItem(TOKEN, data.token)
localStorage.setItem(REFRESH_TOKEN, data.refreshToken)
}
return data
})
var data = await res.json()
if (data.token) {
// If retrieving the token was successful, remove challenge and set the new token and refresh token
localStorage.removeItem(CHALLENGE)
localStorage.setItem(TOKEN, data.token)
localStorage.setItem(REFRESH_TOKEN, data.refreshToken)
}
return data
}
export function speckleFetch(query){
// Calls the GraphQL endpoint of the Speckle server with a specific query.
export async function speckleFetch(query, vars) {
let token = localStorage.getItem(TOKEN)
if (token)
return fetch(
`${SERVER_URL}/graphql`,
{
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: query
try {
var res = await fetch(
`${SERVER_URL}/graphql`,
{
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: query,
variables: vars || null
})
})
})
.then(res => res.json())
return await res.json()
} catch (err) {
console.error("API call failed", err)
}
else
return Promise.reject("You are not logged in (token does not exist)")
}
export const getUserData = () => speckleFetch(userInfoQuery())
// Fetch the current user data using the userInfoQuery
export const getUserData = () => speckleFetch(userInfoQuery)
export const searchStreams = (e) => speckleFetch(streamSearchQuery(e))
// Fetch for streams matching the specified text using the streamSearchQuery
export const searchStreams = (e) => speckleFetch(streamSearchQuery, {searchText: e})
export const getStreamCommits = (streamId, itemsPerPage, cursor) => speckleFetch(streamCommitsQuery(streamId, itemsPerPage, cursor))
// Get commits related to a specific stream, allows for pagination by passing a cursor
export const getStreamCommits = (streamId, itemsPerPage, cursor) => speckleFetch(streamCommitsQuery, {id: streamId, cursor, limit: itemsPerPage})
export const getStreamObject = (streamId, objectId) => speckleFetch(streamObjectQuery, {streamId, objectId}).then(res => res.data?.stream?.object?.data)
export const getObject = (streamId, objectId) => speckleFetch(streamObjectQuery, {streamId, objectId})
export const getStreams = () => speckleFetch(latestStreamsQuery).then(res => res.data?.streams)
+12 -63
View File
@@ -1,32 +1,21 @@
import Vue from 'vue'
import Vuex from 'vuex'
import VuexPersistence from 'vuex-persist'
import {
APP_NAME,
exchangeAccessCode,
getStreamCommits,
getUserData,
goToSpeckleAuthPage,
speckleLogOut
} from "@/speckleUtils";
import router from "@/router";
Vue.use(Vuex)
const vuexLocal = new VuexPersistence({
storage: window.localStorage,
key: `${APP_NAME}.vuex`
})
export default new Vuex.Store({
plugins: [vuexLocal.plugin],
state: {
user: null,
serverInfo: null,
currentStream: null,
latestCommits: null,
previousCursors: [null],
tableOptions: null
serverInfo: null
},
getters: {
isAuthenticated: (state) => state.user != null
@@ -37,21 +26,6 @@ export default new Vuex.Store({
},
setServerInfo(state, info) {
state.serverInfo = info
},
setCurrentStream(state, stream) {
state.currentStream = stream
},
setCommits(state, commits) {
state.latestCommits = commits
},
setTableOptions(state, options) {
state.tableOptions = options
},
resetPrevCursors(state) {
state.previousCursors = [ null ]
},
addCursorToPreviousList(state, cursor){
state.previousCursors.push(cursor)
}
},
actions: {
@@ -59,51 +33,26 @@ export default new Vuex.Store({
// Wipe the state
context.commit("setUser", null)
context.commit("setServerInfo", null)
context.commit("setCurrentStream", null)
context.commit("setCommits", null)
context.commit("setTableOptions", null)
context.commit("resetPrevCursors")
// Wipe the tokens
speckleLogOut()
router.push("/login")
},
exchangeAccessCode(context, accessCode) {
// Here, we could save the tokens to the store if necessary.
return exchangeAccessCode(accessCode)
},
getUser(context) {
return getUserData()
.then(json => {
var data = json.data
context.commit("setUser", data.user)
context.commit("setServerInfo", data.serverInfo)
})
.catch(err => {
console.error(err)
})
async getUser(context) {
try {
var json = await getUserData()
var data = json.data
context.commit("setUser", data.user)
context.commit("setServerInfo", data.serverInfo)
} catch (err) {
console.error(err)
}
},
redirectToAuth() {
goToSpeckleAuthPage()
},
handleStreamSelection(context, stream) {
context.commit("setCurrentStream", stream)
context.commit("setTableOptions", { itemsPerPage: 5 })
context.commit("resetPrevCursors")
return getStreamCommits(stream.id, 5, null)
.then(json => {
context.commit("setCommits", json.data.stream.commits)
})
},
getCommits(context, cursor) {
return getStreamCommits(context.state.currentStream.id, 5, cursor)
.then(json => {
context.commit("setCommits", json.data.stream.commits)
})
},
clearStreamSelection(context){
context.commit("setCurrentStream", null)
context.commit("setCommits", null)
context.commit("setTableOptions", null)
context.commit("resetPrevCursors", [ null ])
}
},
modules: {}
+29 -99
View File
@@ -1,114 +1,44 @@
<template lang="html">
<WelcomeView v-if="!$store.getters.isAuthenticated"/>
<v-container v-else class="home pa-6">
<stream-search @selected="$store.dispatch('handleStreamSelection', $event)"/>
<h2 class="pt-6 primary--text">
<span v-if="selectedStream">
{{ selectedStream.name }} {{ selectedStream.id }}
<v-btn outlined text small class="ml-3" :href="serverUrl+'/streams/'+selectedStream.id">View in server</v-btn>
<v-btn outlined text small class="ml-3" color="error" @click="$store.dispatch('clearStreamSelection')">Clear selection</v-btn>
</span>
<span v-else>
<em>No stream selected. Find one using the search bar 👆🏼</em>
</span>
</h2>
<div class="pt-6">
<v-select
v-model="selectedKeys"
:items="availableKeys"
chips
label="Select data to display"
multiple
></v-select>
<h3 class="pa-2 primary--text">Stream commits:</h3>
<v-data-table
:loading="loading"
:headers="filteredHeaders"
:items="commits ? commits.items : []"
:options.sync="options"
:server-items-length="commits ? commits.totalCount : null"
disable-sort
disable-filtering
:disable-pagination="loading"
class="elevation-1"
></v-data-table>
</div>
<v-container fluid fill-height class="home flex-column justify-center align-center primary--text">
<h1>Hi {{ $store.state.user.name }}!!</h1>
<p>Search for a stream in the navigation bar, or pick from one of your latest 👇🏼</p>
<v-list v-if="streams" max-height="210px" class="overflow-y-auto">
<v-list-item-group>
<v-list-item v-for="stream in streams.items" :key="stream.id" @click="$router.push(`/streams/${stream.id}`)">
<v-list-item-content>
<v-list-item-title>
<v-row class="pa-0 ma-0">
{{ stream.name }}
<v-spacer></v-spacer>
<span class="primary rounded white--text pl-1 pr-1 caption">{{ stream.id }}</span>
</v-row>
</v-list-item-title>
<v-list-item-subtitle class="caption primary--text">
Updated
<timeago :datetime="stream.updatedAt"></timeago>
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</v-container>
</template>
<script>
import StreamSearch from "@/components/StreamSearch";
import WelcomeView from "@/components/WelcomeView";
import {getStreams, TOKEN} from "@/speckleUtils";
export default {
name: 'Home',
components: {WelcomeView, StreamSearch},
data: () => {
data() {
return {
loading: false,
options: {
itemsPerPage: 5
},
serverUrl: process.env.VUE_APP_SERVER_URL,
selectedKeys: ["id", "message", "branchName", "authorName"],
streams: null
}
},
mounted() {
var storedOpts = this.$store.state.tableOptions
if (storedOpts) this.options = storedOpts
async mounted() {
this.streams = await getStreams()
},
methods: {},
computed: {
selectedStream: function () {
return this.$store.state.currentStream
},
previousCursors: function () {
return this.$store.state.previousCursors || [null]
},
commits: function () {
return this.$store.state.latestCommits
},
availableKeys: function () {
var keys = {}
this.commits?.items.forEach(obj => {
Object.keys(obj).forEach(key => {
if (!keys[key]) {
keys[key] = true
}
})
})
return Object.keys(keys)
},
filteredHeaders: function () {
return this.selectedKeys.map(key => {
return {text: key, value: key}
})
}
},
watch: {
options: {
handler(val, oldval) {
this.$store.commit("setTableOptions", val)
if (oldval.page && val.page != oldval.page) {
if (val.page > oldval.page) {
this.loading = true
var cursor = this.$store.state.latestCommits.cursor
this.$store.dispatch("getCommits", cursor).then(() => {
this.$store.commit("addCursorToPreviousList", cursor)
this.loading = false
})
} else {
console.log("page down")
this.loading = true
this.$store.dispatch("getCommits", this.previousCursors[val.page - 1]).then(() => {
this.loading = false
})
}
}
},
deep: true
}
methods: {
}
}
</script>
+121
View File
@@ -0,0 +1,121 @@
<template>
<v-container fill-height>
<v-container fluid v-if="!isRevitCommit" class="d-flex flex-column justify-center primary--text">
<v-icon color="primary">mdi-x</v-icon>
<p class="headline text-center">The latest commit on this stream does not come from Revit.</p>
<p class="text-center">
<v-btn link to="/" outlined color="primary"><v-icon small left>mdi-home</v-icon>Go home</v-btn>
</p>
</v-container>
<v-container v-else-if="loading" class="d-flex flex-column justify-center align-center">
<v-progress-linear size="50" color="primary" :value="progress"></v-progress-linear>
<p class="body-2 mt-2 primary--text">Processing your data...</p>
</v-container>
<v-container fluid v-if="isRevitCommit && refObj" v-show="!loading">
<revit-dashboard v-if="selectedCommit"
:object-id="selectedCommit.referencedObject"
@loaded="loading = !$event"
@progress="progress = $event"
@legend-clicked="onLegendClick"
:info="refObj['@Project Information']"
:stream="stream">
</revit-dashboard>
</v-container>
</v-container>
</template>
<script>
import {getStreamCommits, getStreamObject} from "@/speckleUtils";
import RevitDashboard from "@/components/RevitDashboard";
import Chart from "chart.js"
export default {
name: "RevitStream",
components: { RevitDashboard },
data() {
return {
stream: null,
selectedCommit: null,
refObj: null,
serverUrl: process.env.VUE_APP_SERVER_URL,
loading: true,
progress: 0
}
},
async mounted() {
if (this.streamId) {
this.getStream()
}
},
computed: {
streamId() {
return this.$route.params.id
},
isRevitCommit() {
return this.selectedCommit?.sourceApplication?.startsWith("Revit")
}
},
methods: {
async onLegendClick(e, legendItem) {
Chart.helpers.each(Chart.instances, function (instance) {
var chart = instance.chart
const index = chart.legend.legendItems.findIndex(element => element.text === legendItem.text);
if (index === -1) return
const {
type
} = chart.config;
if (type === 'pie' || type === 'doughnut') {
// Pie and doughnut charts only have a single dataset and visibility is per item
var ilen = (chart.data.datasets || []).length
for (let i = 0; i < ilen; ++i) {
var meta = chart.getDatasetMeta(i);
// toggle visibility of index if exists
if (meta.data[index]) {
meta.data[index].hidden = !meta.data[index].hidden;
}
}
} else {
const meta = chart.getDatasetMeta(index);
// See controller.isDatasetVisible comment
meta.hidden = meta.hidden === null ? !chart.data.datasets[index].hidden : null;
// We hid a dataset ... rerender the chart
}
chart.update();
})
},
async getStream() {
var res = await getStreamCommits(this.streamId, 1, null)
this.selectedCommit = res.data.stream.commits.items[0]
this.stream = res.data.stream
},
},
watch: {
streamId: {
handler: async function (val, oldVal) {
if (val) this.getStream()
}
},
selectedCommit: {
handler: async function () {
this.refObj = await getStreamObject(this.stream.id, this.selectedCommit.referencedObject)
}
}
}
}
</script>
<style scoped>
.bg-img {
background-position: center;
background-repeat: no-repeat;
/*background-attachment: fixed;*/
}
.max-h {
max-height: 400px;
height: 400px;
}
</style>
+16
View File
@@ -0,0 +1,16 @@
<template lang="html">
<v-container fill-height class="home flex-column justify-center align-center primary--text">
<v-img src="@/assets/logo.png" max-height="140px" max-width="140px"></v-img>
<h1>Welcome to the Speckle Revit Dashboard</h1>
<p>This app allows you to analyse the data sent from Revit to Speckle.</p>
<v-alert type="info" text color="primary">
Check out the <a href="https://speckle.systems/blog" target="_blank">blog post</a> for more info!
</v-alert>
<p class="grey--text">Please log in to access you Speckle data.</p>
</v-container>
</template>
<script>
export default {
name: 'WelcomeView'
}
</script>
+47 -2
View File
@@ -1,6 +1,51 @@
module.exports = {
transpileDependencies: [
'vuetify',
'vuex-persist'
]
'vuex-persist',
'@speckle/objectloader',
'@speckle/viewer'
],
pwa: {
manifestOptions: {
"name": "Revit Dash",
"icons": [
{
"src": "img/icons/android-icon-36x36.png",
"sizes": "36x36",
"type": "image/png",
"density": "0.75"
},
{
"src": "img/icons/android-icon-48x48.png",
"sizes": "48x48",
"type": "image/png",
"density": "1.0"
},
{
"src": "/android-icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"density": "1.5"
},
{
"src": "img/icons/android-icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"density": "2.0"
},
{
"src": "img/icons/android-icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"density": "3.0"
},
{
"src": "img/icons/android-icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"density": "4.0"
}
]
}
}
}