webservices-app/src/routes/+page.svelte

776 lines
25 KiB
Svelte

<script>
// get the data from the JSON Webservices feed in +page.js
export let data;
const { webservices } = data;
import { references } from '$lib/references.js';
import { colours } from '$lib/colours.js';
// the filters
import { setFilter, getFilter } from '$lib/components/filter.js';
import { Accordions, Accordion, AccordionBody, El } from 'yesvelte';
import Filterable from '$lib/components/Filterable.svelte';
// collapsible sections
//import CollapsibleSection from '$lib/components/CollapsibleSection.svelte';
import TechnologyTile from '$lib/components/TechnologyTile.svelte';
// URL-related stuff
import { page } from '$app/stores';
let tabindex = 0; // a counter which facilitates keyboard navigation.
let showModal = true; // use to make a technology modal show when rendered
// return the % a is of b
function percent(a, b) {
return new Intl.NumberFormat('en', { style: "percent" }).format(a/b);
}
//
// digest and return useful info from the Webservices JSON feed
function processData(webservices) {
let stats = { instances: 0, technologies: 0, licenses: 0, weak: 0, copyleft: 0 };
let all = []; // all services
let current = []; // those with active instances
let future = []; // those that are planned
// properties of technologies
let category_list = [];
let analogue_list = [];
let license_list = [];
// properties of instances
let status_list = [];
let affiliate_list = [];
let host_list = [];
// actual objects
let hosts = [];
let affiliates = [];
let licenses = [];
//
// just a trivial reassignment
//let hosts = webservices.hosts;
//
// pull out relevant info in useful chunks.
for (let key in webservices.technologies) {
let tech = webservices.technologies[key];
console.log('considering ' + countArray(webservices.technologies) + ' technologies at the start.');
tech['name'] = key;
//console.log('tech '+key+' ('+tech.categories.length+'):', tech.categories);
if (tech.hasOwnProperty('categories') && tech.categories.constructor === Array) {
//console.log('tech '+key+' ('+tech.categories.length+'):', tech.categories);
tech.categories.forEach(function (category, index) {
if (category_list.hasOwnProperty(category)) category_list[category]++;
else category_list[category] = 1;
});
}
if (tech.hasOwnProperty('analogues') && tech.analogues.constructor === Array) {
tech.analogues.forEach(function (analogue, index) {
if (analogue_list.hasOwnProperty(analogue)) analogue_list[analogue]++;
else analogue_list[analogue] = 1;
});
}
if (tech.hasOwnProperty('license')) {
let license = tech.license;
if (license_list.hasOwnProperty(license)) license_list[license]++;
else license_list[license] = 1;
}
if (hasInstances(tech) || tech.instances == 'all') {
if (tech.instances != 'all') {
current.push(tech);
//console.log(tech.name + ': ' + tech.instances.length + ' instances...');
tech.instances.forEach(function (instance, i) {
stats.instances++;
if (instance.hasOwnProperty('status')) {
let tag = instance.status;
if (status_list.hasOwnProperty(tag)) status_list[tag]++;
else status_list[tag] = 1;
}
if (instance.hasOwnProperty('affiliation')) {
let tag = instance.affiliation;
if (affiliate_list.hasOwnProperty(tag)) affiliate_list[tag]++;
else affiliate_list[tag] = 1;
}
if (instance.hasOwnProperty('host')) {
let tag = instance.host;
if (host_list.hasOwnProperty(tag)) host_list[tag]++;
else host_list[tag] = 1;
}
});
} else {
console.log('tech ' + tech.name + ' applies to *all* instances');
}
} else {
future[key] = tech;
}
all[key] = tech;
}
for (let key in webservices.hosts) {
//console.log('key: ', key);
let host = webservices.hosts[key];
host['name'] = key;
//console.log('host: ', host);
if (
host.hasOwnProperty('domain') &&
!(host.hasOwnProperty('status') && host.status == 'retired')
) {
hosts[key] = host;
}
}
for (let key in webservices.affiliates) {
let affiliate = webservices.affiliates[key];
affiliates[key] = affiliate;
}
for (let key in webservices.licenses) {
let license = webservices.licenses[key];
licenses[key] = license;
}
// assign stats based on gathered data...
stats.current = current.length;
console.log('future technologies: ', future);
stats.future = countArray(future);
console.log('license_list: ', license_list);
stats.licenses = countArray(license_list);
stats.licensed_technologies = numLicensedTechnologies(license_list);
stats.strong = numCopyleft(license_list, references.copyleft_list);
stats.weak = stats.licensed_technologies - stats.strong;
console.log('host_list: ', host_list);
console.log('number of hosts: ', countArray(host_list));
stats.hosts = countArray(host_list);
console.log('stats: ', stats);
return {
stats: stats,
all_services: all,
active_services: current,
candidate_services: future,
tech_lists: {
category_list: category_list,
analogue_list: analogue_list,
license_list: license_list
},
instance_lists: {
status_list: status_list,
affiliate_list: affiliate_list,
host_list: host_list
},
hosts: hosts,
affiliates: affiliates,
licenses: licenses
};
}
// when compound arrays don't have a valid .length key, use this
function countArray(arr) {
var count = 0;
for (var k in arr) {
if (arr.hasOwnProperty(k)) {
count++;
}
}
return count;
}
// count copyleft licenses, by only checking the copyleft totals
function numCopyleft(list, copyleft) {
var count = 0;
copyleft.forEach(function (license) {
//console.log('found ' + list[license] + ' technologies using ' + license);
count += list[license];
});
return count;
}
function numLicensedTechnologies(list) {
var count = 0;
for (var key in list) {
//console.log('found ' + list[key] + ' apps for license ' + key);
count += list[key];
}
return count;
}
// return an object with ob's keys ordered alphabetically, with an id from an object
function getSortedFilter(ob) {
let keys = [];
let i = 0;
let key_array = [];
// first create a flat array.
for (let key in ob) {
keys.push(key);
}
// sort the array alphabetically
keys.sort();
// then create a dictionary of them
keys.forEach(function (name) {
key_array.push({ id: i++, name: name, active: true });
});
return key_array;
}
// return true if a tech object includes valid instances
function hasInstances(tech) {
if (tech.hasOwnProperty('instances') &&
tech.instances.constructor === Array &&
tech.instances.length) {
return true;
}
return false;
}
// get intersection: ref https://bobbyhadz.com/blog/javascript-get-intersection-of-two-arrays
function getIntersection(a, b) {
const set1 = new Set(a);
const set2 = new Set(b);
const intersection = [...set1].filter((element) => set2.has(element));
return intersection;
}
// assign colours from a set of differentiated colours to a list of tags...
// this one is for 'hosts'
//
// host_list is in the form of
function hostColours(host_list, colours, hosts) {
let host_array = {};
let i = 0;
//console.log('hosts:', hosts);
//console.log('host_list:', host_list);
for (let index in host_list) {
console.log(host_list[index]);
let hostname = host_list[index].name;
if (
hosts[hostname].hasOwnProperty('domain') &&
hosts[hostname].hasOwnProperty('affiliation')
) {
host_array[hostname] = {
colour: colours[i++],
domain: hosts[hostname].domain,
affiliation: hosts[hostname].affiliation
};
}
}
return host_array;
}
// assign colours from a set of differentiated colours to a list of tags...
// this one is for 'affiliates'
function affiliateColours(affiliate_list, colours, affiliates) {
let affiliate_result = {};
let i = 0;
//console.log('affiliates:', affiliates);
//console.log('affiliate_list:', affiliate_list);
for (let index in affiliate_list) {
let affiliate = affiliate_list[index].name;
//console.log('affiliate:', affiliate);
if (
affiliates[affiliate].hasOwnProperty('name') &&
affiliates[affiliate].hasOwnProperty('website')
) {
affiliate_result[affiliate] = {
colour: colours[i++],
name: affiliates[affiliate].name,
website: affiliates[affiliate].website
};
}
}
return affiliate_result;
}
// sort technologies alphabetically by name
function sortTechnologies(technologies) {
return technologies.sort((a, b) => a.name.localeCompare(b.name));
}
// set previous and next on each technology
// generic function to find the intersection of two arrays
// approach ref: https://bobbyhadz.com/blog/javascript-get-intersection-of-two-arrays
function inCommon(a, b) {
const set1 = new Set(a);
const set2 = new Set(b);
const intersection = [...set1].filter((element) => set2.has(element));
//console.log('intersection = ', intersection);
return intersection;
}
// converts a filter with id, name, and active fields into an array of names
function flattenFilter(filter) {
let flat = [];
if (filter.constructor === Array) {
filter.forEach(function (e) {
if (e.active) flat.push(e.name);
});
}
return flat;
}
// filter technologies based on an array of filterables on tech and a list of filters
function filterTechnologiesByArray(technologies, field, list) {
const included = [];
technologies.forEach(function (tech) {
if (hasInstances(tech) || tech.instances == 'all') {
if (typeof tech[field] != 'undefined') {
let arr = tech[field];
//if (field == 'analogues') console.log('tech['+field+'] = ', tech[field]);
const intersection = inCommon(arr, list);
if (intersection && intersection.constructor === Array) {
//console.log('intersection length: ', intersection.length);
if (intersection.length > 0) {
included.push(tech);
}
}
// this tech doesn't define that field, so don't eliminate it from the list
} else {
included.push(tech);
}
} else {
console.log('not including tech ', tech.name);
}
});
//console.log('found ' + included.length + ' technologies.');
return included;
}
// filter technologies based on a single filterable value on tech and a list of filters
function filterTechnologiesByValue(technologies, field, list) {
const included = [];
technologies.forEach(function (tech) {
if (hasInstances(tech) || tech.instances == 'all') {
if (typeof tech[field] != 'undefined') {
let arr = [tech[field]];
const intersection = inCommon(arr, list);
if (intersection && intersection.constructor === Array) {
//console.log('intersection length: ', intersection.length);
if (intersection.length > 0) {
included.push(tech);
}
}
} else {
included.push(tech);
}
}
});
//console.log('found ' + included.length + ' technologies.');
return included;
}
function filterTechnologiesByInstanceValue(technologies, field, list) {
const included = [];
technologies.forEach(function (tech) {
var to_include = false;
if (hasInstances(tech)) {
//console.log('instances: ', tech.instances);
for (let id in tech.instances) {
console.log('id = ', id);
console.log('field = ', field);
let instance = tech.instances[id];
let value = instance[field];
console.log('field value = ', value);
console.log('list = ', list);
if (instance.hasOwnProperty(field)) {
//const intersection = inCommon([instance[field]], list);
const intersection = inCommon([value], list);
if (intersection && intersection.constructor === Array) {
console.log('intersection length: ', intersection.length);
// if any one instance belonging to the tech is in the set, add the tech.
if (intersection.length > 0) {
if (instance.hasOwnProperty('domain')) {
console.log('setting to include ',instance.domain);
}
to_include = true;
} else {
// if an instance doesn't belong in the set, remove it.
/*if (instance.hasOwnProperty('domain')) {
tech.instances[instance.domain].pop();
}*/
}
}
} else {
console.log('instance missing field ', field);
}
}
}
if (to_include) {
console.log('including ', tech.name);
included.push(tech);
}
});
//console.log('found ' + included.length + ' technologies.');
return included;
}
//
// process the data received by the web request!
const results = processData(webservices);
const licenses = results.licenses;
//const technologies = webservices.technologies;
//console.log(technologies);
// set up filters for each type
setFilter('categories');
setFilter('analogues');
setFilter('licenses');
setFilter('statuses');
setFilter('affiliates');
setFilter('hosts');
// define the handle for each writable
const categoryFilter = getFilter('categories');
const analogueFilter = getFilter('analogues');
const licenseFilter = getFilter('licenses');
const statusFilter = getFilter('statuses');
const affiliateFilter = getFilter('affiliates');
const hostFilter = getFilter('hosts');
// populate the writable with actual data
$categoryFilter = getSortedFilter(results.tech_lists.category_list);
$analogueFilter = getSortedFilter(results.tech_lists.analogue_list);
$licenseFilter = getSortedFilter(results.tech_lists.license_list);
$statusFilter = getSortedFilter(results.instance_lists.status_list);
$affiliateFilter = getSortedFilter(results.instance_lists.affiliate_list);
$hostFilter = getSortedFilter(results.instance_lists.host_list);
const hosts = results.hosts;
//console.log('host_data: ', results.hosts);
const affiliates = results.affiliates;
//console.log('affiliate_data: ', results.affiliates);
const host_colours = hostColours($hostFilter, colours, hosts);
//console.log('host_colours:', host_colours);
colours.sort();
const affiliate_colours = affiliateColours($affiliateFilter, colours, affiliates);
//console.log('affiliate_colours:',affiliate_colours);
//console.log('licenses:',licenses );
let filteredTechnologies;
let technologies;
// reactive stuff...
$: {
console.log(
'about to filter technologies by categories - starting with ' +
results.active_services.length +
' technologies...'
);
// filter by categories
filteredTechnologies = filterTechnologiesByArray(
results.active_services,
'categories',
flattenFilter($categoryFilter)
);
console.log(
'about to filter technologies by analogues - starting with ' +
filteredTechnologies.length +
' technologies...'
);
// filter by analogues
filteredTechnologies = filterTechnologiesByArray(
filteredTechnologies,
'analogues',
flattenFilter($analogueFilter)
);
/*console.log(
'about to filter technologies by license - starting with ' +
filteredTechnologies.length +
' technologies...'
);*/
filteredTechnologies = filterTechnologiesByValue(
filteredTechnologies,
'license',
flattenFilter($licenseFilter)
);
console.log(
'about to filter technologies by instance status - starting with ' +
filteredTechnologies.length +
' technologies...'
);
filteredTechnologies = filterTechnologiesByInstanceValue(
filteredTechnologies,
'status',
flattenFilter($statusFilter)
)
filteredTechnologies = filterTechnologiesByInstanceValue(
filteredTechnologies,
'affiliation',
flattenFilter($affiliateFilter)
)
filteredTechnologies = filterTechnologiesByInstanceValue(
filteredTechnologies,
'host',
flattenFilter($hostFilter)
)
//filteredTechnologies = filterTechnologiesByCategoryList(results.all_services, flattenFilter($categoryFilter));
console.log(
'about to sort technologies alphabetically - starting with ' +
filteredTechnologies.length +
' technologies...'
);
const technologies = sortTechnologies(filteredTechnologies);
//console.log('finally have ' + technologies.length + ' technologies left to show...');
}
</script>
<div class="webservices">
<div class="introduction">
<div class="summary-wrapper">
<div class="summary">
<h2>Statistics</h2>
<ul>
<li>
<span class="label">Current web service instances:
<span class="number">{results.stats.instances}</span>
</span>
</li>
<li>
<span class="label">Active web service technologies:
<span class="number">{results.stats.current}</span>
</span>
</li>
<li>
<span class="label">New technologies to be added:
<span class="number">{results.stats.future}</span>
</span>
</li>
<li>
<span class="label">Licenses used by these
<span class="number">{results.stats.licensed_technologies}</span>
technologies:
<span class="number">{results.stats.licenses}</span>
<ul>
<li>
<span class="label">'strong' copyleft licenses:
<span class="number">{results.stats.strong}</span>
(<span class="number">{percent(results.stats.strong, results.stats.licensed_technologies)}</span>)
</span>
</li>
<li>
<span class="label">'weak' open source licenses :
<span class="number">{results.stats.weak}</span>
(<span class="number">{percent(results.stats.weak, results.stats.licensed_technologies)}</span>)
</span>
</li>
</ul>
</span>
</li>
<li>
<span class="label">Number of hosts:
<span class="number">{results.stats.hosts}</span>
</span>
</li>
</ul>
</div>
</div>
<h1>Web Services</h1>
<p>
This sites exists to provide an 'always-up-to-date', in-depth description of the
<a href="https://tech.oeru.org/foss-libresoftware-its-about-clarity-and-values"
title="What do I mean by 'libre' software?">libre software</a>
web services I (<a href="https://davelane.nz"
title="Who is this Dave Lane character?">Dave Lane</a>) have set up and maintain.
</p>
<p>
These services are run primarily on commodity Linux Virtual Private Servers, aka 'hosts',
which I also maintain. Some systems are also run on real physical hosts either in my home
hosting facility or in commercial hosting facilities. These hosts are based in Aotearoa NZ,
Singapore, the US, and in Germany. Almost all run an LTS (Long Term Support) version of Ubuntu
Linux although a few hosts run Debian Linux.
</p>
<p>
Almost all the hosts are <a href="https://tech.oeru.org/creating-your-own-oer-foundation-style-libre-self-hosting-infrastructure-docker-compose-and-ubuntu">configured in a particular way</a> which allows multiple services to run harmoniously on the same host. I use a set of somewhere
between one and twenty-one Docker containers (which I run via Docker Compose) for each service,
to keep different services, each of which has different software dependencies, from interfering
with one another. Each service is made available to the Internet via a 'reverse proxy' (for which
I use Nginx) and user interactions with them are secured (encrypted) with Let's Encrypt SSL certificates (which renew automatically).
</p>
<p>
To manage the servers and keep them up-to-date, I use SSH to establish secure encrypted
connections to them (via key-based authentication). I run updates of the hosts manually, and
update individual services when required (urgently if updates are security-related). Some of
the Docker containers I run I have built myself, in other cases I use those supplied by the
communities developing the service.
</p>
<p>
One huge advantage of the Docker Compose pattern is that I can rapidly move services between
different hosts, or even replicate services very rapidly for development or staging purposes.
Using them has revolutionised my hosting processes.
</p>
</div>
<Accordions>
<Accordion title="Filters">
<AccordionBody>
<p>
You can filter the software services listed below by the following filtering dimensions.
Click a tag to remove or re-add it, or use 'select all or none' to select none, or invert your selection if it's more useful.</p>
<p><strong>Tip</strong>: if you want to see all technologies tagged with a specific tag, click that tag to <em>deselect</em> it, and then 'invert selection'. You'll see only the technologies that have the resulting tag.
</p>
<Accordions>
<Accordion title="Software Catergory Filter">
<AccordionBody>
<div class="filter-group categories">
<Filterable
filterValues={categoryFilter}
histogram={results.tech_lists.category_list}
context={'categories'}>
<h2 slot="title">Categories</h2>
<p slot="description">
These are general categories of software which help you understand the sort of software you're looking at, as well as identifying functionally similar software for comparison.
</p>
</Filterable>
</div>
</AccordionBody>
</Accordion>
<Accordion title="Software Analogue Filter">
<AccordionBody>
<div class="filter-group analogues">
<Filterable
filterValues={analogueFilter}
{tabindex}
histogram={results.tech_lists.analogue_list}
context={'analogues'}>
<h2 slot="title">Analogues</h2>
<p slot="description">
Because all the software listed here is '<a
href="https://tech.oeru.org/foss-libresoftware-its-about-clarity-and-values"
>libre</a
>', I've included the names of functionally similar but heavily marketed
<a href="https://davelane.nz/proprietary">proprietary</a> software that is therefore
better known to the general population, also for the purposes of understanding the
use of the software shown and to what it can be compared.
</p>
</Filterable>
</div>
</AccordionBody>
</Accordion>
<Accordion title="License Filter">
<AccordionBody>
<div class="filter-group licenses">
<Filterable
filterValues={licenseFilter}
{tabindex}
histogram={results.tech_lists.license_list}
context={'licenses'}
>
<h2 slot="title">Open Source & Copyleft Licenses</h2>
<p slot="description">
These are the various '<a href="https://opensource.org/licenses">open source</a
>' and '<a href="https://copyleft.org">copyleft</a>' licenses that apply to the
software listed. You can chose
</p>
</Filterable>
</div>
</AccordionBody>
</Accordion>
<Accordion title="Service Status Filter">
<AccordionBody>
<div class="filter-group statuses">
<Filterable
filterValues={statusFilter}
{tabindex}
histogram={results.instance_lists.status_list}
context={'statuses'}
>
<h2 slot="title">Statuses</h2>
<p slot="description"></p>
</Filterable>
</div>
</AccordionBody>
</Accordion>
<Accordion title="Service Affiliates Filter">
<AccordionBody>
<div class="filter-group affilates">
<Filterable filterValues={affiliateFilter}
{tabindex} histogram={results.instance_lists.affiliate_list}
context={'affiliates'}
colours={affiliate_colours}><h2 slot="title">Affiliates</h2>
<p slot="description"></p>
</Filterable>
</div>
</AccordionBody>
</Accordion>
<Accordion title="Service Host Filter">
<AccordionBody>
<div class="filter-group hosts">
<Filterable filterValues={hostFilter}
{tabindex}
histogram={results.instance_lists.host_list}
context={'hosts'}
colours={host_colours}><h2 slot="title">Hosts</h2>
<p slot="description"></p>
</Filterable>
</div>
</AccordionBody>
</Accordion>
</Accordions>
</AccordionBody>
</Accordion>
</Accordions>
<p>Showing {filteredTechnologies.length} active technology tiles.</p>
<!--<p>Current URL: {$page.url}...</p>-->
<div class="tiles">
{#each filteredTechnologies as technology, index}
<!--{#if (index != 1) }-->
<TechnologyTile {technology} {affiliate_colours} {licenses}/>
<!--{:else}
<TechnologyTile {technology} {affiliate_colours} {licenses} {showModal}/>
{/if}-->
{:else}
<p class='no-tiles'>No technologies with active instances are selected.<br/> If you selected a filter tag but no technologies appear, it is because that technology is in a testing or planning stage and does not yet have a publicly visible instance.</p>
{/each}
</div>
</div>
<style>
.webservices {
display: grid;
margin: 0 auto 3em auto;
padding: 0;
position: relative;
}
.introduction {
width: 100%;
}
.summary-wrapper {
content: '';
display: block;
float: right;
padding: 0;
margin: 0;
}
.summary {
border-radius: 15px;
float: right;
clear: both;
/*display: block;*/
background-color: #ccc;
/*display: block;*/
max-width: 30%;
min-width: 30em;
border: 2px #aaa solid;
margin: 0 15px 20px 6px;
padding: 10px 20px;
box-shadow: 6px 6px 20px #999;
}
.summary .label {
color: #666;
}
.summary .number {
color: #000;
}
@media screen and (max-width: 600px) {
.summary {
float: none;
width: 90%;
margin: auto;
}
}
</style>