qa: Back to the same situation

This commit is contained in:
nemunaire 2022-11-07 01:00:04 +01:00
commit 1aa82bb2ef
27 changed files with 1336 additions and 22 deletions

View file

@ -0,0 +1,17 @@
<script>
export let date;
export let dateStyle;
export let timeStyle;
function formatDate(input, dateStyle, timeStyle) {
if (typeof input === 'string') {
input = new Date(input);
}
return new Intl.DateTimeFormat(undefined, {
dateStyle,
timeStyle,
}).format(input);
}
</script>
{formatDate(date, dateStyle, timeStyle)}

View file

@ -0,0 +1,54 @@
<script>
import { goto } from '$app/navigation';
import {
Button,
Container,
Icon,
Table,
} from 'sveltestrap';
import QAItems from '$lib/components/QAItems.svelte';
import QANewItem from '$lib/components/QANewItem.svelte';
import { themes, themesIdx } from '$lib/stores/themes';
if ($themes.length == 0) {
themes.refresh();
}
export let exercice = {};
let countCreation = 0;
</script>
<h2>
{exercice.title}
{#if $themes.length && $themesIdx[exercice.id_theme]}
<small>
<a href="themes/{exercice.id_theme}" title={$themesIdx[exercice.id_theme].authors}>{$themesIdx[exercice.id_theme].name}</a>
</small>
{#if $themesIdx[exercice.id_theme].exercices && $themesIdx[exercice.id_theme].exercices[exercice.id]}
<div class="btn-group" role="group">
<a href="exercices/{$themesIdx[exercice.id_theme].exercices[exercice.id].previous}" title="Exercice précédent" class:disabled={!$themesIdx[exercice.id_theme].exercices[exercice.id].previous} class="btn btn-sm btn-light"><Icon name="chevron-left" /></a>
<a href="exercices/{$themesIdx[exercice.id_theme].exercices[exercice.id].next}" title="Exercice suivant" class:disabled={!$themesIdx[exercice.id_theme].exercices[exercice.id].next} class="btn btn-sm btn-light"><Icon name="chevron-right" /></a>
</div>
{/if}
<a href="../{$themesIdx[exercice.id_theme].urlid}/{exercice.urlid}" target="_self" class="float-right ml-2 btn btn-sm btn-info"><Icon name="play-fill" /> Site du challenge</a>
{/if}
</h2>
<div class="row mb-3">
<div class="col-md-6">{@html exercice.statement}</div>
<div class="col-md-6">{@html exercice.overview}</div>
</div>
<div class="mb-5">
<QANewItem
{exercice}
on:new-query={() => countCreation++}
/>
{#key countCreation}
<QAItems
{exercice}
/>
{/key}
</div>

View file

@ -1,4 +1,6 @@
<script>
import { page } from '$app/stores'
import {
Badge,
Button,
@ -20,7 +22,15 @@
Row,
} from 'sveltestrap';
const version = fetch('api/version', {headers: {'Accept': 'application/json'}}).then((res) => res.json())
import { auth, version } from '$lib/stores/auth';
export let activemenu = "";
$: {
const path = $page.url.pathname.split("/");
if (path.length > 1) {
activemenu = path[1];
}
}
</script>
<Navbar color="dark" dark expand="md">
@ -30,25 +40,46 @@
</NavbarBrand>
<Nav navbar>
<NavItem>
<NavLink href=".">
<NavLink
href="."
active={activemenu === ''}
>
<Icon name="house-door" />
Accueil
</NavLink>
</NavItem>
<NavItem>
<NavLink href="themes">
<NavLink
href="themes"
active={activemenu === 'themes'}
>
<Icon name="box-seam" />
Scénarios
</NavLink>
</NavItem>
<NavItem>
<NavLink href="teams">
<NavLink
href="exercices"
active={activemenu === 'exercices'}
>
<Icon name="bar-chart-steps" />
Étapes
</NavLink>
</NavItem>
<NavItem>
<NavLink
href="teams"
active={activemenu === 'teams'}
>
<Icon name="people" />
Équipes
</NavLink>
</NavItem>
<NavItem>
<NavLink href="repositories">
<NavLink
href="repositories"
active={activemenu === 'repositories'}
>
<Icon name="archive" />
Dépôts
</NavLink>
@ -56,12 +87,8 @@
</Nav>
<Nav class="ms-auto text-light" navbar>
<NavItem class="ms-2">
{#await version}
veuillez patienter
{:then v}
v{v.version}
{#if v.auth}&ndash; Logged as {v.auth.name} (team #{v.auth.id_team}){/if}
{/await}
v{$version.version}
{#if $auth}&ndash; Logged as {$auth.name} (team #{$auth.id_team}){/if}
</NavItem>
</Nav>
</Navbar>

View file

@ -0,0 +1,93 @@
<script>
import { goto } from '$app/navigation';
import {
Spinner,
} from 'sveltestrap';
import { getQAView } from '$lib/todo';
import { getExerciceQA } from '$lib/qa';
import { exercicesIdx } from '$lib/stores/exercices';
import { themesIdx } from '$lib/stores/themes';
import { todos } from '$lib/stores/todo';
export { className as class };
let className = '';
function show(id) {
goto("exercices/" + id)
}
let my_exercices = [];
let my_exercicesP = update_exercices()
async function update_exercices() {
const view = await getQAView();
my_exercices = [];
for (const v of view) {
v.queries = null;
v.queriesNSolved = 0;
v.queriesNClosed = 0;
getExerciceQA(v.id_exercice).then((queries) => {
v.queries = queries;
for (const q of queries) {
if (q.solved == null) v.queriesNSolved++;
if (q.closed == null) v.queriesNClosed++;
}
my_exercices.push(v);
my_exercices = my_exercices;
});
}
}
</script>
<div class={className}>
<h3>Vos étapes</h3>
{#await my_exercicesP}
{:then}
<table class="table table-stripped table-hover">
<thead>
<tr>
<th>Défi</th>
<th>Requêtes</th>
</tr>
</thead>
<tbody>
{#each my_exercices as todo (todo.id)}
<tr
class:table-success={todo.queries && todo.queries.length > 0}
class:table-warning={todo.queriesNSolved > 0}
on:click={() => show(todo.id_exercice)}
>
<td>
{#if $exercicesIdx.length == 0 && $themesIdx.length == 0}
<Spinner size="sm" />
{:else if $themesIdx[$exercicesIdx[todo.id_exercice]]}
<a href="themes/{$exercicesIdx[todo.id_exercice].id_theme}">
{$themesIdx[$exercicesIdx[todo.id_exercice].id_theme].name}
</a>
&ndash;
{/if}
{#if $exercicesIdx.length == 0}
<Spinner size="sm" />
{:else if $exercicesIdx[todo.id_exercice]}
{$exercicesIdx[todo.id_exercice].title}
{#if $exercicesIdx[todo.id_exercice].wip}
<Icon name="cone-striped" />
{/if}
{/if}
</td>
<td>
{#if todo.queries && todo.queries.length}
{todo.queriesNSolved} / {todo.queriesNClosed}
{:else}
0
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
{/await}
</div>

View file

@ -0,0 +1,91 @@
<script>
import { goto } from '$app/navigation';
import {
Spinner,
} from 'sveltestrap';
import { getExerciceTested, getQAWork } from '$lib/todo'
import { exercicesIdx } from '$lib/stores/exercices'
import { themesIdx } from '$lib/stores/themes'
import { todos } from '$lib/stores/todo'
export { className as class };
let className = '';
let exo_doneP = getExerciceTested();
let tododone = { };
getQAWork().then((queries) => {
for (const q of queries) {
tododone[q.id_exercice] = q;
}
})
function show(id) {
goto("exercices/" + id)
}
</script>
<div class={className}>
<h3>Étapes à tester et valider</h3>
{#await todos.refresh()}
{:then}
{#await exo_doneP}
{:then exo_done}
<table class="table table-stripped table-hover">
<thead>
<tr>
<th>Avancement</th>
<th>Scénario</th>
<th>Défi</th>
</tr>
</thead>
<tbody>
{#each $todos as todo (todo.id)}
<tr
style:cursor="pointer"
class:text-light={!tododone[todo.id_exercice] && !exo_done[todo.id_exercice]}
class:table-dark={!tododone[todo.id_exercice] && !exo_done[todo.id_exercice]}
class:table-warning={!tododone[todo.id_exercice] && exo_done[todo.id_exercice] == 'access'}
class:table-info={!tododone[todo.id_exercice] && (exo_done[todo.id_exercice] == 'tried' || exo_done[todo.id_exercice] == 'solved')}
class:table-success={tododone[todo.id_exercice]} on:click={() => show(todo.id_exercice)}
>
<td>
{#if tododone[todo.id_exercice]}
Commenté
{#if !exo_done[todo.id_exercice] || exo_done[todo.id_exercice] != 'solved'}
mais pas testé/terminé
{/if}
{:else if exo_done[todo.id_exercice] && exo_done[todo.id_exercice] != 'access'}
À commenter
{:else}
À tester
{/if}
</td>
<td>
{#if $exercicesIdx.length == 0 && $themesIdx.length == 0}
<Spinner size="sm" />
{:else}
<a href="themes/{$exercicesIdx[todo.id_exercice].id_theme}">
{$themesIdx[$exercicesIdx[todo.id_exercice].id_theme].name}
</a>
{/if}
</td>
<td>
{#if $exercicesIdx.length == 0}
<Spinner size="sm" />
{:else}
{$exercicesIdx[todo.id_exercice].title}
{#if $exercicesIdx[todo.id_exercice].wip}
<Icon name="cone-striped" />
{/if}
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
{/await}
{/await}
</div>

View file

@ -0,0 +1,183 @@
<script>
import { createEventDispatcher } from 'svelte';
import {
Button,
Card,
CardBody,
CardHeader,
Icon,
Spinner,
} from 'sveltestrap';
import DateFormat from '$lib/components/DateFormat.svelte';
import { getQAComments, QAComment } from '$lib/qa.js';
import { auth } from '$lib/stores/auth';
const dispatch = createEventDispatcher();
export let query_selected;
let query_commentsP;
$: query_commentsP = getQAComments(query_selected.id);
let newComment = new QAComment();
let submissionInProgress = false;
function addComment() {
submissionInProgress = true;
newComment.save(query_selected.id).then(() => {
query_commentsP = getQAComments(query_selected.id);
newComment = new QAComment();
submissionInProgress = false;
})
}
function updateQA() {
query_selected.save().then(() => {
dispatch("update-queries");
})
}
function solveQA() {
query_selected.solved = new Date();
query_selected.save().then(() => {
dispatch("update-queries");
})
}
function reopenQA() {
query_selected.solved = null;
query_selected.save().then(() => {
dispatch("update-queries");
})
}
function closeQA() {
query_selected.closed = new Date();
query_selected.save().then(() => {
dispatch("update-queries");
})
}
function deleteQA() {
query_selected.delete().then(() => {
query_selected = null;
dispatch("update-queries");
})
}
</script>
<Card>
<CardHeader>
<div class="d-flex justify-content-between">
<h4 class="card-title mb-0">{query_selected.subject}</h4>
<div>
{#if $auth && $auth.id_team == query_selected.id_team}
{#if query_selected.solved && !query_selected.closed}
<Button on:click={closeQA} color="success">
<Icon name="check" />
Valider la résolution
</Button>
<Button on:click={reopenQA} color="danger">
<Icon name="x" />
Réouvrir
</Button>
{/if}
{#if !query_selected.solved}
<Button on:click={deleteQA} color="danger">
<Icon name="trash-fill" />
Supprimer
</Button>
{/if}
{:else if $auth && !query_selected.solved}
<Button on:click={solveQA} color="success">
<Icon name="check" />
Marquer comme résolu
</Button>
{/if}
</div>
</div>
</CardHeader>
<CardBody>
<div class="row">
<dl class="col row">
<dt class="col-3">Qui ?</dt>
<dd class="col-9">{query_selected.user} (team #{query_selected.id_team})</dd>
<dt class="col-3">État</dt>
<dd class="col-9">{query_selected.state}</dd>
<dt class="col-3">Date de création</dt>
<dd class="col-9">
<DateFormat date={query_selected.creation} dateStyle="long" timeStyle="medium" />
</dd>
<dt class="col-3">Date de résolution</dt>
<dd class="col-9">
{#if query_selected.solved}
<DateFormat date={query_selected.solved} dateStyle="long" timeStyle="medium" />
{:else}
-
{/if}
</dd>
<dt class="col-3">Date de clôture</dt>
<dd class="col-9">
{#if query_selected.closed}
<DateFormat date={query_selected.closed} dateStyle="long" timeStyle="medium" />
{:else}
-
{/if}
</dd>
</dl>
<div class="col-auto">
{#if $auth && $auth.id_team == query_selected.id_team}
<Button on:click={updateQA} color="primary">
<Icon name="upload" />
Mettre à jour
</Button>
{/if}
</div>
</div>
{#await query_commentsP}
<div class="d-flex">
<Spinner />
<div>
Chargement des commentaires en cours&hellip;
</div>
</div>
{:then query_comments}
<table class="table table-striped">
{#each query_comments as comment (comment.id)}
<tr>
<td style="white-space: pre-line">
Le <DateFormat date={comment.date} dateStyle="medium" timeStyle="short" />, <strong>{comment.user}</strong> a écrit&nbsp;: {comment.content}
</td>
</tr>
{/each}
</table>
{/await}
<form on:submit|preventDefault={addComment}>
<label for="newComment">Répondre :</label>
<textarea
class="form-control"
placeholder="Ajouter un commentaire"
rows="2"
id="newComment"
bind:value={newComment.content}
></textarea>
<Button
type="submit"
color="primary"
class="mt-1 float-right"
disabled={!newComment.content || newComment.content.length == 0 || submissionInProgress}
>
{#if submissionInProgress}
<Spinner size="sm" />
{/if}
Ajouter le commentaire
</Button>
</form>
</CardBody>
</Card>

View file

@ -0,0 +1,95 @@
<script>
import {
Button,
Icon,
Spinner,
} from 'sveltestrap';
import { getExerciceQA, QAComment } from '$lib/qa.js';
import QAItem from '$lib/components/QAItem.svelte';
export let exercice = { };
export let query_selected = null;
const fields = [ "state", "subject" ];
let queriesP;
$: queriesP = getExerciceQA(exercice.id);
let thumbInProgress = null;
function thumbUp(qid) {
thumbInProgress = qid;
const thumb = new QAComment({
content: "+1",
});
thumb.save(qid).then(() => {
thumbInProgress = null;
})
}
function updateQueries() {
queriesP = getExerciceQA(exercice.id);
}
</script>
{#await queriesP}
{:then queries}
<table
class="table table-bordered table-striped"
class:table-hover={queries.length}
class:table-sm={queries.length}
>
<thead class="thead-dark">
<tr>
{#each fields as field}
<th>
{ field }
</th>
{/each}
<th></th>
</tr>
</thead>
{#if queries.length}
<tbody>
{#each queries as q (q.id)}
<tr on:click={() => query_selected = q} class:bg-warning={query_selected && q.id == query_selected.id}>
{#each fields as field}
<td>
{@html q[field]}
</td>
{/each}
<td>
<button
type="button"
class="btn btn-sm btn-light"
disabled={thumbInProgress !== null}
on:click|preventDefault={() => thumbUp(q.id)}
>
{#if thumbInProgress == q.id}
<Spinner size="sm" />
{:else}
<Icon name="hand-thumbs-up" />
{/if}
</button>
</td>
</tr>
{/each}
</tbody>
{:else}
<tbody>
<tr>
<td colspan={fields.length+1} class="font-weight-bold text-info text-center">
Aucune requête enregistrée
</td>
</tr>
</tbody>
{/if}
</table>
{#if query_selected}
<QAItem
bind:query_selected={query_selected}
on:update-queries={updateQueries}
/>
{/if}
{/await}

View file

@ -0,0 +1,76 @@
<script>
import { createEventDispatcher } from 'svelte';
import {
Button,
Spinner,
} from 'sveltestrap';
import { QAQuery, QAStates } from '$lib/qa.js';
const dispatch = createEventDispatcher();
export let exercice = {};
let newQuery = new QAQuery();
let creationInProgress = false;
function saveQuery() {
newQuery.id_exercice = exercice.id;
creationInProgress = true;
newQuery.save().then((res) => {
dispatch("new-query");
newQuery = new QAQuery();
creationInProgress = false;
})
}
</script>
<form on:submit|preventDefault={saveQuery} class="card mb-3">
<div class="card-header">
Qu'avez-vous pensé de cette étape&nbsp;?
</div>
<div class="card-body">
<div class="mb-3 row">
<label for="state" class="col-2 col-form-label col-form-label-sm">État</label>
<div class="col-10">
<select class="form-select form-select-sm" id="state" bind:value={newQuery.state}>
{#each Object.keys(QAStates) as state}
<option value={state}>
{QAStates[state]}
</option>
{/each}
</select>
</div>
</div>
<div class="mb-3 row">
<label for="subject" class="col-2 col-form-label col-form-label-sm">Sujet</label>
<div class="col-10">
<input
type="text"
class="form-control form-control-sm"
id="subject"
placeholder="Le pcap ne contient pas l'IP attendue"
bind:value={newQuery.subject}
>
</div>
</div>
<div class="mb-3 row">
<label for="description" class="col-2 col-form-label col-form-label-sm">Description</label>
<div class="col-10">
<textarea class="form-control form-control-sm" placeholder="Ajouter un commentaire" rows="2" id="description" bind:value={newQuery.content}></textarea>
</div>
</div>
<Button
type="submit"
color="primary"
class="float-end"
disabled={creationInProgress}
>
{#if creationInProgress}
<Spinner size="sm" />
{/if}
Soumettre
</Button>
</div>
</form>