Web App:
- Refactor of the app to be a bit more grown up - Use the Vue Router to provide a browser history API Server: - Begin support for attribute searching
This commit is contained in:
parent
c027cd5410
commit
1e467d5d3b
22
README.md
22
README.md
@ -1,16 +1,21 @@
|
|||||||
# dn42regsrv
|
# dn42regsrv
|
||||||
|
|
||||||
A REST API for the DN42 registry, written in Go, to provide a bridge between
|
A REST API for the DN42 registry, written in Go, to provide a bridge between
|
||||||
interactive applications and the DN42 registry.
|
interactive applications and registry data.
|
||||||
|
|
||||||
|
A public instance of the API and explorer web app can be accessed via:
|
||||||
|
|
||||||
|
* [https://explorer.burble.com/](https://explorer.burble.com/) (Internet link)
|
||||||
|
* [http://collector.dn42:8042/](http://collector.dn42:8042/) (DN42 Link)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- REST API for querying DN42 registry objects
|
* REST API for querying DN42 registry objects
|
||||||
- Able to decorate objects with relationship information based on SCHEMA type definitions
|
* Able to decorate objects with relationship information based on SCHEMA type definitions
|
||||||
- Includes a simple webserver for delivering static files which can be used to deliver
|
* Includes a simple webserver for delivering static files which can be used to deliver
|
||||||
basic web applications utilising the API (such as the included DN42 Registry Explorer)
|
basic web applications utilising the API (such as the included DN42 Registry Explorer)
|
||||||
- Automatic pull from the DN42 git repository to keep the registry up to date
|
* Automatic pull from the DN42 git repository to keep the registry up to date
|
||||||
- Included responsive web app for exploring the registry
|
* Includes a responsive web app for exploring the registry
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
@ -27,8 +32,8 @@ Use --help to view configurable options
|
|||||||
${GOPATH}/bin/dn42regsrv --help
|
${GOPATH}/bin/dn42regsrv --help
|
||||||
```
|
```
|
||||||
|
|
||||||
The server requires access to a clone of the DN42 registry and for the git executable
|
The server requires access to a clone of the DN42 registry and for
|
||||||
to be accessible.
|
the git executable to be accessible.
|
||||||
If you want to use the auto pull feature then the registry must
|
If you want to use the auto pull feature then the registry must
|
||||||
also be writable by the server.
|
also be writable by the server.
|
||||||
|
|
||||||
@ -60,6 +65,5 @@ Please feel free to raise issues or create pull requests for the project git rep
|
|||||||
|
|
||||||
### DN42 Registry Explorer Web App
|
### DN42 Registry Explorer Web App
|
||||||
|
|
||||||
- Add search history and fix going back
|
|
||||||
- Allow for attribute searches
|
- Allow for attribute searches
|
||||||
|
|
||||||
|
@ -2,51 +2,21 @@
|
|||||||
// DN42 Registry Explorer
|
// DN42 Registry Explorer
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
// global store for data that is loaded once
|
||||||
// registry-stats component
|
const GlobalStore = {
|
||||||
|
data: {
|
||||||
Vue.component('registry-stats', {
|
RegStats: null,
|
||||||
template: '#registry-stats-template',
|
Index: null
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
state: "loading",
|
|
||||||
error: "",
|
|
||||||
types: null,
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateSearch: function(str) {
|
|
||||||
vm.updateSearch(str)
|
|
||||||
},
|
|
||||||
|
|
||||||
reload: function(event) {
|
|
||||||
this.types = null,
|
|
||||||
this.state = "loading"
|
|
||||||
|
|
||||||
axios
|
|
||||||
.get('/api/registry/')
|
|
||||||
.then(response => {
|
|
||||||
this.types = response.data
|
|
||||||
this.state = 'complete'
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.error = error
|
|
||||||
this.state = 'error'
|
|
||||||
console.log(error)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.reload()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
// registry object component
|
// registry object component
|
||||||
|
|
||||||
Vue.component('reg-object', {
|
Vue.component('reg-object', {
|
||||||
template: '#reg-object-template',
|
template: '#reg-object-template',
|
||||||
props: [ 'link' ],
|
props: [ 'link', 'content' ],
|
||||||
data() {
|
data() {
|
||||||
return { }
|
return { }
|
||||||
},
|
},
|
||||||
@ -97,14 +67,158 @@ Vue.component('reg-attribute', {
|
|||||||
})
|
})
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
// construct a search URL from a search term
|
// search input component
|
||||||
|
|
||||||
function matchObjects(objects, rtype, term) {
|
Vue.component('search-input', {
|
||||||
|
template: '#search-input-template',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
search: '',
|
||||||
|
searchTimeout: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
debounceSearch: function(value) {
|
||||||
|
if (this.searchTimeout) {
|
||||||
|
clearTimeout(this.searchTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// map an empty search box to the landing page
|
||||||
|
if (value == "") { value = "/" }
|
||||||
|
|
||||||
|
this.searchTimeout = setTimeout(
|
||||||
|
this.$router.push.bind(this.$router, value), 500
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
|
||||||
|
// listen to search updates and set the search text appropriately
|
||||||
|
this.$root.$on('SearchChanged', value => {
|
||||||
|
this.search = value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// registry-stats component
|
||||||
|
|
||||||
|
Vue.component('registry-stats', {
|
||||||
|
template: '#registry-stats-template',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
state: null,
|
||||||
|
error: null,
|
||||||
|
store: GlobalStore.data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
// just fetch the stats from the API server via axios
|
||||||
|
reload(event) {
|
||||||
|
this.store.RegStats = null
|
||||||
|
this.state = "loading"
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get('/api/registry/')
|
||||||
|
.then(response => {
|
||||||
|
this.store.RegStats = response.data
|
||||||
|
this.state = 'complete'
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.error = error
|
||||||
|
this.state = 'error'
|
||||||
|
console.log(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.store.RegStats == null) {
|
||||||
|
this.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// root component
|
||||||
|
|
||||||
|
Vue.component('app-root', {
|
||||||
|
template: '#app-root-template',
|
||||||
|
mounted() {
|
||||||
|
this.$root.$emit('SearchChanged', '')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// search results
|
||||||
|
|
||||||
|
Vue.component('app-search', {
|
||||||
|
template: '#app-search-template',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
state: null,
|
||||||
|
search: null,
|
||||||
|
results: null,
|
||||||
|
store: GlobalStore.data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// trigger a search on route update and on mount
|
||||||
|
beforeRouteUpdate(to, from, next) {
|
||||||
|
this.search = to.params.pathMatch
|
||||||
|
this.doSearch()
|
||||||
|
next()
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// store the search for later
|
||||||
|
this.search = this.$route.params.pathMatch
|
||||||
|
|
||||||
|
// the index must be loaded before any real work takes place
|
||||||
|
if (this.store.Index == null) {
|
||||||
|
this.loadIndex()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// index was already loaded, go search
|
||||||
|
this.doSearch()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
// load the search index from the API
|
||||||
|
loadIndex() {
|
||||||
|
|
||||||
|
this.state = 'loading'
|
||||||
|
this.$root.$emit('SearchChanged', 'Initialising ...')
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get('/api/registry/*')
|
||||||
|
.then(response => {
|
||||||
|
this.store.Index = response.data
|
||||||
|
|
||||||
|
// if a query parameter has been passed,
|
||||||
|
// then go search
|
||||||
|
if (this.search != null) {
|
||||||
|
this.$nextTick(this.doSearch())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.state = 'error'
|
||||||
|
console.log(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// substring match object names against a search
|
||||||
|
matchObjects(objects, rtype, term) {
|
||||||
var results = [ ]
|
var results = [ ]
|
||||||
|
|
||||||
|
// check each object
|
||||||
for (const obj in objects) {
|
for (const obj in objects) {
|
||||||
|
|
||||||
|
// matches are all lower case
|
||||||
var s = objects[obj].toLowerCase()
|
var s = objects[obj].toLowerCase()
|
||||||
|
|
||||||
var pos = s.indexOf(term)
|
var pos = s.indexOf(term)
|
||||||
if (pos != -1) {
|
if (pos != -1) {
|
||||||
if ((pos == 0) && (s == term)) {
|
if ((pos == 0) && (s == term)) {
|
||||||
@ -116,113 +230,63 @@ function matchObjects(objects, rtype, term) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
},
|
||||||
|
|
||||||
|
|
||||||
function searchFilter(index, term) {
|
// filter the index to find matches
|
||||||
|
searchFilter() {
|
||||||
|
|
||||||
var results = [ ]
|
var results = [ ]
|
||||||
|
var index = this.store.Index
|
||||||
|
|
||||||
// comparisons are lowercase
|
// comparisons are lowercase
|
||||||
term = term.toLowerCase()
|
var term = this.search.toLowerCase()
|
||||||
|
|
||||||
// includes a '/' ? search only in that type
|
// check if search includes a '/'
|
||||||
var slash = term.indexOf('/')
|
var slash = term.indexOf('/')
|
||||||
if (slash != -1) {
|
if (slash != -1) {
|
||||||
|
// match only on the specific type
|
||||||
var rtype = term.substring(0, slash)
|
var rtype = term.substring(0, slash)
|
||||||
var term = term.substring(slash + 1)
|
var term = term.substring(slash + 1)
|
||||||
objects = index[rtype]
|
objects = index[rtype]
|
||||||
|
|
||||||
if (objects != null) {
|
if (objects != null) {
|
||||||
results = matchObjects(objects, rtype, term)
|
results = this.matchObjects(objects, rtype, term)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
||||||
// walk though the entire index
|
// walk though the entire index
|
||||||
for (const rtype in index) {
|
for (const rtype in index) {
|
||||||
results = results.concat(matchObjects(index[rtype], rtype, term))
|
var objlist = this.matchObjects(index[rtype], rtype, term)
|
||||||
|
results = results.concat(objlist)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
|
||||||
// main application
|
|
||||||
|
|
||||||
// application data
|
|
||||||
var appData = {
|
|
||||||
searchInput: '',
|
|
||||||
searchTimeout: 0,
|
|
||||||
state: '',
|
|
||||||
debug: "",
|
|
||||||
index: null,
|
|
||||||
filtered: null,
|
|
||||||
result: null
|
|
||||||
}
|
|
||||||
|
|
||||||
// methods
|
|
||||||
var appMethods = {
|
|
||||||
|
|
||||||
loadIndex: function(event) {
|
|
||||||
|
|
||||||
this.state = 'loading'
|
|
||||||
this.searchInput = 'Initialisation ...'
|
|
||||||
|
|
||||||
axios
|
|
||||||
.get('/api/registry/*')
|
|
||||||
.then(response => {
|
|
||||||
this.index = response.data
|
|
||||||
|
|
||||||
// if a query parameter has been passed,
|
|
||||||
// update the search
|
|
||||||
if (window.location.search != "") {
|
|
||||||
var param = window.location.search.substr(1)
|
|
||||||
this.$nextTick(this.updateSearch(param))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.state = ''
|
|
||||||
this.searchInput = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
// what to do here ?
|
|
||||||
console.log(error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// called on every search input change
|
// perform the search and present results
|
||||||
debounceSearchInput: function(value) {
|
doSearch() {
|
||||||
|
// notify other components that the search is updated
|
||||||
|
this.$root.$emit('SearchChanged', this.search)
|
||||||
|
|
||||||
if (this.search_timeout) {
|
// filter matches against the index
|
||||||
clearTimeout(this.search_timeout)
|
filtered = this.searchFilter()
|
||||||
}
|
|
||||||
|
|
||||||
// reset if searchbox is empty
|
// got nothing ?
|
||||||
if (value == "") {
|
if (filtered.length == 0) {
|
||||||
this.state = ""
|
|
||||||
this.searchInput = ""
|
|
||||||
this.filtered = null
|
|
||||||
this.results = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.search_timeout =
|
|
||||||
setTimeout(this.updateSearch.bind(this,value),500)
|
|
||||||
},
|
|
||||||
|
|
||||||
// called after the search input has been debounced
|
|
||||||
updateSearch: function(value) {
|
|
||||||
this.searchInput = value
|
|
||||||
this.filtered = searchFilter(this.index, value)
|
|
||||||
if (this.filtered.length == 0) {
|
|
||||||
this.state = "noresults"
|
this.state = "noresults"
|
||||||
}
|
}
|
||||||
else if (this.filtered.length == 1) {
|
|
||||||
this.state = "loading"
|
|
||||||
var details = this.filtered[0]
|
|
||||||
|
|
||||||
query = '/api/registry/' + details[0] + '/' + details[1]
|
// just one result
|
||||||
|
else if (filtered.length == 1) {
|
||||||
|
var objname = filtered[0]
|
||||||
|
|
||||||
|
// load the object from the API
|
||||||
|
this.state = 'loading'
|
||||||
|
query = '/api/registry/' + objname[0] + '/' + objname[1]
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.get(query)
|
.get(query)
|
||||||
@ -235,63 +299,35 @@ var appMethods = {
|
|||||||
this.state = 'error'
|
this.state = 'error'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lots of results
|
||||||
else {
|
else {
|
||||||
this.state = "resultlist"
|
this.state = "resultlist"
|
||||||
this.result = this.filtered
|
this.result = filtered
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// main vue application starts here
|
||||||
|
|
||||||
|
// initialise the Vue Router
|
||||||
|
const router = new VueRouter({
|
||||||
|
routes: [
|
||||||
|
{ path: '/', component: Vue.component('app-root') },
|
||||||
|
{ path: '/*', component: Vue.component('app-search') }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// and the main app instance
|
||||||
|
const vm = new Vue({
|
||||||
|
el: '#explorer_app',
|
||||||
|
data: {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
router
|
||||||
copyPermalink: function() {
|
|
||||||
|
|
||||||
// create a temporary textarea element off the page
|
|
||||||
var target = document.createElement("textarea")
|
|
||||||
target.style.position = "absolute"
|
|
||||||
target.style.left = "-9999px"
|
|
||||||
target.style.top = "0"
|
|
||||||
target.id = "_hidden_permalink_"
|
|
||||||
document.body.appendChild(target)
|
|
||||||
|
|
||||||
// set the text area content
|
|
||||||
target.textContent = this.permalink
|
|
||||||
|
|
||||||
// copy it to the clipboard
|
|
||||||
var currentFocus = document.activeElement
|
|
||||||
target.focus()
|
|
||||||
target.select()
|
|
||||||
document.execCommand('copy')
|
|
||||||
|
|
||||||
// and return to normal
|
|
||||||
if (currentFocus && typeof currentFocus.focus === "function") {
|
|
||||||
currentFocus.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.removeChild(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// intialise Vue instance
|
|
||||||
|
|
||||||
var vm = new Vue({
|
|
||||||
el: '#explorer',
|
|
||||||
data: appData,
|
|
||||||
methods: appMethods,
|
|
||||||
computed: {
|
|
||||||
permalink: function() {
|
|
||||||
return window.location.origin + '/?' + this.searchInput
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.loadIndex()
|
|
||||||
this.$nextTick(function() {
|
|
||||||
$('.popover-dismiss').popover({
|
|
||||||
trigger: 'focus'
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -1,141 +1,33 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<title>DN42 Registry Explorer</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<link rel="stylesheet" href="bootstrap.min.css">
|
<link rel="stylesheet" href="bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||||
|
<!-- Style overrides -->
|
||||||
<style>
|
<style>
|
||||||
.material-icons { display:inline-flex;vertical-align:middle }
|
.material-icons { display:inline-flex;vertical-align:middle }
|
||||||
.table th, .table td { padding: 0.2rem; border: none }
|
body { box-shadow: inset 0 2em 10em rgba(0,0,0,0.4); min-height: 100vh }
|
||||||
pre { margin-bottom: 0px }
|
|
||||||
.regref span {
|
|
||||||
padding: 0.3em 1em 0.3em 1em; margin: 0.4em;
|
|
||||||
white-space: nowrap; display:inline-block;
|
|
||||||
}
|
|
||||||
a { cursor: pointer }
|
|
||||||
</style>
|
</style>
|
||||||
<title>DN42 Registry Explorer</title>
|
|
||||||
</head>
|
</head>
|
||||||
<body style="box-shadow: inset 0 2em 10em rgba(0,0,0,0.4); min-height: 100vh">
|
|
||||||
<div id="explorer">
|
<body>
|
||||||
|
<div id="explorer_app">
|
||||||
|
|
||||||
<nav class="navbar navbar-fixed-top navbar-expand-md navbar-dark bg-dark">
|
<nav class="navbar navbar-fixed-top navbar-expand-md navbar-dark bg-dark">
|
||||||
<form class="form-inline" id="SearchForm">
|
<search-input></search-input>
|
||||||
<input v-bind:value="searchInput"
|
<div class="collapse navbar-collapse w-100 ml-auto text-nowrap">
|
||||||
v-on:input="debounceSearchInput($event.target.value)"
|
<div class="ml-auto"><router-link class="navbar-brand"
|
||||||
class="form-control-lg" size="30" type="search"
|
to="/">Registry Explorer</router-link> <a class="pull-right navbar-brand"
|
||||||
placeholder="Search the registry" aria-label="Search"/>
|
href="https://dn42.us/"><img src="/dn42_logo.png" width="173" height="60"/></a>
|
||||||
</form>
|
</div>
|
||||||
<a tabindex="0" style="margin-left: 1em" class="popover-dismiss btn btn-sm btn-dark" role="button"
|
</div>
|
||||||
data-toggle="popover" data-placement="bottom" data-trigger="focus"
|
|
||||||
title="Copied to clipboard" v-on:click="copyPermalink"
|
|
||||||
v-bind:data-content="permalink">link</a>
|
|
||||||
|
|
||||||
<div class="collapse navbar-collapse w-100 ml-auto">
|
|
||||||
<div class="ml-auto"><a class="navbar-brand"
|
|
||||||
href="/">Registry Explorer</a> <a class="pull-right navbar-brand"
|
|
||||||
href="https://dn42.us/"><img src="/dn42_logo.png" width="173"
|
|
||||||
height="60"/></a></div>
|
|
||||||
</nav>
|
</nav>
|
||||||
<div style="padding: 1em">
|
|
||||||
|
|
||||||
<div v-show="searchInput == ''">
|
<router-view></router-view>
|
||||||
|
|
||||||
<div class="jumbotron">
|
|
||||||
<h1>DN42 Registry Explorer</h1>
|
|
||||||
<p class="lead">Just start typing in the search box to start searching the registry</p>
|
|
||||||
<hr/>
|
|
||||||
<p>
|
|
||||||
<p>Search Tips</p>
|
|
||||||
<ul>
|
|
||||||
<li>Searches are case independent
|
|
||||||
<li>No need to hit enter, searches will start immediately
|
|
||||||
<li>Prefixing the search by a registry type followed by / will narrow the search to
|
|
||||||
just that type (e.g. <a v-on:click="updateSearch('domain/.dn42')"
|
|
||||||
class="text-success">domain/.dn42</a> )
|
|
||||||
<li>Searching for <b>type/</b> will return all the objects for that type (e.g.
|
|
||||||
<a v-on:click="updateSearch('schema/')" class="text-success">schema/</a> )
|
|
||||||
<li>A blank search box will return you to these instructions
|
|
||||||
<li>Searches are made on object names; searching the content of objects
|
|
||||||
is not supported (yet!).
|
|
||||||
<li>Going back (or any search history) is also not supported yet.
|
|
||||||
</ul>
|
|
||||||
<hr/>
|
|
||||||
<p>The registry explorer is a simple web app using
|
|
||||||
<a href="https://git.dn42.us/burble/dn42regsrv">dn42regsrv</a>;
|
|
||||||
a REST API for the DN42 registry built with <a href="https://golang.org/">Go</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<registry-stats/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section v-if="state == 'loading'">
|
|
||||||
<div class="alert alert-info" role="alert">
|
|
||||||
Loading data ...
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-else-if="state == 'error'">
|
|
||||||
<div class="alert alert-primary clearfix" role="alert">
|
|
||||||
An error recurred whilst retrieving data <button type="button" class="float-right btn btn-warning" v-on:click="reload"><span class="material-icons">refresh</span> Refresh</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-else-if="state == 'noresults'">
|
|
||||||
<h2>Searching for "{{ searchInput }}" ...</h2>
|
|
||||||
<div class="alert alert-dark" role="alert">
|
|
||||||
Sorry, no results found
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-else-if="state == 'resultlist'">
|
|
||||||
<h2>Listing results for "{{ searchInput }}" ...</h2>
|
|
||||||
<div class="container d-flex flex-row flex-wrap">
|
|
||||||
<div style="text-align: center">
|
|
||||||
<span v-for="value in result" style="margin: 0.5em 1em 0.5em 1em; display:inline-block">
|
|
||||||
<reg-object v-bind:link="value[0] + '/' + value[1]"></reg-object>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-else-if="state == 'result'">
|
|
||||||
<div v-for="(val, key) in result">
|
|
||||||
<h2><reg-object v-bind:link="key"></reg-object></h2>
|
|
||||||
<div style="padding-left: 2em">
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr><th scope="col">Key</th><th scope="col">Value</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="a in val.Attributes">
|
|
||||||
<th scope="row" class="text-primary" style="white-space:nowrap">{{ a[0] }}</th>
|
|
||||||
<td><reg-attribute v-bind:content="a[1]"></reg-attribute></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<section v-if="val.Backlinks.length != 0">
|
|
||||||
<p>Referenced by</p>
|
|
||||||
<div style="padding-left: 2em">
|
|
||||||
<table class="table">
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="r in val.Backlinks">
|
|
||||||
<td><reg-object v-bind:link="r"></reg-object></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<textarea class="d-none" ref="permalink">
|
|
||||||
{{ permalink }}
|
|
||||||
</textarea>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<footer class="page-footer font-small">
|
<footer class="page-footer font-small">
|
||||||
<div style="margin-top: 20px; padding: 5px">
|
<div style="margin-top: 20px; padding: 5px">
|
||||||
@ -147,33 +39,159 @@ Powered by
|
|||||||
<a href="http://alexcorvi.github.io/anchorme.js/">Anchorme</a>.
|
<a href="http://alexcorvi.github.io/anchorme.js/">Anchorme</a>.
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<script type="text/x-template" id="search-input-template">
|
||||||
|
<form class="form-inline" id="SearchForm">
|
||||||
|
<input v-bind:value="search"
|
||||||
|
v-on:input="debounceSearch($event.target.value)"
|
||||||
|
class="form-control-lg" size="30" type="search"
|
||||||
|
placeholder="Search the registry" aria-label="Search"/>
|
||||||
|
</form>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/x-template" id="app-root-template">
|
||||||
|
<div class="p-3">
|
||||||
|
<div class="jumbotron">
|
||||||
|
<h1>DN42 Registry Explorer</h1>
|
||||||
|
<p class="lead">Just start typing in the search box to start searching the registry</p>
|
||||||
|
<hr/>
|
||||||
|
<p>
|
||||||
|
<p>Search Tips</p>
|
||||||
|
<ul>
|
||||||
|
<li>Searches are case independent
|
||||||
|
<li>No need to hit enter, searches will start immediately
|
||||||
|
<li>Prefixing the search by a registry type followed by / will narrow the search to
|
||||||
|
just that type (e.g. <router-link class="text-success"
|
||||||
|
to="domain/.dn42">domain/.dn42</router-link>)</li>
|
||||||
|
<li>Searching for <b>type/</b> will return all the objects for that type (e.g.
|
||||||
|
<router-link class="text-success" to="schema/">schema/</router-link>)</li>
|
||||||
|
<li>A blank search box will return you to these instructions</li>
|
||||||
|
<li>Just copy to the URL to link to search results</li>
|
||||||
|
<li>Searches are made on object names; searching the content of objects
|
||||||
|
is not supported (yet!).</li>
|
||||||
|
</ul>
|
||||||
|
<hr/>
|
||||||
|
<p>
|
||||||
|
The registry explorer is a simple web app using
|
||||||
|
<a href="https://git.dn42.us/burble/dn42regsrv">dn42regsrv</a>;
|
||||||
|
a REST API for the DN42 registry built with <a href="https://golang.org/">Go</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<registry-stats></registry-stats>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
<script type="text/x-template" id="registry-stats-template">
|
<script type="text/x-template" id="registry-stats-template">
|
||||||
<div class="container d-flex flex-column w-75">
|
<div class="container d-flex flex-column w-75">
|
||||||
<h5>Registry Stats</h5>
|
<h5>Registry Stats</h5>
|
||||||
<section v-if="state == 'error'">
|
<section v-if="state == 'error'">
|
||||||
<div class="alert alert-primary clearfix" role="alert">
|
<div class="alert alert-warning clearfix" role="alert">
|
||||||
An error recurred whilst retrieving data <button type="button" class="float-right btn btn-warning" v-on:click="reload"><span class="material-icons">refresh</span> Refresh</button>
|
An error recurred whilst retrieving data from the API
|
||||||
|
<button type="button" class="float-right btn btn-primary"
|
||||||
|
v-on:click="reload"><span class="material-icons">refresh</span>
|
||||||
|
Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section v-else-if="state == 'loading'">
|
<section v-else-if="state == 'loading'">
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="alert alert-info" role="alert">Loading data ...</div>
|
||||||
Loading data ...
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
<section v-else>
|
<section v-else>
|
||||||
<p style="padding:1em;text-align:center">
|
<p class="p-3 text-center">
|
||||||
<span v-for="(value, key) in types" style="margin-left:0.5em;margin-right:0.5em;white-space:nowrap;display:inline-block"><a v-on:click="updateSearch(key + '/')" class="text-success">{{ key }}</a>: <b>{{ value }}</b> records</span>
|
<span v-for="(value, key) in store.RegStats"
|
||||||
|
class="mx-2 text-nowrap d-inline-block">
|
||||||
|
<router-link v-bind:to="'/'+key+'/'">{{ key }}</router-link>
|
||||||
|
: <b>{{ value }}</b>
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script type="text/x-template" id="app-search-template">
|
||||||
|
<div class="p-3">
|
||||||
|
|
||||||
|
<section v-if="state == 'loading'">
|
||||||
|
<div class="alert alert-info" role="alert">Loading data ...</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else-if="state == 'error'">
|
||||||
|
<div class="alert alert-warning clearfix" role="alert">
|
||||||
|
An error recurred whilst retrieving search results
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else-if="state == 'noresults'">
|
||||||
|
<h2>Searching for "{{ search }}" ...</h2>
|
||||||
|
<div class="alert alert-dark" role="alert">
|
||||||
|
Sorry, no results found
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else-if="state == 'resultlist'">
|
||||||
|
<h2>Listing results for "{{ search }}" ...</h2>
|
||||||
|
<div class="container d-flex flex-row flex-wrap">
|
||||||
|
<div class="text-center">
|
||||||
|
<span v-for="ref in result">
|
||||||
|
<reg-object class="mx-4 my-2" v-bind:link="ref[0]+'/'+ref[1]"></reg-object>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else-if="state == 'result'">
|
||||||
|
<div v-for="(val, key) in result">
|
||||||
|
<h2 class="mb-4"><reg-object v-bind:link="key"></reg-object></h2>
|
||||||
|
<div class="pl-4">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr><th scope="col">Key</th><th scope="col">Value</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="a in val.Attributes">
|
||||||
|
<th scope="row" class="border-0 py-1 pr-3 text-primary text-nowrap">
|
||||||
|
{{ a[0] }}
|
||||||
|
</th>
|
||||||
|
<td class="border-0 p-1">
|
||||||
|
<reg-attribute v-bind:content="a[1]"></reg-attribute>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<section v-if="val.Backlinks.length != 0">
|
||||||
|
<p>Referenced by</p>
|
||||||
|
<div class="pl-4">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="rlink in val.Backlinks">
|
||||||
|
<td class="border-0 p-1">
|
||||||
|
<reg-object v-bind:link="rlink"></reg-object>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else>
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
Something went wrong, an invalid search state was encountered ({{ state }})
|
||||||
|
<p>{{ $route.params.pathMatch }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
<script type="text/x-template" id="reg-object-template">
|
<script type="text/x-template" id="reg-object-template">
|
||||||
<span class="regref"><a v-on:click="updateSearch(rtype + '/' + obj)" class="text-success"
|
<span class="text-nowrap d-inline-block">
|
||||||
style="margin-right: 0.4em">{{ obj }}</a> <span
|
<router-link class="text-success mr-2" v-bind:to="'/'+link">
|
||||||
class="badge badge-pill badge-dark text-muted">{{ rtype }}</span></span></span>
|
{{ ( content ? content : obj ) }}
|
||||||
|
</router-link>
|
||||||
|
<span class="badge back-pill badge-dark test-muted">{{ rtype }}</span>
|
||||||
|
</span>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/x-template" id="reg-attribute-template">
|
<script type="text/x-template" id="reg-attribute-template">
|
||||||
@ -184,11 +202,12 @@ class="badge badge-pill badge-dark text-muted">{{ rtype }}</span></span></span>
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous"></script>
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.2/dist/vue.js"></script>
|
<script src="https://unpkg.com/vue"></script -->
|
||||||
<!-- <script src="https://cdn.jsdelivr.net/npm/vue@2.6.2/dist/vue.min.js"></script> -->
|
<script src="https://unpkg.com/vue-router"></script -->
|
||||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
<script src="https://unpkg.com/axios@0.18.0/dist/axios.min.js"></script>
|
||||||
|
<!-- script src="https://unpkg.com/vue@2.6.2/dist/vue.min.js"></script -->
|
||||||
|
<!-- script src="https://unpkg.com/vue-router@3.0.2/dist/vue-router.js"></script -->
|
||||||
<script src="anchorme.min.js"></script>
|
<script src="anchorme.min.js"></script>
|
||||||
<script src="explorer.js"></script>
|
<script src="explorer.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
51
registry.go
51
registry.go
@ -52,6 +52,7 @@ type RegAttributeSchema struct {
|
|||||||
type RegTypeSchema struct {
|
type RegTypeSchema struct {
|
||||||
Ref string
|
Ref string
|
||||||
Attributes map[string]*RegAttributeSchema
|
Attributes map[string]*RegAttributeSchema
|
||||||
|
KeyIndex map[string]map[*RegObject][]*RegAttribute
|
||||||
}
|
}
|
||||||
|
|
||||||
// the registry itself
|
// the registry itself
|
||||||
@ -78,10 +79,7 @@ func RegistryMakePath(t string, o string) string {
|
|||||||
|
|
||||||
// attribute functions
|
// attribute functions
|
||||||
|
|
||||||
// return a pointer to a RegType from a decorated attribute value
|
// nothing here
|
||||||
func (*RegAttribute) ExtractRegType() *RegType {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// object functions
|
// object functions
|
||||||
|
|
||||||
@ -144,6 +142,40 @@ func (schema *RegTypeSchema) validate(attributes []*RegAttribute) []*RegAttribut
|
|||||||
return validated
|
return validated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add an attribute to the key map
|
||||||
|
func (schema *RegTypeSchema) addKeyIndex(object *RegObject,
|
||||||
|
attribute *RegAttribute) {
|
||||||
|
|
||||||
|
objmap := schema.KeyIndex[attribute.Key]
|
||||||
|
// create a new object map if it didn't exist
|
||||||
|
if objmap == nil {
|
||||||
|
objmap = make(map[*RegObject][]*RegAttribute)
|
||||||
|
schema.KeyIndex[attribute.Key] = objmap
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the object/attribute reference
|
||||||
|
objmap[object] = append(objmap[object], attribute)
|
||||||
|
}
|
||||||
|
|
||||||
|
// object functions
|
||||||
|
|
||||||
|
// add a backlink to an object
|
||||||
|
func (object *RegObject) addBacklink(ref *RegObject) {
|
||||||
|
|
||||||
|
// check if the backlink already exists, this could be the case
|
||||||
|
// if an object is referenced multiple times (e.g. admin-c & tech-c)
|
||||||
|
for _, blink := range object.Backlinks {
|
||||||
|
if blink == ref {
|
||||||
|
// already exists, just return as nothing to do
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// didn't find a match, add the backlink
|
||||||
|
object.Backlinks = append(object.Backlinks, ref)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
// reload the registry
|
// reload the registry
|
||||||
|
|
||||||
@ -363,6 +395,7 @@ func (registry *Registry) parseSchema() {
|
|||||||
typeSchema := &RegTypeSchema{
|
typeSchema := &RegTypeSchema{
|
||||||
Ref: typeName,
|
Ref: typeName,
|
||||||
Attributes: make(map[string]*RegAttributeSchema),
|
Attributes: make(map[string]*RegAttributeSchema),
|
||||||
|
KeyIndex: make(map[string]map[*RegObject][]*RegAttribute),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure the type exists
|
// ensure the type exists
|
||||||
@ -455,11 +488,14 @@ func (registry *Registry) decorate() {
|
|||||||
for _, attribute := range object.Data {
|
for _, attribute := range object.Data {
|
||||||
cattribs += 1
|
cattribs += 1
|
||||||
|
|
||||||
|
// add this attribute to the key map
|
||||||
|
schema.addKeyIndex(object, attribute)
|
||||||
|
|
||||||
attribSchema := schema.Attributes[attribute.Key]
|
attribSchema := schema.Attributes[attribute.Key]
|
||||||
// are there relations defined for this attribute ?
|
// are there relations defined for this attribute ?
|
||||||
// attribSchema may be null if this attribute is user defined (x-*)
|
// attribSchema may be null if this attribute is user defined (x-*)
|
||||||
if (attribSchema != nil) && attribute.matchRelation(object,
|
if (attribSchema != nil) &&
|
||||||
attribSchema.Relations) {
|
attribute.matchRelation(object, attribSchema.Relations) {
|
||||||
// matched
|
// matched
|
||||||
cmatched += 1
|
cmatched += 1
|
||||||
} else {
|
} else {
|
||||||
@ -500,7 +536,8 @@ func (attribute *RegAttribute) matchRelation(parent *RegObject,
|
|||||||
attribute.RawValue, object.Ref)
|
attribute.RawValue, object.Ref)
|
||||||
|
|
||||||
// and add a back reference to the related object
|
// and add a back reference to the related object
|
||||||
object.Backlinks = append(object.Backlinks, parent)
|
object.addBacklink(parent)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user