panel/admin: Continue moving out information

This page further splits up the admin page into several parts.
This commit is contained in:
Tom Wiesing 2023-11-10 19:30:05 +01:00
parent ff92df3a87
commit 419902c59b
No known key found for this signature in database
12 changed files with 645 additions and 271 deletions

View file

@ -75,6 +75,10 @@ var (
menuGrants = component.DummyMenuItem()
menuPurge = component.DummyMenuItem()
menuSnapshots = component.DummyMenuItem()
menuSSH = component.DummyMenuItem()
menuStats = component.DummyMenuItem()
menuData = component.DummyMenuItem()
menuDrupal = component.DummyMenuItem()
)
func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http.Handler, err error) {
@ -145,6 +149,26 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http
router.Handler(http.MethodGet, route+"instance/:slug/snapshots", snapshots)
}
{
ssh := admin.instanceSSH(ctx)
router.Handler(http.MethodGet, route+"instance/:slug/ssh", ssh)
}
{
stats := admin.instanceStats(ctx)
router.Handler(http.MethodGet, route+"instance/:slug/stats", stats)
}
{
data := admin.instanceData(ctx)
router.Handler(http.MethodGet, route+"instance/:slug/data", data)
}
{
drupal := admin.instanceDrupal(ctx)
router.Handler(http.MethodGet, route+"instance/:slug/drupal", drupal)
}
// add a router for the login page
router.Handler(http.MethodPost, route+"login", admin.loginHandler(ctx))

View file

@ -7,8 +7,6 @@
<a class="pure-button pure-button-small" href="#overview">Info &amp; Status</a>
<a class="pure-button pure-button-small" href="#requirements">Drupal Status Report</a>
<a class="pure-button pure-button-small" href="#wisski">WissKI Data</a>
<a class="pure-button pure-button-small" href="#stats">Statistics</a>
<a class="pure-button pure-button-small" href="#ssh">SSH Keys</a>
</div>
</div>
@ -126,39 +124,6 @@
</div>
</div>
<div class="pure-u-1 pure-u-xl-2-5">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Drupal Info
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Drupal Version
</td>
<td>
<code>{{ .Info.DrupalVersion }}</code>
</td>
</tr>
<tr>
<td>
Default Theme
</td>
<td>
<code>{{ .Info.Theme }}</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-2-5">
<div class="padding">
@ -288,228 +253,3 @@
</div>
</div>
</div>
<div class="pure-u-1-1">
<h2 id="requirements">Drupal Status Report</h2>
</div>
<div class="pure-u-1">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th>
ID
</th>
<th>
Severity
</th>
<th>
Title
</th>
<th>
Value
</th>
<th>
Description
</th>
</tr>
</thead>
<tbody>
{{ range $name, $req := .Info.Requirements }}
<tr>
<td>
<code>{{ $req.ID }}</code>
</td>
<td>
{{ $req.Level }}
</td>
<td>
{{ $req.Title }}
</td>
<td>
{{ $req.Value }}
</td>
<td>
{{ $req.Description }}
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1-1">
<h2 id="wisski">WissKI Data</h2>
</div>
<div class="pure-u-1 pure-u-xl-1-2">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Pathbuilders
</th>
</tr>
</thead>
<tbody>
{{ range $name, $xml := .Info.Pathbuilders }}
<tr>
<td>
<code>{{ $name }}</code>
</td>
<td>
<code class="pathbuilder" data-name="{{ $name }}">{{ $xml }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-1-2">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th>
URI Prefixes
{{ if .Info.NoPrefixes }}
(excluded from resolver)
{{ end }}
</th>
</tr>
</thead>
<tbody>
{{ range $index, $prefix := .Info.Prefixes }}
<tr>
<td>
<code>{{ $prefix }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1-1">
<h2 id="stats">Statistics</h2>
</div>
<div class="pure-u-1 pure-u-xl-1-2">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="5">
Bundles
</th>
</tr>
<tr>
<th>
Label
</th>
<th>
Machine Name
</th>
<th>
Count
</th>
<th>
LastEdit
</th>
<th>
MainBundle
</th>
</tr>
</thead>
<tbody>
{{ range .Info.Statistics.Bundles.Bundles }}
<tr>
<td>
<code>{{ .Label }}</code>
</td>
<td>
<code>{{ .MachineName }}</code>
</td>
<td>
<code>{{ .Count }}</code>
</td>
<td>
<code class="date">{{ .LastEdit.Time.Format "2006-01-02T15:04:05Z07:00" }}</code>
</td>
<td>
<code>{{ .MainBundle }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-1-2">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Triplestore
</th>
</tr>
<tr>
<th>
URI
</th>
<th>
Count
</th>
</tr>
</thead>
<tbody>
{{ range .Info.Statistics.Triplestore.Graphs }}
<tr>
<td>
<code>{{ .URI }}</code>
</td>
<td>
<code>{{ .Count }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1-1">
<h2 id="ssh">SSH Keys</h2>
<table class="pure-table pure-table-bordered padding">
<tbody>
{{ range .Info.SSHKeys }}
<tr>
<td>
<code>{{ . }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>

View file

@ -0,0 +1,61 @@
<div class="pure-u-1-1">
<h2 id="wisski">WissKI Data</h2>
</div>
<div class="pure-u-1 pure-u-xl-1-2">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Pathbuilders
</th>
</tr>
</thead>
<tbody>
{{ range $name, $xml := .Pathbuilders }}
<tr>
<td>
<code>{{ $name }}</code>
</td>
<td>
<code class="pathbuilder" data-name="{{ $name }}">{{ $xml }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-1-2">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th>
URI Prefixes
{{ if .NoPrefixes }}
(excluded from resolver)
{{ end }}
</th>
</tr>
</thead>
<tbody>
{{ range $index, $prefix := .Prefixes }}
<tr>
<td>
<code>{{ $prefix }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>

View file

@ -0,0 +1,94 @@
<div class="pure-u-1-1">
<h2 id="info">Drupal Information</h2>
</div>
<div class="pure-u-1-1">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Drupal Info
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Drupal Version
</td>
<td>
<code>{{ .DrupalVersion }}</code>
</td>
</tr>
<tr>
<td>
Default Theme
</td>
<td>
<code>{{ .DefaultTheme }}</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1-1">
<h2 id="requirements">Status Report</h2>
<p>
This mirrors the <em>Status Report</em> page found inside drupal.
</p>
</div>
<div class="pure-u-1">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th>
ID
</th>
<th>
Severity
</th>
<th>
Title
</th>
<th>
Value
</th>
<th>
Description
</th>
</tr>
</thead>
<tbody>
{{ range $name, $req := .Requirements }}
<tr>
<td>
<code>{{ $req.ID }}</code>
</td>
<td>
{{ $req.Level }}
</td>
<td>
{{ $req.Title }}
</td>
<td>
{{ $req.Value }}
</td>
<td>
{{ $req.Description }}
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>

View file

@ -0,0 +1,38 @@
<div class="pure-u-1-1">
<h2>SSH Access</h2>
<p>
Every WissKI Instance comes with access via SSH.
Every distillery user that has administrative access to this instance can access it.
Furthermore, every distillery administrator has access.
</p>
<p>
To access the ssh server of this instance, use:
</p>
<code class="copy">
<pre>ssh -J {{ .PanelDomain }}:{{ .Port }} www-data@{{ .Hostname }}</pre>
</code>
<p>
For this to work, configure an ssh key on your <a href="/user/ssh">Personal SSH Page</a>.
</p>
</div>
<div class="pure-u-1-1">
<h2>SSH Keys</h2>
<p>
This page lists all SSH Keys that have access to this system.
</p>
</div>
<div class="pure-u-1">
<table class="pure-table pure-table-bordered padding">
<tbody>
{{ range .SSHKeys }}
<tr>
<td>
<code>{{ . }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>

View file

@ -0,0 +1,98 @@
<div class="pure-u-1-1">
<p>
This page contains statistics generated by the <em>WissKI Statistics</em> module.
If the module is not available, this page may be empty.
</p>
</div>
<div class="pure-u-1-1">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="5">
Bundles
</th>
</tr>
<tr>
<th>
Label
</th>
<th>
Machine Name
</th>
<th>
Count
</th>
<th>
LastEdit
</th>
<th>
MainBundle
</th>
</tr>
</thead>
<tbody>
{{ range .Statistics.Bundles.Bundles }}
<tr>
<td>
<code>{{ .Label }}</code>
</td>
<td>
<code>{{ .MachineName }}</code>
</td>
<td>
<code>{{ .Count }}</code>
</td>
<td>
<code class="date">{{ .LastEdit.Time.Format "2006-01-02T15:04:05Z07:00" }}</code>
</td>
<td>
<code>{{ .MainBundle }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1-1">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Triplestore
</th>
</tr>
<tr>
<th>
URI
</th>
<th>
Count
</th>
</tr>
</thead>
<tbody>
{{ range .Statistics.Triplestore.Graphs }}
<tr>
<td>
<code>{{ .URI }}</code>
</td>
<td>
<code>{{ .Count }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>

View file

@ -79,19 +79,12 @@ func (admin *Admin) instanceTabs(slug string, active string) templating.FlagFunc
{Title: "Overview", Path: template.URL("/admin/instance/" + slug), Active: active == "overview"},
{Title: "Rebuild", Path: template.URL("/admin/instance/" + slug + "/rebuild"), Active: active == "rebuild"},
{Title: "Users & Grants", Path: template.URL("/admin/instance/" + slug + "/users"), Active: active == "users"},
{Title: "Drupal Status", Path: template.URL("/admin/instance/" + slug + "/drupal"), Active: active == "drupal"},
{Title: "WissKI Data", Path: template.URL("/admin/instance/" + slug + "/data"), Active: active == "data"},
{Title: "WissKI Stats", Path: template.URL("/admin/instance/" + slug + "/stats"), Active: active == "stats"},
{Title: "SSH", Path: template.URL("/admin/instance/" + slug + "/ssh"), Active: active == "ssh"},
{Title: "Snapshots", Path: template.URL("/admin/instance/" + slug + "/snapshots"), Active: active == "snapshots"},
{Title: "Purge", Path: template.URL("/admin/instance/" + slug + "/purge"), Active: active == "purge"},
// TODO: These still need to be migrated to their own tabs
// Then we also need to redo the main page
/*
{Title: "Status", Path: template.URL("/instance/" + slug + "/status"), Active: active == "status"},
{Title: "Database", Path: template.URL("/instance/" + slug + "/database"), Active: active == "database"},
{Title: "Drupal", Path: template.URL("/instance/" + slug + "/drupal"), Active: active == "drupal"},
{Title: "Users & Grants", Path: template.URL("/instance/" + slug + "/users"), Active: active == "users"},
{Title: "Stats", Path: template.URL("/instance/" + slug + "/stats"), Active: active == "stats"},
{Title: "SSH", Path: template.URL("/instance/" + slug + "/ssh"), Active: active == "ssh"},
*/
}
return flags
}

View file

@ -0,0 +1,77 @@
package admin
import (
"context"
_ "embed"
"html/template"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/tkw1536/pkglib/httpx"
"github.com/julienschmidt/httprouter"
)
//go:embed "html/instance_data.html"
var instanceDataHTML []byte
var instanceDataTemplate = templating.Parse[instanceDataContext](
"instance_data.html", instanceDataHTML, nil,
templating.Assets(assets.AssetsAdmin),
)
type instanceDataContext struct {
templating.RuntimeFlags
Instance *wisski.WissKI
Pathbuilders map[string]string
NoPrefixes bool
Prefixes []string
}
func (admin *Admin) instanceData(ctx context.Context) http.Handler {
tpl := instanceDataTemplate.Prepare(
admin.dependencies.Templating,
templating.Crumbs(
menuAdmin,
menuInstances,
menuInstance,
menuData,
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ctx instanceDataContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
// setup the context with just the instance
ctx.Instance, err = admin.dependencies.Instances.WissKI(r.Context(), slug)
if err != nil {
return ctx, nil, httpx.ErrNotFound
}
server := ctx.Instance.PHP().NewServer()
defer server.Close()
ctx.Pathbuilders, err = ctx.Instance.Pathbuilder().GetAll(r.Context(), server)
if err != nil {
return ctx, nil, httpx.ErrInternalServerError
}
prefixes := ctx.Instance.Prefixes()
ctx.NoPrefixes = prefixes.NoPrefix()
ctx.Prefixes, err = prefixes.All(r.Context(), server)
if err != nil {
return ctx, nil, httpx.ErrInternalServerError
}
return ctx, []templating.FlagFunc{
templating.ReplaceCrumb(menuInstance, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + ctx.Instance.Slug)}),
templating.ReplaceCrumb(menuData, component.MenuItem{Title: "SSH", Path: template.URL("/admin/instance/" + ctx.Instance.Slug + "/data")}),
templating.Title(ctx.Instance.Slug + " - Data"),
admin.instanceTabs(slug, "data"),
}, nil
})
}

View file

@ -0,0 +1,86 @@
package admin
import (
"context"
_ "embed"
"html/template"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/tkw1536/pkglib/httpx"
"github.com/julienschmidt/httprouter"
)
//go:embed "html/instance_drupal.html"
var instanceDrupalHTML []byte
var instanceDrupalTemplate = templating.Parse[instanceDrupalContext](
"instance_drupal.html", instanceDrupalHTML, nil,
templating.Assets(assets.AssetsAdmin),
)
type instanceDrupalContext struct {
templating.RuntimeFlags
Instance *wisski.WissKI
DrupalVersion string
DefaultTheme string
Requirements []status.Requirement
}
func (admin *Admin) instanceDrupal(ctx context.Context) http.Handler {
tpl := instanceDrupalTemplate.Prepare(
admin.dependencies.Templating,
templating.Crumbs(
menuAdmin,
menuInstances,
menuInstance,
menuDrupal,
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ctx instanceDrupalContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
// setup the context with just the instance
ctx.Instance, err = admin.dependencies.Instances.WissKI(r.Context(), slug)
if err != nil {
return ctx, nil, httpx.ErrNotFound
}
server := ctx.Instance.PHP().NewServer()
defer server.Close()
// get the requirements
ctx.Requirements, err = ctx.Instance.Requirements().Get(r.Context(), server)
if err != nil {
return ctx, nil, httpx.ErrInternalServerError
}
// get the drupal version
ctx.DrupalVersion, err = ctx.Instance.Version().Get(r.Context(), server)
if err != nil {
return ctx, nil, httpx.ErrInternalServerError
}
// get the default theme
ctx.DefaultTheme, err = ctx.Instance.Theme().Get(r.Context(), server)
if err != nil {
return ctx, nil, httpx.ErrInternalServerError
}
return ctx, []templating.FlagFunc{
templating.ReplaceCrumb(menuInstance, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + ctx.Instance.Slug)}),
templating.ReplaceCrumb(menuDrupal, component.MenuItem{Title: "Drupal Status", Path: template.URL("/admin/instance/" + ctx.Instance.Slug + "/drupal")}),
templating.Title(ctx.Instance.Slug + " - Drupal Status"),
admin.instanceTabs(slug, "drupal"),
}, nil
})
}

View file

@ -0,0 +1,80 @@
package admin
import (
"context"
_ "embed"
"html/template"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/tkw1536/pkglib/httpx"
"github.com/julienschmidt/httprouter"
gossh "golang.org/x/crypto/ssh"
)
//go:embed "html/instance_ssh.html"
var instanceSSHHTML []byte
var instanceSSHTemplate = templating.Parse[instanceSSHContext](
"instance_ssh.html", instanceSSHHTML, nil,
templating.Assets(assets.AssetsAdmin),
)
type instanceSSHContext struct {
templating.RuntimeFlags
Instance *wisski.WissKI
SSHKeys []string
Hostname string
PanelDomain string
Port uint16
}
func (admin *Admin) instanceSSH(ctx context.Context) http.Handler {
tpl := instanceSSHTemplate.Prepare(
admin.dependencies.Templating,
templating.Crumbs(
menuAdmin,
menuInstances,
menuInstance,
menuSSH,
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ctx instanceSSHContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
// setup the context with just the instance
ctx.Instance, err = admin.dependencies.Instances.WissKI(r.Context(), slug)
if err != nil {
return ctx, nil, httpx.ErrNotFound
}
ctx.Hostname = ctx.Instance.Domain()
ctx.PanelDomain = admin.Config.HTTP.PanelDomain()
ctx.Port = admin.Config.Listen.SSHPort
keys, err := ctx.Instance.SSH().Keys(r.Context())
if err != nil {
return ctx, nil, httpx.ErrInternalServerError
}
ctx.SSHKeys = make([]string, len(keys))
for i, key := range keys {
ctx.SSHKeys[i] = string(gossh.MarshalAuthorizedKey(key))
}
return ctx, []templating.FlagFunc{
templating.ReplaceCrumb(menuInstance, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + ctx.Instance.Slug)}),
templating.ReplaceCrumb(menuSSH, component.MenuItem{Title: "SSH", Path: template.URL("/admin/instance/" + ctx.Instance.Slug + "/ssh")}),
templating.Title(ctx.Instance.Slug + " - SSH"),
admin.instanceTabs(slug, "ssh"),
}, nil
})
}

View file

@ -0,0 +1,67 @@
package admin
import (
"context"
_ "embed"
"html/template"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/tkw1536/pkglib/httpx"
"github.com/julienschmidt/httprouter"
)
//go:embed "html/instance_stats.html"
var instanceStatsHTML []byte
var instanceStatsTemplate = templating.Parse[instanceStatsContext](
"instance_stats.html", instanceStatsHTML, nil,
templating.Assets(assets.AssetsAdmin),
)
type instanceStatsContext struct {
templating.RuntimeFlags
Instance *wisski.WissKI
Statistics status.Statistics
}
func (admin *Admin) instanceStats(ctx context.Context) http.Handler {
tpl := instanceStatsTemplate.Prepare(
admin.dependencies.Templating,
templating.Crumbs(
menuAdmin,
menuInstances,
menuInstance,
menuStats,
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ctx instanceStatsContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
// setup the context with just the instance
ctx.Instance, err = admin.dependencies.Instances.WissKI(r.Context(), slug)
if err != nil {
return ctx, nil, httpx.ErrNotFound
}
// read statistics
ctx.Statistics, err = ctx.Instance.Stats().Get(r.Context(), nil)
if err != nil {
return ctx, nil, httpx.ErrInternalServerError
}
return ctx, []templating.FlagFunc{
templating.ReplaceCrumb(menuInstance, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + ctx.Instance.Slug)}),
templating.ReplaceCrumb(menuStats, component.MenuItem{Title: "SSH", Path: template.URL("/admin/instance/" + ctx.Instance.Slug + "/stats")}),
templating.Title(ctx.Instance.Slug + " - Stats"),
admin.instanceTabs(slug, "stats"),
}, nil
})
}

View file

@ -122,6 +122,22 @@ func (wisski *WissKI) Blocks() *extras.Blocks {
return export[*extras.Blocks](wisski)
}
func (wisski *WissKI) Stats() *extras.Stats {
return export[*extras.Stats](wisski)
}
func (wisski *WissKI) Requirements() *extras.Requirements {
return export[*extras.Requirements](wisski)
}
func (wisski *WissKI) Version() *extras.Version {
return export[*extras.Version](wisski)
}
func (wisski *WissKI) Theme() *extras.Theme {
return export[*extras.Theme](wisski)
}
//
// All components
// THESE SHOULD NEVER BE CALLED DIRECTLY