Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Groupings to pages #254

Open
grcameron opened this issue Jan 21, 2020 · 1 comment
Open

Add Groupings to pages #254

grcameron opened this issue Jan 21, 2020 · 1 comment

Comments

@grcameron
Copy link

Pa11y Dashboard is a great tool, but it's apparent that in our use case (100's of pages across different projects), that it would be really useful to have the following:

  • Ability to group pages into different areas/sites. e.g. a site has many areas which has many pages. Or the ability to 'tag' pages to allow for viewing all pages under a specific tag.
  • A dashboard graph view showing everything under that specific tag, area or site.

The reason for this is to so that I can easily display the progress of our accessibility improvements within the pa11y dashboard UI.

I spoke to @joeyciechanowicz over slack about this, they have a private repo where something similar has been done, but not tested/reviewed/formalised.

@jasonday
Copy link

jasonday commented Mar 6, 2020

Adding the conversation from slack here for reference:


Grant Cameron Jan 21st at 4:12 AM
Hi folks, was wondering if anyone is aware of a fork for pa11y dashboard that offers a way to group and visualise pages (e.g. Site has many areas which has many pages)

Joey Ciechanowicz 1 month ago
Hi Grant 👋
I'm not aware of any forks which add this feature.
I hacked something together on a private board, but haven't had the time to formalise it and write it up with tests etc to be part of the full pa11y-dashboard

Grant Cameron 1 month ago
Thanks Joey, is there anything I could do to help with that? I was going to hack something together myself, but maybe that time could be spent helping formalise something. If there is a plan to do that anyway? If not it's cool I can hack something together for our needs I think.

Joey Ciechanowicz 1 month ago
Well, PR's are always always welcome ❤️

Joey Ciechanowicz 1 month ago
To get something down proper, it's best to open an issue to discuss the change so as to gather feedback

Joey Ciechanowicz 1 month ago
If you want to hack it together yourself, I can try dig out the code I threw together

Joey Ciechanowicz 1 month ago
This goes in route and gathers the results, grouping by splitting the name on :
route/wallboard.js

// This file is part of Pa11y Dashboard.
//
// Pa11y Dashboard is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pa11y Dashboard is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Pa11y Dashboard.  If not, see <http://www.gnu.org/licenses/>.
'use strict';
​
module.exports = route;
​
function taskState(counts) {
	if (counts.error > 0) {
		return 'error';
	}
​
	if (counts.warning > 0) {
		return 'warning';
	}
​
	if (counts.notice > 0) {
		return 'notice';
	}
​
	return 'none';
}
​
// Route definition
function route(app) {
	app.express.get('/wallboard', (request, response, next) => {
		if (request.query.reload !== undefined) {
			return response.render('reload', {
				layout: false,
				url: 'wallboard'
			});
		}
​
		app.webservice.tasks.get({lastres: true}, (error, tasks) => {
			if (error) {
				return next(error);
			}
​
			const grouped = tasks.reduce((groups, task) => {
				if (!task.last_result) {
					return groups;
				}
​
				task.overallState = taskState(task.last_result.count);
​
				const parts = task.name.split(':');
				if (parts.length >= 2) {
					const group = parts[0];
					task.name = parts.slice(1).join(':');
​
					if (!groups[group]) {
						groups[group] = [];
					}
​
					groups[group].push(task);
				} else {
					groups.default.push(task);
				}
​
				return groups;
			}, {default: []});
​
			const groups = Object.entries(grouped).map(([groupName, groupedTasks]) => ({
				groupName,
				defaultGroup: groupName === 'default',
				tasks: groupedTasks
			})).filter(group => group.tasks.length > 0);
​
			response.render('wallboard', {
				groups,
				layout: false
			});
		});
	});
​
	app.express.get('/wallboard-graph', (request, response, next) => {
		if (request.query.reload !== undefined) {
			return response.render('reload', {
				layout: false,
				url: 'wallboard-graph'
			});
		}
​
		app.webservice.tasks.get({}, (error, tasks) => {
			app.webservice.tasks.results({}, (error, results) => {
				if (error) {
					return next(error);
				}
​
				const tasksLookup = tasks.reduce((acc, curr) => {
					acc[curr.id] = curr;
					return acc;
				}, {});
​
				const modifiedResults = results.map(result => ({
					date: result.date,
					errors: result.count.error,
					warnings: result.count.warning,
					notices: result.count.notices,
					name: tasksLookup[result.task].name
				}));
​
				response.render('wallboard-graph', {
					data: modifiedResults,
					layout: false
				});
			});
		});
	});
}

Collapse

Joey Ciechanowicz 1 month ago
This goes in view/wallboard.html and renders /wallboard
view/wallboard.html

<!DOCTYPE html>
<html lang="{{lang}}" class="no-javascript">
<head>
	<meta charset="utf-8"/>
	<title>Pa11y Wallboard</title>
	<meta name="description" content="Pa11y Wallboard"/>
​
	{{#if noindex}}
	<meta name="robots" content="noindex"/>
	{{/if}}
​
	<link rel="icon" type="image/png" href="favicon.png"/>
​
	<!-- For mobile devices. -->
	<meta name="viewport" content="width=device-width"/>
​
	<style type="text/css">
		:root {
			--error: #ed4b35;
			--warning: #8b8b38;
			--notice: #28628b;
		}
​
		* {
			box-sizing: border-box;
		}
​
		body {
			background: #3d3c3c;
			padding: 0;
			margin: 0;
			font-family: Inconsolata, monospace;
			color: #ada8a8;
		}
​
		.header {
			width: 100%;
			text-align: center;
		}
​
		.groups {
			margin: 5px;
			display: flex;
			flex-wrap: wrap;
			align-content: space-around;
		}
​
		.task {
			background: #2a2929;
			margin: 0 5px 1%;
			width: 16%;
		}
​
		.task__header {
			color: #fff;
			width: 100%;
			padding: 0 5px 0 5px;
			min-height: 55px;
		}
​
		.task__header__group-name {
			font-size: 1em;
			margin: 0;
			color: #ada8a8;
			border-radius: 4px;
			padding: 0 2px 0 2px
		}
​
		.task__header__name {
			margin: 0;
		}
​
		.task--error {
			border-top: 4px solid var(--error);
		}
​
		.task--warning {
			border-top: 4px solid var(--warning);
		}
​
		.task--notice {
			border-top: 4px solid var(--notice);
		}
​
		.task--none {
			border-top: 4px solid #11c560;
		}
​
		.task__counts {
			width: 100%;
			display: flex;
		}
​
		.count {
			width: 33.333%;
			display: inline-block;
			line-height: 40px;
			text-align: center;
			font-size: 1.5em;
			color: white;
		}
​
		.count--error {
			background: var(--error);
		}
​
		.count--warning {
			background: var(--warning);
		}
​
		.count--notice {
			background: var(--notice);
		}
​
		.group-name--1 {
			background: #0069c5;
			color: #fff;
		}
​
		.group-name--2 {
			background: #6c757d;
			color: #fff;
		}
​
		.group-name--3 {
			background: #268942;
			color: #fff;
		}
​
		.group-name--4 {
			background: #b12f3d;
			color: #fff;
		}
​
		.group-name--5 {
			background: #b88707;
			color: #fff;
		}
​
		.group-name--0 {
			background: #fff;
			color: #000;
		}
	</style>
</head>
​
<body>
​
<header class="header">
	<h1 class="header__text">Accessibility</h1>
</header>
​
<section class="groups">
	{{#each groups}}
	{{#each tasks}}
	<div class="task task--{{overallState}}">
		<div class="task__header">
			<h2 class="task__header__name">{{name}}</h2>
​
			{{#unless ../defaultGroup}}
			<h1 class="task__header__group-name group-name--{{mod @../index 6}}">
				{{../groupName}}
			</h1>
			{{/unless}}
		</div>
		{{#last_result}}
		<div class="task__counts">
			<div class="count count--error">{{count.error}}</div>
			<div class="count count--warning">{{count.warning}}</div>
			<div class="count count--notice">{{count.notice}}</div>
		</div>
		{{/last_result}}
	</div>
	{{/each}}
	{{/each}}
</section>
​
</body>
</html>

Collapse

Joey Ciechanowicz 1 month ago
This goes in view/wallboard-graph.html and renders /wallboard-graph
view/wallboard-graph.html

<!DOCTYPE html>
<html lang="{{lang}}" class="no-javascript">
<head>
	<meta charset="utf-8"/>
	<title>Pa11y Wallboard</title>
	<meta name="description" content="Pa11y Wallboard"/>
​
	{{#if noindex}}
	<meta name="robots" content="noindex"/>
	{{/if}}
​
	<link rel="icon" type="image/png" href="favicon.png"/>
​
	<!-- For mobile devices. -->
	<meta name="viewport" content="width=device-width"/>
​
	<script src="https://d3js.org/d3.v5.min.js"></script>
​
	<style type="text/css">
		:root {
			--error: #ed4b35;
			--warning: #8b8b38;
			--notice: #28628b;
		}
​
		* {
			box-sizing: border-box;
		}
​
		body {
			background: #3d3c3c;
			padding: 0;
			margin: 0;
			font-family: Inconsolata, monospace;
			color: #ada8a8;
		}
​
		html, body, #container {
			height: 100%;
		}
​
		.header {
			width: 100%;
			text-align: center;
		}
​
		#container {
			width: 100%;
			display: grid;
			grid-template-columns: 100%;
			grid-template-rows: 80% 20%;
		}
​
		#graph {
			width: 100%;
			height: 100%;
		}
​
		#labels {
			margin: 0;
		}
​
		li {
			display: inline-block;
			padding-left: 8px;
			font-size: 2rem;
		}
​
		li:before {
			content: '\2022';
			margin-right: 2px;
		}
​
​
		.axis {
			color: #ada8a8;
			stroke-width: 2;
			font-size: 1.4rem;
		}
​
		path {
			fill: none;
			stroke-width: 2;
		}
​
		.legend {
			font-size: 2rem;
		}
	</style>
</head>
​
<body>
​
<header class="header">
	<h1 class="header__text">Converged Accessbility Errors</h1>
</header>
​
<div id="container">
	<svg id="graph"></svg>
	<ul id="labels"></ul>
</div>
​
<script>
	const data = {{{json data}}};
​
	// Set the dimensions of the canvas / graph
	const margin = {top: 10, right: 50, bottom: 35, left: 60};
	const containerDimensions = document.getElementById('graph').getBoundingClientRect();
​
	console.log(containerDimensions);
​
	const width = document.getElementById('graph').clientWidth - margin.left - margin.right;
	const height = Math.floor(containerDimensions.height) - margin.top - margin.bottom;
​
	// Parse the date / time
​
	data.forEach(result => {
		result.date = new Date(result.date);
	});
​
	data.sort((a, b) => {
		return a.date > b.date ?
				1
				: a.date < b.date ?
						-1
						: 0;
	});
​
	const timeDomain = d3.extent(data, result => result.date);
	timeDomain[1] = new Date(timeDomain[1].valueOf()).setDate(timeDomain[1].getDate() + 4);
​
	// Set the ranges
	const x = d3.scaleTime()
			.domain(d3.extent(data, result => result.date))
			.range([0, width]);
​
	const y = d3.scaleLinear()
			.domain([0, d3.max(data, result => result.errors) + 1])
			.range([height, 0])
​
	// Define the line
	const line = d3.line()
			.x(result => x(result.date))
			.y(result => y(result.errors));
​
	// Adds the svg canvas
	const svg = d3.select('#graph')
			.attr('width', width + margin.left + margin.right)
			.attr('height', height + margin.top + margin.bottom)
			.append('g')
			.attr('transform',
					'translate(' + margin.left + ',' + margin.top + ')');
​
	const labels = d3.select('#labels');
​
	// Nest the entries by symbol
	const dataNest = d3.nest()
			.key(result => result.name)
			.entries(data);
​
	// set the colour scale
	const color = d3.scaleOrdinal(d3.schemeCategory10);
​
	legendSpace = width / dataNest.length; // spacing for the legend
​
	// Loop through each symbol / key
	dataNest.forEach((nest, index) => {
​
		svg.append('path')
				.attr('class', 'line')
				.style('stroke', color(nest.key))
				.attr('d', line(nest.values));
​
		// Add the Legend
		labels.append('li')
				.style('color', color(nest.key))
				.text(nest.key);
​
	});
​
	// Add the X Axis
	svg.append('g')
			.attr('class', 'axis')
			.attr('transform', 'translate(0,' + height + ')')
			.call(d3.axisBottom(x));
​
	// Add the Y Axis
	svg.append('g')
			.attr('class', 'axis')
			.call(d3.axisLeft(y));
​
	svg.selectAll('.dot')
			.data(data)
			.enter().append('circle')
			.attr('class', 'dot')
			.attr('cx', result =>  x(result.date))
			.attr('cy', result => y(result.errors))
			.style('fill', result => color(result.name))
			.attr('r', 5)
​
</script>
​
​
</body>
</html>

Collapse

Joey Ciechanowicz 1 month ago
Shove this in view/reload.html and then you can use /wallboard?reload=true or /wallboard-graph?reload=true
view/reload.html

<!DOCTYPE html>
<html lang="{{lang}}" class="no-javascript">
<head>
	<meta charset="utf-8"/>
	<title>Pa11y Wallboard</title>
	<meta name="description" content="Pa11y Wallboard"/>
​
	{{#if noindex}}
	<meta name="robots" content="noindex"/>
	{{/if}}
​
	<link rel="icon" type="image/png" href="favicon.png"/>
​
	<!-- For mobile devices. -->
	<meta name="viewport" content="width=device-width"/>
​
	<style>
		html, body, iframe { height: 100%; width: 100%}
		* {
			margin: 0;
			padding: 0;
		}
	</style>
</head>
​
<body>
​
<iframe id="wallboard-frame" style="position: absolute; height: 100%; border: none" src="/{{url}}"></iframe>
​
<script type="text/javascript">
	setInterval(() => {
		document.getElementById('wallboard-frame').contentWindow.location.reload();
		window.location.reload();
	}, 5 * 60 * 1000);
</script>
​
</body>
</html>

Collapse

Grant Cameron 1 month ago
Thanks so much Joey! I'll raise an issue to discuss further, and in the mean time I'll try out your changes, really appreciate it.

Grant Cameron 1 month ago
For reference, created this issue: #254
🌟
1

Grant Cameron 1 month ago
This is great, managed to get that going for the time being.

Joey Ciechanowicz 1 month ago
fantastic!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants