Skip to content

Commit c13a5a4

Browse files
committed
Frontend: Add kiosk mode
1 parent 62ba753 commit c13a5a4

File tree

8 files changed

+160
-51
lines changed

8 files changed

+160
-51
lines changed

JiraLib/Services/JiraGraphService.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -231,14 +231,17 @@ private async Task<string> CompileGraphToSvgViaDot(string graphvizSource)
231231
process.StandardError.Close();
232232
process.StandardOutput.Close();
233233

234-
process.Close();
235-
await process.WaitForExitAsync();
234+
if (!process.HasExited)
235+
{
236+
await process.WaitForExitAsync();
237+
}
236238

237239
if (process.ExitCode != 0) throw new Exception(error);
238240

239241
return output;
240242
}
241243

244+
242245
private async Task<Process> SendToDot(string graphSource, string arguments)
243246
{
244247
var process1 = new Process

JiraTools.Web.Api/JiraTools.Web.Api.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
</ItemGroup>
4343

4444
<ItemGroup>
45-
<TypeScriptCompile Remove="wwwroot\script.js" />
46-
<TypeScriptCompile Include="wwwroot\script.ts" />
45+
<TypeScriptCompile Remove="wwwroot\*.js" />
46+
<TypeScriptCompile Include="wwwroot\*.ts" />
4747
</ItemGroup>
4848

4949
</Project>
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Options } from "./options";
2+
3+
let graphviz: any | null = null;
4+
async function getGraphvizWasm(): Promise<any> {
5+
const response = await fetch("api/jiragraph/wasm-module");
6+
const script = await response.text();
7+
eval(script);
8+
const wasmModule: any = (window as { [key: string]: any })["@hpcc-js/wasm"] as any;
9+
return await wasmModule.Graphviz.load();
10+
}
11+
12+
export async function loadGraphvizWasm(): Promise<void> {
13+
graphviz = await getGraphvizWasm();
14+
console.log("Graphviz wasm module loaded", graphviz.version());
15+
}
16+
17+
export function graphvizLoaded():boolean {
18+
return graphviz != null;
19+
}
20+
21+
export async function dotSvg(dotCode: string): Promise<string> {
22+
return await graphviz.dot(dotCode, "svg");
23+
}
24+
25+
export async function dotPng(dotCode: string) {
26+
return await graphviz.dot(dotCode, "png");
27+
}
28+
29+
export async function getGraph(dto: Options) {
30+
const graphResponse = await fetch("/api/jiragraph", {
31+
method: "POST",
32+
headers: {
33+
"Content-Type": "application/json"
34+
},
35+
body: JSON.stringify(dto)
36+
});
37+
38+
if (graphResponse.ok) {
39+
return await graphResponse.text();
40+
} else {
41+
const error: any = (await graphResponse.json());
42+
console.log("Error", error);
43+
throw `${error.detail.replace("401 ()", "401 Invalid Jira Authorization")}`;
44+
}
45+
}
46+
47+
(window as any).loadGraphvizWasm = loadGraphvizWasm;
48+
loadGraphvizWasm();

JiraTools.Web.Api/wwwroot/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ <h1>Jira Graph</h1>
7272

7373
<input type="submit" id="SVG" value="SVG" title="Includes links on the issues, but doesn't care how big your screen is." />
7474
<!--<input type="submit" id="PNG" value="PNG" title="Size is ok, easier to copy the image, but doesn't include links to the issues."/>-->
75+
<input type="submit" id="KIOSK" value="Kiosk mode" />
7576
</div>
7677

7778

JiraTools.Web.Api/wwwroot/kiosk.html

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>JiraGraph</title>
7+
<style>
8+
/* This will make the SVG span the whole screen */
9+
svg {
10+
width: 100vw;
11+
height: 100vh;
12+
display: block;
13+
}
14+
</style>
15+
<script type="module" src="./kiosk.js"></script>
16+
</head>
17+
<body>
18+
<div id="svg-container"></div>
19+
</body>
20+
</html>

JiraTools.Web.Api/wwwroot/kiosk.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Options } from './options';
2+
import * as GraphService from './graphService.js';
3+
4+
let storedOptions: Options | null = null;
5+
const svgContainer = document.getElementById('svg-container');
6+
7+
function getOptionsFromStorage(): Options | null {
8+
const optionsString = sessionStorage.getItem('options');
9+
if (optionsString) {
10+
const options = JSON.parse(optionsString) as Options;
11+
options.OutputFormat = 'Svg';
12+
return options;
13+
}
14+
return null;
15+
}
16+
17+
async function updateGraph() {
18+
// If we haven't loaded options before, try to get them from storage
19+
if (!storedOptions) {
20+
storedOptions = getOptionsFromStorage();
21+
}
22+
23+
if (storedOptions) {
24+
try {
25+
const svgString = await createGraph(storedOptions);
26+
if (svgContainer) {
27+
svgContainer.innerHTML = svgString;
28+
}
29+
} catch (error) {
30+
if (svgContainer) {
31+
svgContainer.innerHTML = "<p>Error fetching graph. Please try again later.</p>";
32+
}
33+
console.error("Error fetching graph:", error);
34+
}
35+
} else {
36+
if (svgContainer) {
37+
svgContainer.innerHTML = "<p>Invalid or missing options. Please check the storage and try again.</p>";
38+
}
39+
}
40+
}
41+
42+
async function createGraph(dto: Options): Promise<string> {
43+
if (GraphService.graphvizLoaded()) {
44+
dto.OutputFormat = "Dot";
45+
const dotCode = await GraphService.getGraph(dto);
46+
return await GraphService.dotSvg(dotCode);
47+
} else {
48+
dto.OutputFormat = "Svg";
49+
return await GraphService.getGraph(dto);
50+
}
51+
}
52+
53+
// Initial call
54+
updateGraph();
55+
56+
// Set an interval to update the graph every minute
57+
setInterval(updateGraph, 60000);

JiraTools.Web.Api/wwwroot/script.ts

+23-45
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Options } from './options';
2+
import * as GraphService from './graphService.js';
23

34
const graphElement: HTMLImageElement = document.getElementById("graph") as HTMLImageElement;
45
console.log("Script loaded");
@@ -81,21 +82,7 @@ async function getGraph(dto: Options): Promise<string> {
8182

8283
graphElement.innerHTML = '';
8384

84-
const graphResponse = await fetch("/api/jiragraph", {
85-
method: "POST",
86-
headers: {
87-
"Content-Type": "application/json"
88-
},
89-
body: JSON.stringify(dto)
90-
});
91-
92-
if (graphResponse.ok) {
93-
return await graphResponse.text();
94-
} else {
95-
const error: any = (await graphResponse.json());
96-
console.log("Error", error);
97-
throw `${error.detail.replace("401 ()", "401 Invalid Jira Authorization")}`;
98-
}
85+
return await GraphService.getGraph(dto)
9986
}
10087

10188
/**
@@ -146,10 +133,8 @@ function setSvg(svgData: string): void {
146133
graphElement.innerHTML = svgData;
147134
}
148135

149-
let graphviz: any | null = null;
150-
151136
let lockForm = false;
152-
export async function createGraph(event: SubmitEvent): Promise<void> {
137+
export async function onSubmitForm(event: SubmitEvent): Promise<void> {
153138
event.preventDefault();
154139

155140
if (lockForm) {
@@ -161,7 +146,22 @@ export async function createGraph(event: SubmitEvent): Promise<void> {
161146

162147
const form = new JiraGraphForm(event.target as HTMLFormElement);
163148
const dto = getDto(form);
164-
switch (event.submitter!.id) {
149+
150+
if (event.submitter!.id === "KIOSK") {
151+
goToKioskWithOptions(dto);
152+
} else {
153+
await createGraph(event.submitter!.id, dto, submitButton);
154+
}
155+
}
156+
157+
function goToKioskWithOptions(options: Options) {
158+
sessionStorage.setItem('options', JSON.stringify(options));
159+
window.location.href = 'kiosk.html';
160+
}
161+
162+
163+
async function createGraph(format: "PNG" | "SVG" | string, dto: Options, submitButton: HTMLButtonElement) {
164+
switch (format) {
165165
case "PNG":
166166
dto.OutputFormat = "Png";
167167
break;
@@ -174,7 +174,7 @@ export async function createGraph(event: SubmitEvent): Promise<void> {
174174
}
175175

176176
try {
177-
if (!graphviz) {
177+
if (!GraphService.graphvizLoaded()) {
178178
await createGraphOnServer(dto);
179179
} else {
180180
await createGraphInBrowser(dto);
@@ -204,22 +204,17 @@ async function createGraphOnServer(dto: Options) {
204204
}
205205

206206
async function createGraphInBrowser(dto: Options) {
207-
if (!graphviz) {
208-
alert("Can't compile dot in browser, graphviz module not loaded.");
209-
return;
210-
}
211-
212207
const outputFormat = dto.OutputFormat;
213208
dto.OutputFormat = "Dot";
214209
const dotCode = await getGraph(dto);
215210
dto.OutputFormat = outputFormat;
216211
switch (outputFormat) {
217212
case 'Png':
218-
const pngData = await graphviz.dot(dotCode, "png");
213+
const pngData = await GraphService.dotPng(dotCode);
219214
setPng(pngData);
220215
break;
221216
case 'Svg':
222-
const svgData = await graphviz.dot(dotCode, "svg");
217+
const svgData = await GraphService.dotSvg(dotCode);
223218
setSvg(svgData);
224219
break;
225220
default:
@@ -228,24 +223,7 @@ async function createGraphInBrowser(dto: Options) {
228223
}
229224
}
230225

231-
(window as any).createGraph = createGraph;
232-
233-
234-
async function getGraphvizWasm(): Promise<any> {
235-
const response = await fetch("api/jiragraph/wasm-module");
236-
const script = await response.text();
237-
eval(script);
238-
const wasmModule: any = (window as { [key: string]: any })["@hpcc-js/wasm"] as any;
239-
return await wasmModule.Graphviz.load();
240-
}
241-
242-
export async function loadGraphvizWasm() {
243-
graphviz = await getGraphvizWasm();
244-
console.log("Graphviz wasm module loaded", graphviz.version());
245-
}
246-
247-
(window as any).loadGraphvizWasm = loadGraphvizWasm;
248-
loadGraphvizWasm();
226+
(window as any).createGraph = onSubmitForm;
249227

250228
/**
251229
* Here be style stuff.

README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ The service will check the length to determine whether to use the session ID or
3939

4040
### Web API
4141

42-
The frontend doesn't have many options yet. It's a simple html page with a form to enter the basic needed data and jira issues IDs (multiple can be entered, separated by comma).
42+
The frontend doesn't have all options yet. It's a simple html page with a form to enter options and jira issue IDs (multiple can be entered, separated by comma).
4343
By default this will simply walk the graph, ignore epics and subtasks, and build everything into an SVG, which will be diplayed on the page.
4444
The resulting SVG can be copied from dev tools. I might later add another button that puts the SVG into an `<img>` tag so it can simply be copied with a context click.
4545

46-
The backend supports all the options available to the CLI tool, there's a swagger page at `/swagger` to test it out.
46+
Kiosk mode can be started via the Kiosk mode button. This will hide the form and periodically reload the page with the same options. Default period is 1 minute. Refresh the page to immediately refresh the graph.
47+
48+
The backend supports all the options available to the CLI tool, there's a `/swagger` page to test it out.

0 commit comments

Comments
 (0)