Merge branch 'alan/frontend' into main

This commit is contained in:
Alan Rynne
2021-10-28 18:50:23 +02:00
63 changed files with 14942 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead
+18
View File
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/essential',
'eslint:recommended'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-unused-vars': 'off'
}
}
+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
+26
View File
@@ -0,0 +1,26 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.graphqlconfig
schema.graphql
+78
View File
@@ -0,0 +1,78 @@
<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/>
<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>
# About Speckle
What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube/views/B9humiSpHzM?label=Speckle%20in%201%20minute%20video&style=social)
### 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
- **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!
### Try Speckle now!
Give Speckle a try in no time by:
- [![speckle XYZ](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://speckle.xyz) ⇒ creating an account at
- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
### Resources
- [![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
![Untitled](https://user-images.githubusercontent.com/2679513/132021739-15140299-624d-4410-98dc-b6ae6d9027ab.png)
# Repo structure
> TBD!!!
- `frontend/`
- `backend/`
### Other repos
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).
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
exclude: [
/(Speckle.js\.). /
]
}
+12693
View File
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
{
"name": "speckle-revit-dashboard-app",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@speckle/objectloader": "^2.1.1",
"@speckle/viewer": "^2.1.1",
"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-router": "^3.2.0",
"vue-timeago": "^5.1.3",
"vuetify": "^2.4.11",
"vuex": "^3.4.0",
"vuex-persist": "^3.1.3"
},
"devDependencies": {
"@mdi/font": "^5.9.55",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-pwa": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"graphql-tag": "^2.12.5",
"sass": "^1.32.0",
"sass-loader": "^10.0.0",
"vue-cli-plugin-vuetify": "~2.3.1",
"vue-template-compiler": "^2.6.11",
"vuetify-loader": "^1.7.0"
}
}
+1
View File
@@ -0,0 +1 @@
/* /index.html 200
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 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

@@ -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.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

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

+19
View File
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow:
+152
View File
@@ -0,0 +1,152 @@
<template lang="html">
<v-app>
<v-app-bar app color="primary" dark dense>
<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="master class"
>
<h3 class="text--white">AEC Tech</h3>
</v-badge>
</v-btn>
<v-spacer></v-spacer>
<stream-search
v-if="isAuthenticated"
@selected="$router.push(`/streams/${$event.id}`)"
/>
<v-spacer></v-spacer>
<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')"
>
<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>
<v-main>
<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
}
},
computed: {
isAuthenticated() {
return this.$store.getters.isAuthenticated
}
}
}
</script>
<style lang="scss">
$heading-font-family: "Space Grotesk";
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.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: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

+73
View File
@@ -0,0 +1,73 @@
<template lang="html">
<v-autocomplete
v-model="selectedSearchResult"
:items="streams.items"
:search-input.sync="search"
no-filter
counter="2"
rounded
filled
dense
flat
hide-no-data
hide-details
placeholder="Streams Search"
item-text="name"
item-value="id"
return-object
clearable
append-icon=""
@update:search-input="debounceInput"
>
<template #item="{ item }" color="background">
<v-list-item-content>
<v-list-item-title>
<v-row class="pa-0 ma-0">
{{ item.name }}
<v-spacer></v-spacer>
<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 primary--text">
Updated
<timeago :datetime="item.updatedAt"></timeago>
</v-list-item-subtitle>
</v-list-item-content>
</template>
</v-autocomplete>
</template>
<script>
import { debounce } from "debounce"
import { searchStreams } from "@/speckleUtils"
export default {
name: "StreamSearch",
data: () => ({
search: "",
streams: { items: [] },
selectedSearchResult: null
}),
watch: {
selectedSearchResult(val) {
this.search = ""
this.streams.items = []
if (val) this.$emit("selected", val)
}
},
methods: {
async fetchSearchResults(e) {
if (!e || e?.length < 3) return
var json = await searchStreams(e)
this.streams = json.data.streams
},
debounceInput: debounce(function(e) {
this.fetchSearchResults(e)
}, 300)
}
}
</script>
<style scoped></style>
@@ -0,0 +1,38 @@
<template lang="html">
<v-hover v-slot="{ hover }">
<div
:class="{
primary: !hover & !selected,
'primary lighten-2': hover & !selected,
success: !hover & selected,
'success darken-2': hover & selected
}"
class="pa-2 commit-button d-flex justify-center align-center rounded"
@click="onClick"
>
A
</div>
</v-hover>
</template>
<script>
export default {
data() {
return {
selected: false
}
},
methods: {
onClick(event) {
this.selected = !this.selected
}
}
}
</script>
<style>
.commit-button {
cursor: pointer;
min-height: 60px;
}
</style>
@@ -0,0 +1,116 @@
<template lang="html">
<v-sheet class="pa-4" elevation="8">
<p>Compare commit</p>
<v-row dense no-gutters>
<v-col cols="6">
<v-slide-group v-model="commitA" center-active show-arrows>
<v-slide-item
v-for="n in commits"
:key="n.id"
v-slot="{ active, toggle }"
:value="n"
>
<v-tooltip bottom>
<template v-slot:activator="{ on, attrs }">
<v-card
v-bind="attrs"
v-on="on"
:color="active ? 'success' : 'primary lighten-1'"
class="ma-1"
height="100"
width="40"
@click="toggle"
>
<div class="d-flex fill-height justify-center align-center">
<v-scale-transition mode="out-in">
<span v-if="!active" style="writing-mode: vertical-rl;">
{{ n.id }}
</span>
<v-icon
v-else
color="white"
size="20"
v-text="'mdi-close-circle-outline'"
></v-icon>
</v-scale-transition>
</div>
</v-card>
</template>
<span>{{ n.message }}</span>
</v-tooltip>
</v-slide-item>
</v-slide-group>
</v-col>
<v-col cols="6">
<v-slide-group v-model="commitB" center-active show-arrows>
<v-slide-item
v-for="n in commits"
:key="n.id"
v-slot="{ active, toggle }"
:value="n"
:disabled="commitA && n.id === commitA['id']"
>
<v-tooltip bottom>
<template v-slot:activator="{ on, attrs }">
<v-card
v-bind="attrs"
v-on="on"
:color="active ? 'success' : 'primary lighten-1'"
class="ma-1"
height="100"
width="40"
@click="toggle"
:disabled="commitA && n.id === commitA['id']"
>
<div class="d-flex fill-height justify-center align-center">
<v-scale-transition mode="out-in">
<span v-if="!active" style="writing-mode: vertical-rl;">
{{ n.id }}
</span>
<v-icon
v-else
color="white"
size="20"
v-text="'mdi-close-circle-outline'"
></v-icon>
</v-scale-transition>
</div>
</v-card>
</template>
<span>{{ n.message }}</span>
</v-tooltip>
</v-slide-item>
</v-slide-group>
</v-col>
<div>
You are about to compare commit
<v-chip :close="commitA" @click:close="commitA = null">
{{ commitA ? commitA.id : "Select commit" }}
</v-chip>
against
<v-chip :close="commitB" @click:close="commitB = null">
{{ commitB ? commitB.id : "Select commit" }}
</v-chip>
<v-btn color="success" :disabled="!commitA || !commitB">
Run this!
</v-btn>
</div>
</v-row>
</v-sheet>
</template>
<script>
export default {
name: "CommitPanel",
components: {},
props: ["commits"],
data() {
return {
commitA: null,
commitB: null
}
}
}
</script>
<style></style>
@@ -0,0 +1,112 @@
<template lang="html">
<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>
@@ -0,0 +1,132 @@
<template lang="html">
<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>
@@ -0,0 +1,157 @@
<template lang="html">
<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>
@@ -0,0 +1,26 @@
<template lang="html">
<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>
+441
View File
@@ -0,0 +1,441 @@
<template lang="html">
<v-container fluid fill-height class="transparent pa-0">
<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' : ''}`"
>
<div id="renderer" ref="renderer"></div>
<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-container>
</template>
<script>
import throttle from "lodash.throttle"
import { Viewer } from "@speckle/viewer"
import { TOKEN } from "@/speckleUtils"
import ObjectSimpleViewer from "@/components/viewer/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: [],
viewer: null
}
},
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(() => this.viewer.onWindowResize(), 20)
},
loadProgress(newVal) {
if (newVal >= 99) {
let views = this.viewer.interactions.getViews()
this.namedViews.push(...views)
}
},
objectUrls() {
this.unloadData()
this.load()
}
},
// TODO: pause rendering on destroy, reinit on mounted.
async mounted() {
this.domElement = this.$refs.renderer
if (!this.viewer) {
this.viewer = new Viewer({ container: this.$refs.renderer })
}
this.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() {
this.viewer.interactions.zoomExtents()
},
setView(view) {
this.viewer.interactions.rotateTo(view)
},
setNamedView(id) {
this.viewer.interactions.setView(id)
},
sectionToggle() {
this.viewer.interactions.toggleSectionBox()
},
setupEvents() {
this.viewer.on("load-warning", ({ message }) => {
this.alertMessage = message
this.showAlert = true
})
this.viewer.on(
"load-progress",
throttle(
function(args) {
this.loadProgress = args.progress * 100
this.zoomEx()
}.bind(this),
200
)
)
this.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
this.viewer.onWindowResize()
this.objectUrls?.forEach(url => {
this.viewer.loadObject(url, localStorage.getItem(TOKEN))
this.viewerLastLoadedUrl = url
})
this.setupEvents()
this.hasLoadedModel = true
},
unloadData() {
this.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>
@@ -0,0 +1,8 @@
query {
user {
avatar
id
name
email
}
}
+19
View File
@@ -0,0 +1,19 @@
import Vue from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import router from './router'
import store from './store'
import vuetify from './plugins/vuetify'
import VueTimeago from 'vue-timeago'
Vue.use(VueTimeago, { locale: 'en' })
Vue.config.productionTip = false
new Vue({
router,
store,
vuetify,
render: h => h(App)
}).$mount('#app')
+43
View File
@@ -0,0 +1,43 @@
import '@mdi/font/css/materialdesignicons.css'
import Vue from 'vue'
import Vuetify from 'vuetify/lib'
Vue.use(Vuetify)
let darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
let hasDarkMode = localStorage.getItem('darkModeEnabled')
if (!hasDarkMode && darkMediaQuery) {
localStorage.setItem('darkModeEnabled', 'dark')
}
export default new Vuetify({
icons: {
iconfont: 'mdi'
},
theme: {
dark: false,
themes: {
light: {
primary: '#047EFB', //blue
secondary: '#7BBCFF', //light blue
accent: '#FCF25E', //yellow
error: '#FF5555', //red
warning: '#FF9100', //orange
info: '#313BCF', //dark blue
success: '#4caf50',
background: '#eeeeee',
text: '#FFFFFF'
},
dark: {
primary: '#047EFB', //blue
secondary: '#7BBCFF', //light blue
accent: '#FCF25E', //yellow
error: '#FF5555', //red
warning: '#FF9100', //orange
info: '#313BCF', //dark blue
success: '#4caf50',
background: '#3a3b3c'
}
}
}
})
+32
View File
@@ -0,0 +1,32 @@
/* eslint-disable no-console */
import { register } from 'register-service-worker'
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready () {
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB'
)
},
registered () {
console.log('Service worker has been registered.')
},
cached () {
console.log('Content has been cached for offline use.')
},
updatefound () {
console.log('New content is downloading.')
},
updated () {
console.log('New content is available; please refresh.')
},
offline () {
console.log('No internet connection found. App is running in offline mode.')
},
error (error) {
console.error('Error during service worker registration:', error)
}
})
}
+79
View File
@@ -0,0 +1,79 @@
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 StreamView from "@/views/StreamView"
Vue.use(VueRouter)
const routes = [
{
path: "/",
name: "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: StreamView,
meta: {
requiresAuth: true,
title: "Stream | Speckle Revit Dashboard"
}
}
]
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
})
router.beforeEach(async (to, from, next) => {
if (to.meta.title) {
document.title = to.meta.title
}
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
+75
View File
@@ -0,0 +1,75 @@
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{
id
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
}
}
}`
+95
View File
@@ -0,0 +1,95 @@
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
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)
// Save challenge in localStorage
localStorage.setItem(CHALLENGE, challenge)
// Send user to auth page
window.location = `${SERVER_URL}/authn/verify/${process.env.VUE_APP_SPECKLE_ID}/${challenge}`
}
// 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)
}
// 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',
},
body: JSON.stringify({
accessCode: accessCode,
appId: process.env.VUE_APP_SPECKLE_ID,
appSecret: process.env.VUE_APP_SPECKLE_SECRET,
challenge: localStorage.getItem(CHALLENGE)
})
})
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
}
// 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)
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
})
})
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)")
}
// Fetch the current user data using the userInfoQuery
export const getUserData = () => speckleFetch(userInfoQuery)
// Fetch for streams matching the specified text using the streamSearchQuery
export const searchStreams = (e) => speckleFetch(streamSearchQuery, {searchText: e})
// 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)
+59
View File
@@ -0,0 +1,59 @@
import Vue from 'vue'
import Vuex from 'vuex'
import {
exchangeAccessCode,
getUserData,
goToSpeckleAuthPage,
speckleLogOut
} from "@/speckleUtils";
import router from "@/router";
Vue.use(Vuex)
export default new Vuex.Store({
state: {
user: null,
serverInfo: null
},
getters: {
isAuthenticated: (state) => state.user != null
},
mutations: {
setUser(state, user) {
state.user = user
},
setServerInfo(state, info) {
state.serverInfo = info
}
},
actions: {
logout(context) {
// Wipe the state
context.commit("setUser", null)
context.commit("setServerInfo", null)
// Wipe the tokens
speckleLogOut()
router.push("/login")
},
exchangeAccessCode(context, accessCode) {
// Here, we could save the tokens to the store if necessary.
return exchangeAccessCode(accessCode)
},
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()
}
},
modules: {}
})
+54
View File
@@ -0,0 +1,54 @@
<template lang="html">
<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 {getStreams, TOKEN} from "@/speckleUtils";
export default {
name: 'Home',
data() {
return {
streams: null
}
},
async mounted() {
this.streams = await getStreams()
},
methods: {
}
}
</script>
<style lang="scss">
#viewer {
min-height: 500px;
}
.v-data-footer__select {
display: none !important;
}
</style>
+104
View File
@@ -0,0 +1,104 @@
<template lang="html">
<v-container fill-height fluid class="pa-0">
<div class="float-center-top">
<CommitPanel v-if="stream" :commits="stream.commits.items"></CommitPanel>
</div>
<v-row class="fill-height" no-gutters>
<v-col fill-height cols="6">
<Renderer
v-if="stream"
:object-urls="[objectUrl(0)]"
show-selection-helper
></Renderer>
</v-col>
<v-col fill-height cols="6">
<Renderer
v-if="stream"
:object-urls="[objectUrl(1)]"
show-selection-helper
></Renderer>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { getStreamCommits, getStreamObject } from "@/speckleUtils"
import Renderer from "../components/viewer/Renderer.vue"
import CommitPanel from "@/components/commitSelector/CommitPanel.vue"
export default {
name: "StreamView",
components: { Renderer, CommitPanel },
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
}
},
methods: {
async getStream() {
var res = await getStreamCommits(this.streamId, 10, null)
this.selectedCommit = res.data.stream.commits.items[0]
this.stream = res.data.stream
},
objectUrl(i) {
return [
`${this.serverUrl}/streams/${this.stream.id}/objects/${this.stream.commits.items[i].referencedObject}`
]
}
},
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;
}
.float-center-top {
position: absolute;
display: flex;
left: 50%;
max-width: 80%;
transform: translatex(-50%);
top: 1em;
z-index: 3;
}
</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>
+51
View File
@@ -0,0 +1,51 @@
module.exports = {
transpileDependencies: [
'vuetify',
'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"
}
]
}
}
}