626 lines
18 KiB
Svelte
626 lines
18 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 Filterable from '$lib/components/Filterable.svelte';
|
|
// collapsible sections
|
|
import CollapsibleSection from '$lib/components/CollapsibleSection.svelte'
|
|
|
|
//console.log('colours: ', colours);
|
|
|
|
//
|
|
// digest and return useful info from the Webservices JSON feed
|
|
function processData(webservices) {
|
|
let instances = 0;
|
|
let current = [];
|
|
let future = [];
|
|
// 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 = [];
|
|
//
|
|
// 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];
|
|
if (tech.hasOwnProperty('categories') && tech.categories.constructor === Array) {
|
|
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['name'] = key;
|
|
current.push(tech);
|
|
//console.log(tech.name + ': ' + tech.instances.length + ' instances...');
|
|
tech.instances.forEach(function (instance, i) {
|
|
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 {
|
|
future[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;
|
|
}
|
|
|
|
return {
|
|
total_instances: instances,
|
|
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
|
|
};
|
|
}
|
|
|
|
// console.log(technologies);
|
|
// 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;
|
|
}
|
|
|
|
// return an object with ob's keys ordered alphabetically, with an idfrom 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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// combine an array of terms into a sentence with proper Oxford commas, dealing with the special cases
|
|
// of one or two elements.
|
|
function toOxfordCommaString(arr) {
|
|
if (arr.length == 1) return arr;
|
|
if (arr.length == 2) return arr.join(' and ');
|
|
else {
|
|
var last = arr.pop();
|
|
return arr.join(', ') + ', and ' + last;
|
|
}
|
|
}
|
|
|
|
// return a singular or plural term depending on the value of 'count'
|
|
function pluraliser(singular, plural, count) {
|
|
if (count > 1) return plural;
|
|
return singular;
|
|
}
|
|
|
|
// 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) {
|
|
//console.log('affiliate:', affiliate);
|
|
let affiliate = affiliate_list[index].name;
|
|
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));
|
|
}
|
|
|
|
// 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 a list of Categories
|
|
function filterTechnologiesByCategoryList(technologies, list) {
|
|
console.log('looking for tech in categories: ', list);
|
|
const included = [];
|
|
technologies.forEach(function (tech) {
|
|
if (hasInstances(tech)) {
|
|
const intersection = inCommon(tech.categories, list);
|
|
//console.log('categories: ', tech.categories);
|
|
//console.log('list: ', list);
|
|
console.log('intersection: ', intersection);
|
|
if (intersection && intersection.constructor === Array) {
|
|
//console.log('found intersection!', intersection.constructor);
|
|
console.log('intersection length: ', intersection.length);
|
|
if (intersection.length > 0) {
|
|
//console.log('including tech: ', tech);
|
|
included.push(tech);
|
|
}
|
|
} else {
|
|
console.log('intersection not array');
|
|
}
|
|
}
|
|
});
|
|
console.log('found ' + included.length + ' technologies.');
|
|
return included;
|
|
}
|
|
|
|
const results = processData(webservices);
|
|
|
|
//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);
|
|
|
|
let filteredTechnologies;
|
|
let technologies;
|
|
|
|
// reactive stuff...
|
|
$: {
|
|
console.log('about to filterTechnologiesByCategoryList');
|
|
filteredTechnologies = filterTechnologiesByCategoryList(results.active_services, flattenFilter($categoryFilter));
|
|
const technologies = sortTechnologies(filteredTechnologies);
|
|
}
|
|
|
|
/*function flipOn(list, id) {
|
|
console.log('list: ' + list + ', id: ' + id);
|
|
return true;
|
|
}*/
|
|
|
|
</script>
|
|
|
|
<div class="webservices">
|
|
<h1>Web Services</h1>
|
|
|
|
<div class="introduction">
|
|
<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 <a href="https://davelane.nz" title="Who is this Dave Lane character?">I</a> have set
|
|
up and maintain.
|
|
</p>
|
|
|
|
</div>
|
|
<div class="summary">
|
|
<ul>
|
|
<li>Total number of services: {results.total_instances}</li>
|
|
</ul>
|
|
</div>
|
|
<CollapsibleSection headerText={'Show or Hide Filters'}>
|
|
<div class="filters">
|
|
<CollapsibleSection headerText={'Show or Hide Software Catergory Filter'}>
|
|
<div class="filter categories">
|
|
<Filterable filterValues={categoryFilter} context={'categories'}>
|
|
<h2 slot="h2">Categories</h2>
|
|
</Filterable>
|
|
</div>
|
|
</CollapsibleSection>
|
|
<CollapsibleSection headerText={'Show or Hide Software Analogue Filter'}>
|
|
<div class="filter analogues">
|
|
<Filterable filterValues={analogueFilter} context={'analogues'}>
|
|
<h2 slot="h2">Analogues</h2>
|
|
</Filterable>
|
|
</div>
|
|
</CollapsibleSection>
|
|
<CollapsibleSection headerText={'Show or Hide License Filter'}>
|
|
<div class="filter licenses">
|
|
<Filterable filterValues={licenseFilter} context={'licenses'}>
|
|
<h2 slot="h2">Open Source & Copyleft Licenses</h2>
|
|
</Filterable>
|
|
</div>
|
|
</CollapsibleSection>
|
|
<CollapsibleSection headerText={'Show or Hide Service Status Filter'}>
|
|
<div class="filter statuses">
|
|
<Filterable filterValues={statusFilter} context={'statuses'}>
|
|
<h2 slot="h2">Statuses</h2>
|
|
</Filterable>
|
|
</div>
|
|
</CollapsibleSection>
|
|
<CollapsibleSection headerText={'Show or Hide Service Affiliates Filter'}>
|
|
<div class="filter affilates">
|
|
<Filterable filterValues={affiliateFilter} context={'affiliates'}>
|
|
<h2 slot="h2">Affiliates</h2>
|
|
</Filterable>
|
|
</div>
|
|
</CollapsibleSection>
|
|
<CollapsibleSection headerText={'Show or Hide Service Host filter'}>
|
|
<div class="filter hosts">
|
|
<Filterable filterValues={hostFilter} context={'hosts'}>
|
|
<h2 slot="h2">Hosts</h2>
|
|
</Filterable>
|
|
</div>
|
|
</CollapsibleSection>
|
|
</div>
|
|
</CollapsibleSection>
|
|
<div class="tiles">
|
|
{#each filteredTechnologies as technology}
|
|
<div class="tile technology">
|
|
<div class="links">
|
|
{#if technology.repository}<span class="repository"
|
|
><a
|
|
href={technology.repository}
|
|
title="The source code repository for {technology.name}">R</a
|
|
></span
|
|
>{/if}
|
|
{#if technology.wikipedia}<span class="wikipedia"
|
|
><a href={technology.wikipedia} title="Wikipedia Page for {technology.name}">W</a
|
|
></span
|
|
>{/if}
|
|
</div>
|
|
<h2><a href={technology.website}>{technology.name}</a></h2>
|
|
<p class="description">
|
|
{technology.description}{#if technology.extended_description}<span
|
|
title={technology.extended_description}>i</span
|
|
>{/if}
|
|
</p>
|
|
{#if technology.categories}<p class="categories">
|
|
{pluraliser('Category', 'Categories', technology.categories.length)}:
|
|
<span class="value">{toOxfordCommaString(technology.categories)}</span>
|
|
</p>{/if}
|
|
{#if technology.analogues}<p class="analogues">
|
|
Alternative to <span class="value">{toOxfordCommaString(technology.analogues)}</span>
|
|
</p>{/if}
|
|
{#if technology.license}<p
|
|
class="license"
|
|
title="The libre license for this project is {technology.license}"
|
|
>
|
|
License: <span class="value">{technology.license}</span>
|
|
</p>{/if}
|
|
{#if hasInstances(technology)}
|
|
<div class="instances">
|
|
<p>
|
|
{#each technology.instances as instance}
|
|
<a
|
|
href="https://{instance.domain}"
|
|
title="{technology.name} instance {instance.domain} hosted on '{instance.host}' by {instance.affiliation}"
|
|
><span
|
|
class="marker circle"
|
|
style="background-color: {affiliate_colours[instance.affiliation].colour}"
|
|
></span></a
|
|
>{/each}
|
|
</p>
|
|
</div>
|
|
{:else}
|
|
<div class="instances">Nothing here yet...</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.webservices {
|
|
display: grid;
|
|
margin: 0 auto 3em auto;
|
|
padding: 0;
|
|
}
|
|
.summary {
|
|
background-color: #ccc;
|
|
display: block;
|
|
}
|
|
.filters {
|
|
/*background-color: #f1ff94;
|
|
padding: 0.5em;
|
|
border: 3px solid #cbea77;
|
|
margin: 1em 0;*/
|
|
margin-bottom: 1em;
|
|
}
|
|
.filters .tags {
|
|
/*padding: 0.5em;*/
|
|
margin: 1em 0;
|
|
display: block;
|
|
}
|
|
.tag-list {
|
|
/* white-space: normal;
|
|
word-break: normal;
|
|
display: inline;*/
|
|
}
|
|
.tags {
|
|
/* white-space: normal;
|
|
word-break: normal;
|
|
display: inline;*/
|
|
}
|
|
.tag {
|
|
font-size: 80%;
|
|
margin-right: 0.5em;
|
|
line-height: 2.5;
|
|
padding: 6px 8px;
|
|
border-radius: 10px;
|
|
white-space: nowrap;
|
|
word-break: normal;
|
|
box-shadow: 3px 3px 3px #6a6d6a;
|
|
}
|
|
.tag:hover { box-shadow: 4px 4px 3px #727372;}
|
|
.tag.category {
|
|
background-color: #9aa34d;
|
|
color: #fff;
|
|
}
|
|
.tag.license {
|
|
background-color: #6498a3;
|
|
color: #fff;
|
|
}
|
|
.tag.status {
|
|
background-color: #a369a2;
|
|
color: #fff;
|
|
}
|
|
.tag.affiliate {
|
|
background-color: #fff;
|
|
color: #000;
|
|
}
|
|
.tag.host {
|
|
background-color: #fff;
|
|
color: #000;
|
|
}
|
|
.tag.on {
|
|
padding-right: 25px;
|
|
background-image: url('/images/check-black-in-white-20px.png');
|
|
background-position: right center;
|
|
background-repeat: no-repeat;
|
|
}
|
|
.tag.off {
|
|
color: #ccc;
|
|
opacity: 0.5;
|
|
}
|
|
.tiles {
|
|
display: grid;
|
|
grid-gap: 15px;
|
|
grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
|
|
}
|
|
/* flip card stuff: https://www.w3schools.com/howto/howto_css_flip_card.asp */
|
|
.tile {
|
|
height: 515px;
|
|
min-width: 240px;
|
|
max-width: 300px;
|
|
/*width: 290px;*/
|
|
overflow: hidden;
|
|
box-sizing: border-box;
|
|
box-shadow: 5px 5px 3px #6a6d6a;
|
|
border: solid 3px #1e6831;
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
position: relative;
|
|
background-color: #eee;
|
|
}
|
|
.tile .links {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
padding: 2px 6px;
|
|
font-size: 90%;
|
|
}
|
|
.tile:hover {
|
|
box-shadow: 10px 10px 6px #727372;
|
|
}
|
|
.tile .links h2 a {
|
|
color: #e6c4fc;
|
|
}
|
|
.tile .links a:visited {
|
|
color: #ced0ff;
|
|
}
|
|
.tile .links a:hover,
|
|
.tile:hover h2 a {
|
|
color: #fff !important;
|
|
}
|
|
.tile h2 {
|
|
background-color: #999;
|
|
text-align: center;
|
|
padding: 0.5em;
|
|
margin: 0;
|
|
}
|
|
.tile h2 a {
|
|
color: #e6c4fc;
|
|
}
|
|
.tile h2 a:visited {
|
|
color: #ced0ff;
|
|
}
|
|
.tile h2 a:hover {
|
|
color: #fff;
|
|
}
|
|
.tile .description {
|
|
height: 260px;
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
}
|
|
.tile p {
|
|
padding: 0 1em 0.2em 1em;
|
|
font-size: 80%;
|
|
color: #555;
|
|
}
|
|
.instances {
|
|
background-color: #ddd;
|
|
position: absolute;
|
|
bottom: 0;
|
|
height: 3em;
|
|
width: 100%;
|
|
}
|
|
.instances .marker {
|
|
margin-right: 3px;
|
|
}
|
|
.webservices li {
|
|
list-style-type: none;
|
|
}
|
|
.value {
|
|
font-weight: bold;
|
|
color: #000;
|
|
}
|
|
.marker {
|
|
vertical-align: middle;
|
|
}
|
|
.circle {
|
|
height: 20px;
|
|
width: 20px;
|
|
background-color: #555;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
}
|
|
.square {
|
|
height: 20px;
|
|
width: 20px;
|
|
background-color: #555;
|
|
display: inline-block;
|
|
}
|
|
</style>
|