Skip to content

Commit

Permalink
webui: Allow embedding visualisations on external sites
Browse files Browse the repository at this point in the history
Add a new read-only way of rendering visualisations without any of the
editor and page layout around it, allowing for embedding the
visualisation on external pages using an iframe.

Also add a new button in the visualisation editor which produces the
HTML code the user needs to embed the chart.
  • Loading branch information
MKleusberg committed Dec 20, 2023
1 parent c67a81c commit 67ddc83
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 3 deletions.
14 changes: 13 additions & 1 deletion webui/jsx/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import MarkdownEditor from "./markdown-editor";
import ProfilePage from "./profile-page";
import SqlTerminal from "./sql-terminal";
import UserPage from "./user-page";
import VisualisationEditor from "./visualisation-editor";
import { Visualisation, VisualisationEditor } from "./visualisation-editor";

{
const rootNode = document.getElementById("db-header-root");
Expand Down Expand Up @@ -177,6 +177,18 @@ import VisualisationEditor from "./visualisation-editor";
}
}

{
const rootNode = document.getElementById("visualisation");
if (rootNode) {
const name = rootNode.dataset.name;
const plotConfig = window[rootNode.dataset.plotConfig];
const branch = rootNode.dataset.branch;

const root = ReactDOM.createRoot(rootNode);
root.render(<Visualisation name={name} plotConfig={plotConfig} branch={branch} />);
}
}

{
const rootNode = document.getElementById("visualisation-editor");
if (rootNode) {
Expand Down
17 changes: 15 additions & 2 deletions webui/jsx/visualisation-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function SavedVisualisations({visualisations, visualisationsStatus, preselectedV
);
}

function Visualisation({name, plotConfig, branch, setRawData, setLastRunResultMessage}) {
export function Visualisation({name, plotConfig, branch, setRawData, setLastRunResultMessage}) {
const [state, setState] = React.useState("new");
const [data, setData] = React.useState(null);

Expand Down Expand Up @@ -240,18 +240,20 @@ function Visualisation({name, plotConfig, branch, setRawData, setLastRunResultMe
}
}

export default function VisualisationEditor() {
export function VisualisationEditor() {
const [selectedBranch, setSelectedBranch] = React.useState(meta.branch);
const [visualisations, setVisualisations] = React.useState(visualisationsData);
const [visualisationsStatus, setVisualisationsStatus] = React.useState(Object.fromEntries(Object.keys(visualisationsData).map(k => [k, {dirty: false, newlyCreated: false, code: visualisationsData[k].sql}])));
const [selectedVisualisation, setSelectedVisualisation] = React.useState("");
const [rawData, setRawData] = React.useState(null);
const [showDataTable, setShowDataTable] = React.useState(false);
const [showEmbedHtml, setShowEmbedHtml] = React.useState(false);
const [lastRunResultMessage, setLastRunResultMessage] = React.useState(null);

// When the selected saved visualisation is changed, update the chart settings controls
React.useEffect(() => {
setRawData(null);
setShowEmbedHtml(false);
}, [selectedVisualisation]);

// When the data is updated, check if the currently selected plot columns still exist
Expand Down Expand Up @@ -605,6 +607,9 @@ export default function VisualisationEditor() {
{rawData !== null ? (<>
<button type="button" className={"btn btn-default" + (showDataTable ? " active" : "")} onClick={() => setShowDataTable(!showDataTable)}>{showDataTable ? "Hide data table" : "Show data table"}</button>&nbsp;
<button type="button" className="btn btn-default" onClick={() => exportDataTable()}>Export to CSV</button>
{visualisationsStatus[selectedVisualisation].newlyCreated ? null :
<>&nbsp;<button type="button" className={"btn btn-default" + (showEmbedHtml ? " active" : "")} onClick={() => setShowEmbedHtml(!showEmbedHtml)}>{showEmbedHtml ? "Hide embedding" : "Embed chart"}</button></>
}
</>) : null}
</TabPanel>
<TabPanel>
Expand Down Expand Up @@ -696,6 +701,14 @@ export default function VisualisationEditor() {
</>) : null}
</div>
) : null}
{showEmbedHtml ? (
<div style={{marginTop: "1em"}}>
<h5>You can embed the chart in other web pages by using this HTML code. Please keep in mind that renaming or deleting your visualisation is going to break the embedding.</h5>
<code>
&lt;iframe width="425" height="350" src={"\"" + window.location.origin + "/visembed/" + meta.owner + "/" + meta.database + "?visname=" + selectedVisualisation + "\""} title={selectedVisualisation + " - DBHub.io visualisation"} style="border: 1px solid black"&gt;&lt;/iframe&gt;
</code>
</div>
) : null}
</>)}
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions webui/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3226,6 +3226,7 @@ func main() {
http.Handle("/updates/", gz.GzipHandler(logReq(updatesPage)))
http.Handle("/upload/", gz.GzipHandler(logReq(uploadPage)))
http.Handle("/vis/", gz.GzipHandler(logReq(visualisePage)))
http.Handle("/visembed/", gz.GzipHandler(logReq(visEmbedPage)))
http.Handle("/watchers/", gz.GzipHandler(logReq(watchersPage)))
http.Handle("/x/apikeygen", gz.GzipHandler(logReq(apiKeyGenHandler)))
http.Handle("/x/branchnames", gz.GzipHandler(logReq(branchNamesHandler)))
Expand Down
33 changes: 33 additions & 0 deletions webui/templates/visembed.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[[ define "visembedPage" ]]
<!doctype html>
<html>
[[ template "head" . ]]
<body>
<div>
<div id="visualisation" data-name="[[ .VisName ]]" data-plot-config="visualisationData" data-branch="[[ .DB.Info.Branch ]]"></div>
<div>
<div class="col-md-1"></div>
<div class="col-md-5">
<span class="pull-left">
View full dataset at <a href="/[[ .DB.Info.Owner ]]/[[ .DB.Info.Database ]]">[[ .DB.Info.Owner ]] / [[ .DB.Info.Database ]]</a>
</span>
</div>
<div class="col-md-5">
<div class="pull-right">
Powered by <a href="/"><img src="/images/sqlitebrowser.svg" height="15"/>
<span style="vertical-align: bottom; color: black; padding-top: 3px;">DBHub.io</span>
</a>
</div>
</div>
<div class="col-md-1"></div>
</div>
</div>
[[ template "script_db_header" . ]]
<script>
var visualisationData = [[ .Visualisation ]];
var branchData = [[ .Branches ]];
</script>
<script src="/js/dbhub.js"></script>
</body>
</html>
[[ end ]]
199 changes: 199 additions & 0 deletions webui/vis.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,205 @@ func visDel(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}

func visEmbedPage(w http.ResponseWriter, r *http.Request) {
var pageData struct {
DB com.SQLiteDBinfo
PageMeta PageMetaInfo
Branches map[string]com.BranchEntry
Visualisation com.VisParamsV2
VisName string
}

// Get all meta information
errCode, err := collectPageMetaInfo(r, &pageData.PageMeta)
if err != nil {
errorPage(w, r, errCode, err.Error())
return
}
dbName, err := getDatabaseName(r)
if err != nil {
errorPage(w, r, http.StatusBadRequest, err.Error())
return
}

// Check if a specific database commit ID was given
commitID, err := com.GetFormCommit(r)
if err != nil {
errorPage(w, r, http.StatusBadRequest, "Invalid database commit ID")
return
}

// Check if a branch name was requested
branchName, err := com.GetFormBranch(r)
if err != nil {
errorPage(w, r, http.StatusBadRequest, "Validation failed for branch name")
return
}

// Check if a named tag was requested
tagName, err := com.GetFormTag(r)
if err != nil {
errorPage(w, r, http.StatusBadRequest, "Validation failed for tag name")
return
}

// Check if a specific release was requested
releaseName := r.FormValue("release")
if releaseName != "" {
err = com.ValidateBranchName(releaseName)
if err != nil {
errorPage(w, r, http.StatusBadRequest, "Validation failed for release name")
return
}
}

// Check if the database exists and the user has access to view it
exists, err := com.CheckDBPermissions(pageData.PageMeta.LoggedInUser, dbName.Owner, dbName.Database, false)
if err != nil {
errorPage(w, r, http.StatusInternalServerError, err.Error())
return
}
if !exists {
errorPage(w, r, http.StatusNotFound, fmt.Sprintf("Database '%s%s%s' doesn't exist", dbName.Owner, "/",
dbName.Database))
return
}

// * Execution can only get here if the user has access to the requested database *

// Check if this is a live database
isLive, _, err := com.CheckDBLive(dbName.Owner, dbName.Database)
if err != nil {
errorPage(w, r, http.StatusInternalServerError, err.Error())
return
}

// Live databases are handled differently to standard ones
if !isLive {
// If a specific commit was requested, make sure it exists in the database commit history
if commitID != "" {
commitList, err := com.GetCommitList(dbName.Owner, dbName.Database)
if err != nil {
errorPage(w, r, http.StatusInternalServerError, err.Error())
return
}
if _, ok := commitList[commitID]; !ok {
// The requested commit isn't one in the database commit history so error out
errorPage(w, r, http.StatusNotFound, fmt.Sprintf("Unknown commit for database '%s/%s'", dbName.Owner,
dbName.Database))
return
}
}

// If a specific release was requested, and no commit ID was given, retrieve the commit ID matching the release
if commitID == "" && releaseName != "" {
releases, err := com.GetReleases(dbName.Owner, dbName.Database)
if err != nil {
errorPage(w, r, http.StatusInternalServerError, "Couldn't retrieve releases for database")
return
}
rls, ok := releases[releaseName]
if !ok {
errorPage(w, r, http.StatusInternalServerError, "Unknown release requested for this database")
return
}
commitID = rls.Commit
}

// Read the branch heads list from the database
pageData.Branches, err = com.GetBranches(dbName.Owner, dbName.Database)
if err != nil {
errorPage(w, r, http.StatusInternalServerError, err.Error())
return
}

// If a specific branch was requested and no commit ID was given, use the latest commit for the branch
if commitID == "" && branchName != "" {
c, ok := pageData.Branches[branchName]
if !ok {
errorPage(w, r, http.StatusInternalServerError, "Unknown branch requested for this database")
return
}
commitID = c.Commit
}

// If a specific tag was requested, and no commit ID was given, retrieve the commit ID matching the tag
if commitID == "" && tagName != "" {
tags, err := com.GetTags(dbName.Owner, dbName.Database)
if err != nil {
errorPage(w, r, http.StatusInternalServerError, "Couldn't retrieve tags for database")
return
}
tg, ok := tags[tagName]
if !ok {
errorPage(w, r, http.StatusInternalServerError, "Unknown tag requested for this database")
return
}
commitID = tg.Commit
}

// If we still haven't determined the required commit ID, use the head commit of the default branch
if commitID == "" {
commitID, err = com.DefaultCommit(dbName.Owner, dbName.Database)
if err != nil {
errorPage(w, r, http.StatusInternalServerError, err.Error())
return
}
}

// Retrieve default branch name details
if branchName == "" {
branchName, err = com.GetDefaultBranchName(dbName.Owner, dbName.Database)
if err != nil {
errorPage(w, r, http.StatusInternalServerError, "Error retrieving default branch name")
return
}
}

pageData.DB.Info.Branch = branchName
}

// Retrieve the database details
err = com.DBDetails(&pageData.DB, pageData.PageMeta.LoggedInUser, dbName.Owner, dbName.Database, commitID)
if err != nil {
errorPage(w, r, http.StatusBadRequest, err.Error())
return
}

// Initial sanity check of the visualisation name
pageData.VisName = r.FormValue("visname")
err = com.ValidateVisualisationName(pageData.VisName)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}

// Get a list of all saved visualisations for this database
visualisations, err := com.GetVisualisations(dbName.Owner, dbName.Database)
if err != nil {
errorPage(w, r, http.StatusInternalServerError, err.Error())
return
}

// Get visualisation data
var ok bool
pageData.Visualisation, ok = visualisations[pageData.VisName]
if ok == false {
errorPage(w, r, http.StatusNotFound, "visualisation not found")
return
}

// Page title
pageData.PageMeta.Title = fmt.Sprintf("Visualisation %s - %s %s %s", pageData.VisName, dbName.Owner, "/", dbName.Database)

// Render the visualisation page
t := tmpl.Lookup("visembedPage")
err = t.Execute(w, pageData)
if err != nil {
log.Printf("Error: %s", err)
}
}

// visExecuteSQL executes a custom SQLite SELECT query.
func visExecuteSQL(w http.ResponseWriter, r *http.Request) {
// Retrieve session data (if any)
Expand Down

0 comments on commit 67ddc83

Please sign in to comment.