Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: TwigSlot/twig-client
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v4
Choose a base ref
...
head repository: TwigSlot/twig-client
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref

Commits on Oct 3, 2022

  1. add search feature

    natashaamin committed Oct 3, 2022
    Copy the full SHA
    7fcdb65 View commit details

Commits on Oct 5, 2022

  1. fix edit and owner name

    natashaamin committed Oct 5, 2022
    Copy the full SHA
    04d398c View commit details

Commits on Oct 6, 2022

  1. Copy the full SHA
    1a58e71 View commit details

Commits on Oct 8, 2022

  1. Neaten (#30)

    * added toggles
    
    * minor bug fix involving clearing of tag_focus
    
    * halfway through cleaning up the excessive data pulls
    
    * caching for get_tag_resources
    
    * halfway
    
    * fixed broken coloring
    
    * color bug fix
    
    * quite smooth now
    
    * neatened things
    
    * small design bugs
    
    * multiadd tags
    tch1001 authored Oct 8, 2022
    Copy the full SHA
    7525c43 View commit details
  2. oopsies for node size

    tch1001 committed Oct 8, 2022
    Copy the full SHA
    162d4d5 View commit details
  3. Copy the full SHA
    ac08783 View commit details
  4. node size

    tch1001 committed Oct 8, 2022
    Copy the full SHA
    74116d4 View commit details
  5. Copy the full SHA
    330b65a View commit details
  6. search

    natashaamin committed Oct 8, 2022
    Copy the full SHA
    daf08f5 View commit details
  7. Copy the full SHA
    061398d View commit details
  8. minor changes

    tch1001 committed Oct 8, 2022
    Copy the full SHA
    fc6a801 View commit details

Commits on Oct 13, 2022

  1. fix link (#31)

    Co-authored-by: natashaamin <natashasiaamin@gmail.com>
    tch1001 and natashaamin authored Oct 13, 2022
    Copy the full SHA
    0b24fb8 View commit details

Commits on Oct 14, 2022

  1. Update README.md

    added link to twigslot.com
    tch1001 authored Oct 14, 2022
    Copy the full SHA
    2257da5 View commit details

Commits on Oct 15, 2022

  1. Copy the full SHA
    d8a12dc View commit details

Commits on Oct 16, 2022

  1. Copy the full SHA
    2b29687 View commit details

Commits on Oct 25, 2022

  1. Update README.md (#54)

    shaunlohhh authored Oct 25, 2022
    Copy the full SHA
    ac52a12 View commit details

Commits on Dec 8, 2022

  1. added mobile view

    tch1001 committed Dec 8, 2022
    Copy the full SHA
    f3143f6 View commit details
  2. Copy the full SHA
    4143907 View commit details
  3. Copy the full SHA
    42b51ce View commit details
  4. Copy the full SHA
    2e46099 View commit details
  5. Copy the full SHA
    8d87f0c View commit details

Commits on Oct 6, 2023

  1. Copy the full SHA
    dc63645 View commit details
  2. Copy the full SHA
    fcf67c0 View commit details

Commits on Dec 1, 2023

  1. dont show buttons for non-owners + cannot edit if not owner, removed …

    …listview (reformatting)
    tch1001 committed Dec 1, 2023
    Copy the full SHA
    ad164bb View commit details
  2. Copy the full SHA
    e42521d View commit details
  3. Copy the full SHA
    c70d502 View commit details
4 changes: 2 additions & 2 deletions .env.development
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VITE_ORY_URL=http://127.0.0.1:4433
VITE_AUTH_URL=http://127.0.0.1:3000
VITE_API_URL=http://127.0.0.1:5000
VITE_AUTOFILL_URL=http://127.0.0.1:5001
VITE_API_URL=http://127.0.0.1:5001
VITE_AUTOFILL_URL=http://127.0.0.1:5002
2 changes: 1 addition & 1 deletion .env.production
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VITE_ORY_URL=https://twigslot.com/kratos
VITE_AUTH_URL=https://twigslot.com/auth
VITE_API_URL=https://twigslot.com/api
VITE_API_URL=https://twigslot.com/twig-api
VITE_AUTOFILL_URL=https://twigslot.com/autofill
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# No, we use yarn
package-lock.json
# Logs
logs
*.log
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ COPY package*.json ./
RUN npm install
COPY ./ .
ENV NODE_OPTIONS="--max-old-space-size=2048"
RUN npm run build
RUN npm run build-no-ts

FROM nginx as production-stage
RUN mkdir /app
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# TWIG Client

Web Client for [twigslot.com](https://twigslot.com)

## Development Setup
```shell
git clone http://github.com/twigslot/twig-client
@@ -26,4 +28,4 @@ Port `5173` is used because [twig-server](https://github.com/twigslot/twig-serve
docker build . -t tch1001/twig_client:v1.5
docker push tch1001/twig_client:v1.5
```
See [twig-server](https://github.com/twigslot/twig-server) for continuation.
See [twig-server](https://github.com/twigslot/twig-server) for continuation.
2 changes: 1 addition & 1 deletion kubernetes/nginx-service.yaml
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ spec:
spec:
containers:
- name: nginx-container
image: tch1001/twig_client:v3.1.1
image: tch1001/twig_client:v4.3.1
ports:
- containerPort: 80
---
7,472 changes: 0 additions & 7,472 deletions package-lock.json

This file was deleted.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"build-no-ts": "vite build",
"preview": "vite preview"
},
"dependencies": {
@@ -15,16 +16,19 @@
"@fortawesome/free-regular-svg-icons": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "^6.1.2",
"@fortawesome/vue-fontawesome": "^3.0.1",
"@hsorby/vue3-katex": "^0.6.0-rc.7",
"@ory/client": "^0.2.0-alpha.4",
"@ory/kratos-client": "^0.10.1",
"@types/jquery": "^3.5.14",
"bulma": "^0.9.4",
"element-plus": "^2.2.17",
"katex": "^0.16.9",
"local-cors-proxy": "^1.1.0",
"neo4j-driver": "^4.4.7",
"serve": "^14.0.1",
"v-network-graph": "^0.6.5",
"vue": "^3.2.37",
"vue-katex": "^0.5.0",
"vue-router": "^4.1.3",
"vuex": "^4.0.2",
"vuex-persist": "^3.1.3",
43 changes: 43 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -116,6 +116,12 @@ body {
bottom: 0;
right: 0;
}
.graph-logs-panel {
background-color: yellow;
position: fixed;
bottom: 0;
left: 0;
}
.info-panel-outer {
position: relative;
@@ -150,4 +156,41 @@ body {
.command {
cursor: pointer
}
.list-item{
width: 100%;
background: white;
/* padding: 1rem; */
margin-left: 1rem;
margin-right: 1rem;
margin-top: 1rem;
}
.list-title{
font-size: 2rem;
}
.list-description{
font-size: 2rem;
}
.list-layout{
height: 100%;
background: white;
}
.list-editable{
left: 0;
width: 45%;
background: white;
padding: 0.5rem;
}
.list-sidebyside{
display: flex;
flex-direction: row;
justify-content: space-between;
}
.list-markdown{
position: absolute;
right: 1rem;
width: 50%;
background: white;
padding: 0.5rem;
}
</style>
2 changes: 1 addition & 1 deletion src/components/ControlPanel.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="control-panel">
<div class="control-panel" v-if="!this.$store.state.mobileView">
<div id="control-box">
<button class="button is-light control-box-item" @click="$emit('home')">Center</button>
<DropdownComponent @custom_change="changed_item" :dropdownItem="options"></DropdownComponent>
44 changes: 28 additions & 16 deletions src/components/DataPanel.vue
Original file line number Diff line number Diff line change
@@ -1,37 +1,38 @@
<template>
<div class="info-panel-outer">
<div class="info-panel-outer" v-if="!this.$store.state.showInfo">
<h1 class="title is-4">ID : {{ (data_panel.uid ? data_panel.uid : "Hover over a node") }}</h1>
<div class="control">
<input class="input is-hovered info-panel-item" type="text" placeholder="Name" :value="data_panel.name"
@focus="pauseKeyDown" @blur="handleBlur('name', $event)" />
@focus="pauseKeyDown" @blur="handleBlur('name', $event)"
:disabled="this.$store.state.kratos_user_id == 'guest'"/>
</div>
<div class="control" :style="{marginBottom: '5px'}">
<input class="input is-hovered info-panel-item" type="text" placeholder="URL" :value="data_panel.link"
@focus="pauseKeyDown" @blur="handleBlur('link', $event)" />
@focus="pauseKeyDown" @blur="handleBlur('link', $event)"
:disabled="this.$store.state.kratos_user_id == 'guest'"/>
</div>
<div class="control">
<a target="_blank" :href="data_panel.link">
<button class="button is-dark">Open</button>
<button class="button is-dark info-panel-item">Open</button>
</a>
<button v-if="!this.$store.state.mobileView" @click="hide_description = !hide_description" class="button info-panel-item">{{ hide_description ? "Show"
: "Hide"}} description</button>
<text class="subtitle is-4 data-panel-item">{{ retrieval_status }}</text>
</div>
<div class="control" :style="{marginTop: '10px'}">
<div class="control" :style="{marginTop: '10px'}" v-if="!hide_description">
<textarea class="textarea" rows="5" cols="50" placeholder="Description" :value="data_panel.description"
@focus="pauseKeyDown" @blur="handleBlur('description', $event)"></textarea>
</div>

<div class="info-panel-inner">
<div></div>
@focus="pauseKeyDown" @blur="handleBlur('description', $event)"
:disabled="this.$store.state.kratos_user_id == 'guest'"></textarea>
</div>
</div>
</template>

<style lang="scss" scoped>
.control{
.control {
align-items: center;
display: flex;
}
.data-panel-item {
margin-inline: 0.2rem;
color: white;
@@ -62,6 +63,7 @@ import graphData from "../graphData";
const project_id: any = ref("");
const retrieval_status = ref("")
const hide_description = ref(true);
export default defineComponent({
name: "DataPanel",
setup() { },
@@ -72,7 +74,8 @@ export default defineComponent({
},
data() {
return {
retrieval_status
retrieval_status,
hide_description
}
},
methods: {
@@ -93,17 +96,17 @@ export default defineComponent({
`${import.meta.env.VITE_AUTOFILL_URL}` + `/?url=${new_value}`;
var name = "",
link = "",
description = "";
description = this.$props.data_panel.description;
try {
retrieval_status.value = "Retrieving website info..."
const response = await axios.get(autofill_request_url);
name = response.data.title;
link = response.data.url;
description = response.data.summary;
description += response.data.summary;
} catch (err) {
name = `${new_value}`;
link = new_value;
description = `description for ${new_value}`;
description += `\n\ndescription for ${new_value}`;
}
const nameLengthLimit = 100;
name =
@@ -119,13 +122,22 @@ export default defineComponent({
request_url_post.substring(0, requestLengthLimit) +
(request_url_post.length > requestLengthLimit ? "..." : "");
retrieval_status.value = "Saving..."
this.$emit('add_log', 'DataPanel', 'saving data panel...')
axios.post(request_url_pre + request_url_post).then((response) => {
console.log(response.data);
graphData.nodes.value[`node${response.data.uid}`] = response.data;
if (this.$props.data_panel.uid == response.data.uid) {
this.$emit("updatedDataPanel");
}
retrieval_status.value = "Saved!"
this.$emit('add_log', 'DataPanel', 'saved')
setTimeout(() => {
retrieval_status.value = ""
}, 1000)
}).catch((err) => {
console.log(err);
retrieval_status.value = "Error!"
this.$emit('add_log', 'DataPanel', 'error')
setTimeout(() => {
retrieval_status.value = ""
}, 1000)
452 changes: 322 additions & 130 deletions src/components/DecoPanel.vue

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions src/components/GraphLogs.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<template>
<div class="graph-logs-panel" @click="show_all = !show_all" v-if="!this.$store.state.mobileView">
<text v-if="show_all" class="log-message" v-for="log in logs">{{ log.type }} : {{ log.message }}<br></text>
<text v-else class="log-message">{{ logs[logs.length-1].type }} : {{ logs[logs.length-1].message }}<br></text>
</div>
</template>
<style>
.log-message{
width: 10rem;
}
</style>

<script lang="ts">
import { defineComponent, reactive, ref } from "vue";
const logs = ref([{ type: 'hello', message: 'world' }]);
const show_all = ref(false);
export default {
name: "DropdownComponent",
data() {
return {
logs,
show_all
}
},
methods: {
add_log: function(type: string, message: string){
logs.value.push({type: type, message: message});
}
},
};
</script>
108 changes: 76 additions & 32 deletions src/components/Navbar.vue
Original file line number Diff line number Diff line change
@@ -2,12 +2,42 @@
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/"> TWIGSLOT </a>

<router-link :to="{ name: 'GraphLayout',
params: {id: this.$store.state.project_id ? this.$store.state.project_id : '0' } }"
class="navbar-item">
Graph
</router-link>

<!-- <router-link :to="{ name: 'ListLayout',
params: {id: this.$store.state.project_id ? this.$store.state.project_id : '0' }}"
class="navbar-item"> -->
<a class="navbar-item"
:href="'/project/' + `${this.$store.state.project_id ? this.$store.state.project_id : '0'}` + '/list'">
List
</a>
<!-- </router-link> -->
<!-- <a class="navbar-item" :style="{ marginLeft: '10px' }" @click="toggleLayout">
<text v-if="this.$store.state.layout == 0"> Graph </text>
<text v-if="this.$store.state.layout == 1"> List </text>
</a> -->
<a class="navbar-item" :style="{ marginLeft: '10px' }" @click="toggleInfo"
v-if="this.$store.state.mobileView && this.$store.state.layout == 0"
>
<text v-if="this.$store.state.showInfo"> Show Info </text>
<text v-else> Hide Info </text>
</a>
<a class="navbar-item" @click="toggleView" >
<text v-if="this.$store.state.mobileView"> Mobile View </text>
<text v-else> Desktop View </text>
</a>
<a
role="button"
class="navbar-burger"
data-target="navMenu"
aria-label="menu"
aria-expanded="false"
role="button"
class="navbar-burger"
data-target="navMenu"
aria-label="menu"
aria-expanded="false"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
@@ -17,7 +47,9 @@

<NavBurger>
<div class="navbar-start">
<a class="navbar-item" href="/explore" :style="{ marginLeft: '10px'}"> Explore </a>
<a class="navbar-item" href="/explore" :style="{ marginLeft: '10px' }">
Explore
</a>
</div>
<div class="navbar-end">
<div v-if="session" class="navbar-item">
@@ -26,43 +58,46 @@
<div class="navbar-item">
<div class="navbar-item">
<a
v-if="!session"
class="navbar-item"
:href="authBasePath + '/login'"
v-if="!session"
class="navbar-item"
:style="{ padding: 0 }"
:href="authBasePath + '/login'"
>
Login
</a>
<a
v-if="session"
class="navbar-item"
:href="logoutUrl"
@click="logout"
v-if="session"
class="navbar-item"
:href="logoutUrl"
@click="logout"
>
Logout
</a>
<a
v-if="session"
class="navbar-item"
:href="authBasePath + '/settings'"
v-if="session"
class="navbar-item"
:href="authBasePath + '/settings'"
>
Settings
</a>
<a
v-if="session"
class="navbar-item"
:href="'/user/' + `${session.identity.id}`"
v-if="session"
class="navbar-item"
:href="'/user/' + `${session.identity.id}`"
>
Profile
</a>
<a
href="https://github.com/twigslot"
:style="{ marginLeft: '20px' }"
>
<font-awesome-icon
icon="fa-brands fa-github"
class="icon"
></font-awesome-icon>
</a>
</div>
</div>

<a class="navbar-item" href="https://github.com/twigslot" :style="{ marginRight: '20px'}">
<font-awesome-icon
icon="fa-brands fa-github"
class="icon"
></font-awesome-icon>
</a>
</div>
</NavBurger>
</nav>
@@ -73,12 +108,8 @@
margin-top: 10px;
}
.list-item {
padding-top: 10px;
}
nav {
font-family: 'Noto Sans', sans-serif;
font-family: "Noto Sans", sans-serif;
}
</style>

@@ -103,6 +134,15 @@ export default defineComponent({
logout: function () {
this.$store.commit("update_kratos_user_id", "guest");
},
toggleView: function() {
this.$store.commit("toggle_view" );
},
toggleLayout: function(){
this.$store.commit("toggle_layout")
},
toggleInfo: function(){
this.$store.commit("toggle_info")
},
},
data() {
return {
@@ -116,7 +156,11 @@ export default defineComponent({
mounted() {
sdk.toSession().then(({ data }) => {
this.session = data;
this.$store.commit("update_kratos_user_id", data.identity.id);
if(data == null || data.identity == null) {
this.$store.commit("update_kratos_user_id", "guest");
}else{
this.$store.commit("update_kratos_user_id", data.identity.id);
}
sdk.createSelfServiceLogoutFlowUrlForBrowsers().then(({ data }) => {
this.logoutUrl = data.logout_url;
});
207 changes: 136 additions & 71 deletions src/components/ProjectList.vue
Original file line number Diff line number Diff line change
@@ -4,78 +4,67 @@
<button class="button is-primary is-light" @click="add_project">
Add Project
</button>
<button v-if="connection_status != 'connected'" class="button is-primary is-danger" @click="reconnect">
<button
v-if="connection_status != 'connected'"
class="button is-primary is-danger"
@click="reconnect"
>
Failed to Connect! Click to retry
</button>
</div>

<table
class="table is-bordered is-fullwidth"
aria-describedby="project-table"
<el-table
:data="querySearch(showcased_projects, search)"
style="width: 100%"
>
<thead>
<tr>
<th><abbr title="project-name">Project</abbr></th>
<th><abbr title="description">Description</abbr></th>
<th><abbr title="owner">Owner</abbr></th>
<th><abbr title="edit">Edit</abbr></th>
</tr>
</thead>
<tbody v-for="project in showcased_projects" >
<td>
<a :href="'/project/' + `${project.project.uid}`">{{
project.project.name
<el-table-column
v-for="column in columns"
:key="column.prop"
:label="column.label"
:prop="column.prop"
:formatter="column.formatter"
:min-width="column.minWidth"
>
<template v-slot="scope" v-if="column.prop === 'project_name'">
<a :href="'/project/' + `${scope.row.project.uid}`">{{
scope.row.project.name
}}</a>
</td>
<td>
{{ project.project.description }}
</td>
<td>
<a :href="'/user/' + `${project.owner.kratos_user_id}`">{{ project.owner.first_name + ' ' + project.owner.last_name }}</a>
</td>
<td
v-if="
project.owner.kratos_user_id == $store.state.kratos_user_id ||
connection_status != 'connected'
"
>
<div class="button-container">
<div class="float-child">
<button
class="button is-primary is-light is-small is-pulled-left"
v-if="
project.owner.kratos_user_id == $store.state.kratos_user_id ||
connection_status != 'connected'
"
@click="edit_project(project.project.uid)"
>
Edit
</button>
</div>
<div class="float-child">
<button
class="button is-primary is-dark is-small is-pulled-left is-danger"
v-if="project.owner.kratos_user_id == $store.state.kratos_user_id ||
connection_status != 'connected'"
@click="delete_project(project.project.uid)"
>
Delete
</button>
</div>
</div>
</td>
<td v-else></td>

</tbody>
</table>
</template>
</el-table-column>
<el-table-column fixed="right" label="Operations">
<template #header>
<el-input
v-model="search"
size="small"
placeholder="Type to search"
/>
</template>
<template #default="scope">
<el-button
link
type="primary"
size="small"
@click="edit_project(scope.$index, scope.row)"
v-if="scope.row.owner.kratos_user_id == this.$store.state.kratos_user_id"
>Edit</el-button
>
<el-button
link
type="danger"
size="small"
@click="delete_project(scope.$index, scope.row)"
v-if="scope.row.owner.kratos_user_id == this.$store.state.kratos_user_id"
>Delete</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</template>

<style lang="scss" scoped>
.main-container {
margin: 20px !important;
font-family: 'Noto Sans', sans-serif;
font-family: "Noto Sans", sans-serif;
}
.button-container {
@@ -89,16 +78,46 @@
.float-child:first-child {
margin-right: 10px;
}
.my-autocomplete li {
line-height: normal;
padding: 7px;
}
.my-autocomplete li .name {
text-overflow: ellipsis;
overflow: hidden;
}
.my-autocomplete li .addr {
font-size: 12px;
color: #b4b4b4;
}
.my-autocomplete li .highlighted .addr {
color: blue;
}
</style>

<script lang="ts">
import axios from "axios";
import { defineComponent, ref } from "vue";
import { defineComponent, onMounted, ref } from "vue";
const kratos_user_id: any = ref("");
const username = ref("");
const showcased_projects: any = ref([]);
const connection_status: any = ref("connecting...");
const search = ref("");
const table = ref("");
const links = ref<ILinkItem[]>([]);
interface ILinkItem {
owner: string;
projects: {
uid: number;
name: string;
description: string;
}[];
}
export default defineComponent({
name: "ProjectList",
@@ -109,11 +128,31 @@ export default defineComponent({
kratos_user_id,
username,
connection_status,
search,
table,
columns: [
{
prop: "project_name",
label: "Project",
},
{
prop: "project.description",
label: "Description",
minWidth: "150px",
},
{
prop: "owner",
label: "Owner",
formatter: (row: any) => {
return `${row.owner.first_name} ${row.owner.last_name}`;
},
},
],
};
},
methods: {
reconnect: function(){
this.get_projects()
reconnect: function () {
this.get_projects();
},
add_project: function () {
const request_url =
@@ -124,20 +163,28 @@ export default defineComponent({
showcased_projects.value.unshift(response.data);
});
},
delete_project: function (project_id: any) {
if(!window.confirm(`Do you really want to delete project ${project_id}`)) return;
delete_project: function (index: number, row: any) {
if (
!window.confirm(
`Do you really want to delete project ${row.project.uid}`
)
)
return;
const request_url =
`${import.meta.env.VITE_API_URL}` +
`/project/${project_id}` +
`/project/${row.project.uid}` +
`/delete`;
axios.post(request_url).then((response) => {
showcased_projects.value = showcased_projects.value.filter(
(ele: any) => ele.project.uid != project_id
(ele: any) => ele.project.uid != row.project.uid
);
});
},
edit_project: function (project_id: any) {
this.$router.push("/project-edit/" + project_id);
edit_project: function (index: number, row: any) {
this.$router.push({
path: "/project-edit/",
query: { id: row.project.uid },
});
},
get_projects: function () {
var request_url = "";
@@ -156,7 +203,6 @@ export default defineComponent({
username.value = response.data.user.username;
}
connection_status.value = "connected";
console.log(response.data.projects)
})
.catch((err) => {
connection_status.value = "failed to connect, err = " + err;
@@ -170,8 +216,23 @@ export default defineComponent({
},
},
];
links.value = showcased_projects.value;
});
},
onLoadSearch: function () {},
querySearch: function (datatable: any, queryString: string) {
return datatable.filter(
(data: any) =>
!queryString ||
data.project.name.toLowerCase().includes(queryString.toLowerCase()) ||
data.project.description
.toLowerCase()
.includes(queryString.toLowerCase())
);
},
handleSelect: function (item: ILinkItem) {
showcased_projects.value = [item];
}
},
mounted() {
kratos_user_id.value = this.$route.params.id;
@@ -189,5 +250,9 @@ export default defineComponent({
},
},
},
computed: {
console: () => console,
window: () => window,
},
});
</script>
7 changes: 7 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -13,10 +13,17 @@ import router from "./router"
import 'bulma/css/bulma.css'
import store from './store'

import 'katex/dist/katex.min.css';
import VueKatex from '@hsorby/vue3-katex';

library.add(faGithub)

const app = createApp(App)
app.use(VNetworkGraph)
app.use(VueKatex, {
globalOptions: {
}
})
app.component('font-awesome-icon', FontAwesomeIcon)
app.use(router)
app.use(ElementPlus)
26 changes: 19 additions & 7 deletions src/router/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Graph from '../views/Graph.vue'
import GraphLayout from '../views/GraphLayout.vue'
import ListLayout from '../views/ListLayout.vue'
import Explore from '../views/Explore.vue'
import Profile from '../views/Profile.vue'
import EditProject from '../views/EditProject.vue'
@@ -13,18 +14,29 @@ const routes = [
},
{
path: '/project',
name: "Graph",
component: Graph
name: "GraphBlank",
component: GraphLayout
},
{
path: '/project/:id',
name: "Graph",
component: Graph
name: "GraphLayout",
component: GraphLayout
},
{
path: '/project-edit/:id',
path: '/project/:id/list',
name: "ListLayout",
component: ListLayout,
},
{
path: '/project/:id/list/:resource_id',
name: "FocusListLayout",
component: ListLayout,
},
{
path: '/project-edit',
name: 'EditProject',
component: EditProject
component: EditProject,
props: (route: any) => ({id: route.query.id})
},
{
path: '/user/:id', // note id could be the kratos_user_id or username
16 changes: 16 additions & 0 deletions src/store/index.ts
Original file line number Diff line number Diff line change
@@ -10,13 +10,29 @@ const store : any = new Vuex.Store ({
state: {
kratos_user_id: 'guest',
selected_mode: 'move',
mobileView: false,
showInfo: true,
layout: 0,
project_id: '0',
},
mutations: {
update_kratos_user_id(state, kratos_user_id){
state.kratos_user_id = kratos_user_id
},
update_selected_mode(state, new_mode){
state.selected_mode = new_mode
},
toggle_view(state){
state.mobileView = !state.mobileView
},
toggle_layout(state){
state.layout = (state.layout + 1) % 2;
},
toggle_info(state){
state.showInfo = !state.showInfo;
},
update_project_id(state, project_id){
state.project_id = project_id;
}
}
})
17 changes: 11 additions & 6 deletions src/views/EditProject.vue
Original file line number Diff line number Diff line change
@@ -8,7 +8,9 @@
</el-button-group>
</div>

<h1 class="title is-3 has-text-centered">Project ID: <span class="is-family-monospace">{{ project.id }}</span></h1>
<h1 class="title is-3 has-text-centered">
Project ID: <span class="is-family-monospace">{{ project.id }}</span>
</h1>

<div class="control" :style="{ paddingBottom: '30px' }">
<el-input
@@ -33,7 +35,7 @@
<style lang="scss" scoped>
.main-container {
margin: 20px !important;
font-family: 'Noto Sans', sans-serif;
font-family: "Noto Sans", sans-serif;
}
.prev-page {
@@ -48,13 +50,16 @@ import { ArrowLeft } from "@element-plus/icons-vue";
const project: any = ref({ id: 0, name: "Loading...", description: "" });
export default defineComponent({
name: "EditProject",
setup() {},
setup() {
},
data() {
return {
project,
ArrowLeft
ArrowLeft,
};
},
methods: {
@@ -72,7 +77,7 @@ export default defineComponent({
},
},
mounted() {
project.id = this.$route.params.id;
project.id = this.$route.query.id;
const request_url =
`${import.meta.env.VITE_API_URL}` +
`/project/${project.id}` +
@@ -82,6 +87,6 @@ export default defineComponent({
project.value = response.data.project;
project.value.id = response.data.project.uid;
});
},
}
});
</script>
2 changes: 2 additions & 0 deletions src/views/Explore.vue
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@
import { defineComponent } from 'vue';
import ProjectList from '../components/ProjectList.vue';
document.title = "Explore"
export default defineComponent({
name: "Explore",
components: {
295 changes: 0 additions & 295 deletions src/views/Graph.vue

This file was deleted.

326 changes: 326 additions & 0 deletions src/views/GraphLayout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
<template>
<div>
<div id="graph" ref="div_ref">
<v-network-graph ref="graph_ref" v-model:selected-nodes="graphData.selectedNodes"
v-model:selected-edges="graphData.selectedEdges" :nodes="graphData.nodes" :edges="graphData.edges"
:layouts="graphData.layouts" :configs="graphData.configs" :event-handlers="eventHandlers" />
</div>
<DataPanelVue @add_log="add_log" @pauseKeyDown="pauseKeyDown" @resumeKeyDown="resumeKeyDown" :data_panel="dataPanel"
@updatedDataPanel="updatedDataPanel"></DataPanelVue>
<ControlPanelVue @add_log="add_log" @home="home" @save-locations="saveLocations" @customkeydown="keydown"
ref="control_panel_ref">
</ControlPanelVue>
<DecoPanelVue @add_log="add_log" ref="deco_panel_ref" @color_node="color_node" @plain_graph="plain_graph" :data_panel="dataPanel"
:project_id="project_id" @pauseKeyDown="pauseKeyDown" @resumeKeyDown="resumeKeyDown"
:selected_nodes="graphData.selectedNodes"></DecoPanelVue>
<GraphLogsVue ref="graph_logs_ref"></GraphLogsVue>
</div>
<ListLayoutVue></ListLayoutVue>

</template>
<script lang="ts">
import axios from "axios";
import DataPanelVue from "../components/DataPanel.vue";
import ControlPanelVue from "../components/ControlPanel.vue";
import DecoPanelVue from "../components/DecoPanel.vue";
import GraphLogsVue from "../components/GraphLogs.vue";
import { defineComponent, reactive, ref } from "vue";
import graphData from "../graphData"
var dataPanel: any = ref({});
document.title = "Editing Project"
const project_id: any = ref("");
const graph_ref: any = ref();
const control_panel_ref: any = ref();
const deco_panel_ref: any = ref();
const graph_logs_ref: any = ref();
var pause_key_down: boolean = false;
function delete_node(node: any) {
const request_url = `${import.meta.env.VITE_API_URL}` +
`/project/${project_id.value}` +
`/resource/${node.substring(4)}` +
`/delete`
axios.post(request_url)
.then(response => {
delete graphData.nodes.value[node]
delete graphData.layouts.value.nodes[node]
})
}
function delete_edge(edge: any) {
const request_url = `${import.meta.env.VITE_API_URL}` +
`/project/${project_id.value}` +
`/relationship/${edge}` +
`/delete`
axios.post(request_url)
.then(response => {
delete graphData.edges.value[edge]
})
}
function add_node(raw: any) {
if (!('color' in raw)) raw.color = 'blue'
if (!('size' in raw)) raw.size = 20
else raw.size = parseInt(raw.size)
graphData.nodes.value[`node${raw.uid}`] = raw
if ('pos_x' in raw && 'pos_y' in raw) {
graphData.layouts.value.nodes[`node${raw.uid}`] = {
'x': raw['pos_x'],
'y': raw['pos_y']
}
}
}
function add_edge(s: string, t: string, uid: any) {
graphData.edges.value[uid] =
{
source: s,
target: t,
uid: uid
}
}
var selected_nodes = ref([]);
var selected_edges = ref([]);
function add_edge_raw(raw: any) {
add_edge(`node${raw[0]}`, `node${raw[2]}`, raw[1].uid)
}
function get_items() {
const request_url = import.meta.env.VITE_API_URL + `/project/${project_id.value}`
axios
.get(request_url)
.then(response => {
graphData.nodes.value = {}
graphData.edges.value = {}
var edge_queue: Array<Object> = []
for (const item of response.data.items) {
if (item instanceof Array) {
// only propagate edge info afer all node info has been done
// this is because edge info depends on node_uid_name_mapping
edge_queue.push(item)
} else {
add_node(item)
}
}
for (const item of edge_queue) {
add_edge_raw(item)
}
})
}
export default defineComponent({
name: "Graph",
components: {
DataPanelVue,
ControlPanelVue,
DecoPanelVue,
GraphLogsVue
},
data() {
return {
graph_ref,
control_panel_ref,
project_id,
graphData,
dataPanel,
eventHandlers: {
// wildcard: capture all events
"*": (type: string, event: any) => {
if (type == 'node:pointerover') {
dataPanel.value = graphData.nodes.value[event.node]
} else if (type == 'node:pointerout') {
if (selected_nodes.value.length > 0) {
dataPanel.value = graphData.nodes.value[selected_nodes.value[0]]
}
} else if (type == 'view:click') {
this.handle_view_click(event)
} else if (type == 'node:select') {
this.handle_node_select(event)
} else if (type == 'node:click') {
this.handle_node_click(event)
} else if (type == 'edge:select') {
this.handle_edge_select(event)
}
},
}
}
},
methods: {
add_log: function (type: string, message: string) {
(this.$refs.graph_logs_ref as any).add_log(type, message);
},
plain_graph: function(){
for(const x in graphData.nodes.value){
graphData.nodes.value[x].color = 'blue'
graphData.nodes.value[x].highest_priority = -100
}
},
color_node: function (node_id: any, color: any, priority: number, override: boolean) {
const n = graphData.nodes.value[`node${node_id}`]
if (!n) return
if(!priority) priority = 0;
if (override) {
n.color = color;
n.highest_priority = -100;
return;
}
if (!('highest_priority' in n)) n.highest_priority = -100;
if (priority >= n.highest_priority) {
n.color = color
n.highest_priority = priority
}
},
updatedDataPanel: function () {
dataPanel.value = graphData.nodes.value[`node${dataPanel.value.uid}`]
},
handle_view_click: function (event: any) {
const selected_mode = (this.$store.state as any).selected_mode
if (selected_mode == 'add-node') {
const request_url = `${import.meta.env.VITE_API_URL}` +
`/project/${project_id.value}` +
`/new?item=node`
axios
.post(request_url)
.then(response => {
this.add_node_with_mouse(response.data, event.event);
});
(this.$store.state as any).selected_mode = 'move'
} else if (selected_mode == 'add-edge') {
(this.$store.state as any).edge_source_node = null;
}
},
handle_node_click: function (event: any) {
const selected_mode = (this.$store.state as any).selected_mode
if (selected_mode == 'add-edge') {
if ((this.$store.state as any).edge_source_node) {
this.create_edge((this.$store.state as any).edge_source_node, event.node);
(this.$store.state as any).selected_mode = 'move'
}
} else {
(this.$store.state as any).edge_source_node = event.node
}
},
create_edge: function (s: any, t: any) {
const request_url = `${import.meta.env.VITE_API_URL}` +
`/project/${project_id.value}` +
`/new?item=relationship` +
`&a_id=${s.substring(4)}&b_id=${t.substring(4)}`
axios.post(request_url)
.then(response => {
graphData.edges.value[response.data.uid] = {
source: s,
target: t,
uid: response.data.uid
}
});
(this.$store.state as any).edge_source_node = null
},
handle_node_select: function (event: any) {
const selected_mode = (this.$store.state as any).selected_mode
if (selected_mode == 'add-edge') {
if (event.length == 1) {
(this.$store.state as any).edge_source_node = event[0]
} else {
(this.$store.state as any).edge_source_node = null
}
}
selected_nodes.value = event
},
handle_edge_select: function (event: any) {
selected_edges.value = event
},
home: function () {
const inf = 10000000000000000;
var minX = inf, minY = inf, maxX = -inf, maxY = -inf;
for (const i in graphData.layouts.value.nodes) {
const node = graphData.layouts.value.nodes[i]
minX = Math.min(minX, node.x)
maxX = Math.max(maxX, node.x)
minY = Math.min(minY, node.y)
maxY = Math.max(maxY, node.y)
}
const padding = 100;
(this.$refs.graph_ref as any).setViewBox({
left: minX - padding,
top: minY - padding,
right: maxX + padding * 5,
bottom: maxY + padding,
});
},
saveLocations: function () {
const node_pos_raw = (this.$refs.graph_ref as any).layouts.nodes
const node_pos: any = {}
for (var n in node_pos_raw) {
node_pos[n.substring(4)] = {
x: node_pos_raw[n].x,
y: node_pos_raw[n].y,
}
}
const request_url = `${import.meta.env.VITE_API_URL}` +
`/project/${project_id.value}` +
`/positions/update`;
const cp_ref: any = (this.$refs.control_panel_ref as any);
cp_ref.save_locations_status = 'saving...'
axios.post(request_url, node_pos).then((response) => {
if (response.status == 200) {
this.add_log('Location', "Saved");
(cp_ref).save_locations_status = 'saved'
setTimeout(() => {
(cp_ref).save_locations_status = ''
}, 1000)
} else {
throw 'err'
}
}).catch((error) => {
(cp_ref).save_locations_status = 'error!'
setTimeout(() => {
(cp_ref).save_locations_status = ''
}, 1000)
})
},
get_items,
add_node_with_mouse: function (raw: any, e: any) {
const point = { x: e.offsetX, y: e.offsetY }
const svgPoint = (this.$refs.graph_ref as any).translateFromDomToSvgCoordinates(point);
graphData.layouts.value.nodes[`node${raw.uid}`] = svgPoint
add_node(raw)
},
keydown: function (e: any) {
if (pause_key_down) return;
if (e.key == 'e') {
this.$store.commit('update_selected_mode', 'add-edge')
} else if (e.key == 'v') {
this.$store.commit('update_selected_mode', 'add-node')
} else if (e.key == 'a') {
this.$store.commit('update_selected_mode', 'move')
} else if (e.key == 'Backspace' || e.key == 'Delete') {
if (selected_nodes.value.length > 0 && confirm(`Delete ${selected_nodes.value.length} nodes?`)) {
for (const node of selected_nodes.value) {
delete_node(node)
}
}
if (selected_edges.value.length > 0 && confirm(`Delete ${selected_edges.value.length} edges?`)) {
for (const edge of selected_edges.value) {
delete_edge(edge)
}
}
}
},
pauseKeyDown: function () { pause_key_down = true; },
resumeKeyDown: function () { pause_key_down = false; }
},
mounted() {
axios.defaults.headers.common['X-User'] = this.$store.state.kratos_user_id
project_id.value = this.$route.params.id
this.$store.commit('update_project_id', project_id.value)
get_items()
},
beforeUnmount() {
// so that the next time we open the graph,
// there won't be a flicker from the old graph data
graphData.nodes.value = {}
graphData.edges.value = {}
}
})
</script>
221 changes: 221 additions & 0 deletions src/views/ListLayout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<template>
<div class="list-layout">
<ul>
<li v-for="rsc in graphData.nodes" class="list-item" ref="nodes">
<div v-if="!focus_resource_id || (focus_resource_id && rsc.uid == focus_resource_id)">
<div class="list-sidebyside">
<div class="list-editable">
<div class="control">
<h1 class="title is-4">ID : {{ rsc.uid }}</h1>
</div>
<div class="control" :style="{ marginTop: '10px', width: '90%' }">
<input class="input is-hovered info-panel-item" type="text" placeholder="Name" :value="rsc.name"
:style="{ width: '80%' }" @focus="pauseKeyDown" @blur="handleBlur('name', $event, rsc)" />
<a target="_blank" :href="rsc.link">
<button class="button is-dark info-panel-item">Open</button>
</a>
</div>
<div class="control" :style="{ marginTop: '10px', width: '90%' }">
<input class="input is-hovered info-panel-item" type="text" placeholder="URL" :value="rsc.link"
@focus="pauseKeyDown" @blur="handleBlur('link', $event, rsc)" />
</div>
<div class="control" :style="{ marginTop: '10px', width: '90%' }">
<textarea class="textarea" rows="5" cols="50" placeholder="Description" :value="rsc.description"
@focus="pauseKeyDown" @blur="handleBlur('description', $event, rsc)"></textarea>
</div>
</div>
<div class="list-markdown">
<text>Incoming edges:</text>
<ul>
<li v-if="graphData.adjacency_list" v-for="towards in graphData.adjacency_list[rsc.uid]"
class="list-item">
<!-- <router-link :to="{ name: 'FocusListLayout', params: { id: project_id, resource_id: towards } }"
@click="forceReload(towards)"> -->
<a :href="'/project/' + project_id + '/list/' + towards">
{{ graphData.nodes['node' + towards].uid }} : {{ graphData.nodes['node' + towards].name }}
</a>
<!-- </router-link> -->
</li>
</ul>
<div v-if="rsc.markdown_description">
<katex-element :expression="rsc.markdown_description">
{{ rsc.markdown_description }}
</katex-element>
</div>
<text> Outgoing edges: </text>
<ul>
<li v-if="graphData.adjacency_list_reverse" v-for="towards in graphData.adjacency_list_reverse[rsc.uid]"
class="list-item">
<!-- <router-link :to="{ name: 'FocusListLayout', params: { id: project_id, resource_id: towards } }"
@click="forceReload(towards)"> -->
<a :href="'/project/' + project_id + '/list/' + towards">
{{ graphData.nodes['node' + towards].uid }} : {{ graphData.nodes['node' + towards].name }}
</a>
<!-- </router-link> -->
</li>
</ul>
</div>
</div>
</div>
</li>
</ul>
</div>
</template>
<script lang="ts">
import axios from "axios";
import { defineComponent, ref } from "vue";
import graphData from "../graphData";
import { transferCheckedChangeFn } from "element-plus";
const project_id: any = ref("");
const focus_resource_id: any = ref("");
function add_node(raw: any) {
if (!('color' in raw)) raw.color = 'blue'
if (!('size' in raw)) raw.size = 20
else raw.size = parseInt(raw.size)
graphData.nodes.value[`node${raw.uid}`] = raw
if ('pos_x' in raw && 'pos_y' in raw) {
graphData.layouts.value.nodes[`node${raw.uid}`] = {
'x': raw['pos_x'],
'y': raw['pos_y']
}
}
}
function add_edge(s: string, t: string, uid: any) {
graphData.edges.value[uid] =
{
source: s,
target: t,
uid: uid
}
}
function add_edge_raw(raw: any) {
add_edge(`node${raw[0]}`, `node${raw[2]}`, raw[1].uid)
}
function populate_adjacency_list() {
console.log(graphData.edges.value)
for (const edge of Object.values(graphData.edges.value)) {
var source_id = edge.source.replace('node', '')
var target_id = edge.target.replace('node', '')
if (!(source_id in graphData.adjacency_list)) {
graphData.adjacency_list[source_id] = []
}
if (!(target_id in graphData.adjacency_list_reverse)) {
graphData.adjacency_list_reverse[target_id] = []
}
graphData.adjacency_list[source_id].push(target_id)
graphData.adjacency_list_reverse[target_id].push(source_id)
}
}
function render_markdown(text: string) {
var lines = text.split('\n\n')
// for each line, turn the things that are not math into \text{...}
for (var i = 0; i < lines.length; i++) {
var line = lines[i]
var new_line = '\\text{'
var in_math = false
for (var j = 0; j < line.length; j++) {
var char = line[j]
if (char == '$') {
if (in_math) {
new_line += '\\text{'
} else {
new_line += '}'
}
in_math = !in_math
} else {
new_line += char
}
}
if (!in_math) {
new_line += '}'
}
lines[i] = new_line
}
return lines.join('\\\\')
}
function get_items() {
const request_url = import.meta.env.VITE_API_URL + `/project/${project_id.value}`
axios
.get(request_url)
.then(response => {
console.log(response.data)
graphData.nodes.value = {}
graphData.edges.value = {}
graphData.adjacency_list = {}
graphData.adjacency_list_reverse = {}
var edge_queue: Array<Object> = []
for (const item of response.data.items) {
if (item instanceof Array) {
// only propagate edge info afer all node info has been done
// this is because edge info depends on node_uid_name_mapping
edge_queue.push(item)
} else {
add_node(item)
}
}
for (const item of edge_queue) {
add_edge_raw(item)
}
for (const node of Object.values(graphData.nodes.value)) {
node.markdown_description = render_markdown(node.description)
}
populate_adjacency_list()
})
}
export default defineComponent({
name: "ListLayout",
data() {
return {
graphData,
project_id,
focus_resource_id
};
},
methods: {
get_items,
pauseKeyDown: function () {
this.$emit("pauseKeyDown");
},
handleBlur: async function (property: string, e: any, resource: any) {
this.$emit("resumeKeyDown");
const new_value = e.target.value;
const resource_id = resource.uid;
if (!project_id.value) return;
if (!resource_id) return;
const request_url_pre =
`${import.meta.env.VITE_API_URL}` +
`/project/${project_id.value}` +
`/resource/${resource_id}` +
`/edit?`;
var request_url_post = `${property}=${encodeURIComponent(new_value)}`;
graphData.nodes.value[`node${resource_id}`].markdown_description = '';
axios.post(request_url_pre + request_url_post).then((response) => {
graphData.nodes.value[`node${response.data.uid}`] = response.data;
graphData.nodes.value[`node${response.data.uid}`].markdown_description = render_markdown(response.data.description);
}).catch((err) => {
alert(err)
})
},
// forceReload: function (towards: any) {
// focus_resource_id.value = towards
// }
},
mounted() {
axios.defaults.headers.common['X-User'] = this.$store.state.kratos_user_id
project_id.value = this.$route.params.id
focus_resource_id.value = this.$route.params.resource_id
this.$store.commit('update_project_id', project_id.value)
get_items()
},
beforeUnmount() {
// so that the next time we open the graph,
// there won't be a flicker from the old graph data
graphData.nodes.value = {}
graphData.edges.value = {}
}
});
</script>
3 changes: 3 additions & 0 deletions src/views/Profile.vue
Original file line number Diff line number Diff line change
@@ -11,6 +11,8 @@ import ProjectList from "../components/ProjectList.vue";
import CardComponent from "../components/CardComponent.vue";
import axios from "axios";
const sdk = login.sdk;
const default_profile_picture = "/profile_pic.jpeg";
const user : any = ref({
@@ -97,6 +99,7 @@ export default defineComponent({
})
}
})
document.title = this.user.firstName + " " + this.user.lastName
},
});
</script>
3 changes: 3 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -3,6 +3,9 @@ import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
server: {
host: "0.0.0.0"
},
plugins: [vue()],
css: {
preprocessorOptions: {
4,538 changes: 2,175 additions & 2,363 deletions yarn.lock

Large diffs are not rendered by default.