Merge branch 'alan/frontend' into main
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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&style=flat-square&logo=discourse&logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&logo=read-the-docs&logoColor=white" alt="docs"></a></p>
|
||||
<p align="center"><a href="https://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 
|
||||
|
||||
### 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:
|
||||
|
||||
- [](https://speckle.xyz) ⇒ creating an account at
|
||||
- [](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
|
||||
|
||||
### Resources
|
||||
|
||||
- [](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
|
||||
- [](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
|
||||
- [](https://speckle.guide/dev/) reference on almost any end-user and developer functionality
|
||||
|
||||

|
||||
|
||||
# Repo structure
|
||||
|
||||
> 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).
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
presets: ['@vue/cli-plugin-babel/preset'],
|
||||
exclude: [
|
||||
/(Speckle.js\.). /
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/* /index.html 200
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
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>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}`
|
||||
@@ -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)
|
||||
@@ -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: {}
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||