Compare commits

..

1 Commits

Author SHA1 Message Date
CNCF-bot cc86e291c1 1.0.649 2022-05-19 09:01:19 +00:00
158 changed files with 5123 additions and 5240 deletions

16
.babelrc.js Normal file
View File

@ -0,0 +1,16 @@
const path = require('path')
module.exports = {
ignore: [".yarn", ".pnp.js"],
presets: ['@babel/preset-env'],
plugins: [
["@babel/plugin-transform-react-jsx", {
"runtime": "automatic"
}],
["module-resolver", {
alias: {
project: process.env.PROJECT_PATH,
dist: path.resolve(process.env.PROJECT_PATH, 'dist', process.env.PROJECT_NAME || '')
}
}]
]
}

View File

@ -1 +0,0 @@
netlify/jsyaml.js

View File

@ -1,19 +0,0 @@
module.exports = {
"extends": "eslint:recommended",
"root": true,
"env": {
"browser": true,
"commonjs": true,
"es2021": true,
"node": true
},
"parserOptions": {
"ecmaVersion": "latest"
},
"rules": {
"no-useless-escape": 0,
"no-prototype-builtins": 0,
"no-empty": 0,
"no-control-regex": 0
}
}

2
.nvmrc
View File

@ -1 +1 @@
v18.3
v18.1

785
.yarn/releases/yarn-3.2.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
enableProgressBars: false
yarnPath: .yarn/releases/yarn-3.2.1.cjs
yarnPath: .yarn/releases/yarn-3.2.0.cjs

View File

@ -2,6 +2,15 @@
X-Robots-Tag: all
Access-Control-Allow-Origin: *
/*.js
Cache-Control: public, max-age=31536000, immutable
/*.css
Cache-Control: public, max-age=31536000, immutable
/*.woff2
Cache-Control: public, max-age=31536000, immutable
/*.svg
Content-Type: image/svg+xml; charset=utf-8

View File

@ -16,4 +16,4 @@ nvm use
npm install -g npm
npm install -g yarn
yarn
yarn node tools/landscapes.js
yarn run babel-node tools/landscapes.js

View File

@ -1,30 +1,19 @@
ip: 147.75.72.237
ip: 145.40.64.211
landscapes:
# name: how we name a landscape project, used on a build server for logs and settings
# repo: a github repo for a specific landscape
# netlify: full | skip - do we build it on a netlify build or not
# hook: - id for a build hook, so it will be triggered after a default branch build
- landscape:
name: lfedge
repo: State-of-the-Edge/lfedge-landscape
hook: 5c80e31894c5c7758edb31e4
- landscape:
name: lfenergy
repo: lf-energy/lfenergy-landscape
hook: 606487bb9da603110d4b8139
- landscape:
name: lf
repo: jmertic/lf-landscape
hook: 606487123224e20fb8f3896e
- landscape:
name: dlt
repo: dltlandscape/dlt-landscape
hook: demo
- landscape:
name: aswf-landscape
repo: AcademySoftwareFoundation/aswf-landscape
hook: 608aa68eb6e5723d5d8a7e00
required: true
- landscape:
name: cncf
repo: cncf/landscape
hook: 5c1bd968fdd72a78a54bdcd1
required: true
- landscape:
name: cdf
repo: cdfoundation/cdf-landscape
@ -43,11 +32,23 @@ landscapes:
repo: graphql/graphql-landscape
hook: 5d5c7ccf64ecb5bd3d2592f7
required: true
- landscape:
name: lf
repo: jmertic/lf-landscape
hook: 606487123224e20fb8f3896e
- landscape:
name: lfai
repo: lfai/landscape
hook: 60648e5b74c76017210a2f53
required: true
- landscape:
name: lfedge
repo: State-of-the-Edge/lfedge-landscape
hook: 5c80e31894c5c7758edb31e4
- landscape:
name: lfenergy
repo: lf-energy/lfenergy-landscape
hook: 606487bb9da603110d4b8139
- landscape:
name: lfph
repo: lfph/lfph-landscape
@ -61,6 +62,10 @@ landscapes:
name: openssf
repo: ossf/ossf-landscape
hook: 613768338a16cb9182b21c3d
- landscape:
name: ospo
repo: todogroup/ospolandscape
hook: 6033b43a2572da242aee6a92
- landscape:
name: presto
repo: prestodb/presto-landscape
@ -77,7 +82,3 @@ landscapes:
name: lfn
repo: lfnetworking/member_landscape
hook: 60788f5fa3cbd68181e3c209
- landscape:
name: riscv
repo: riscv-admin/riscv-landscape
hook: demo

View File

@ -9,5 +9,3 @@ server via rsync.
Most chances is that we will switch to a different build tool soon, so this is
an experimental approach to speedup netlify builds.

View File

@ -1,6 +1,7 @@
// We will execute this script from a landscape build,
// "prepublish": "cp yarn.lock _yarn.lock",
// "postpublish": "rm _yarn.lock || true"
const LANDSCAPEAPP = process.env.LANDSCAPEAPP || "@latest"
const remote = `root@${process.env.BUILD_SERVER}`;
const dockerImage = 'netlify/build:focal';
const dockerHome = '/opt/buildhome';
@ -30,20 +31,28 @@ const debug = function() {
}
}
const runLocal = function(command, showProgress) {
const runLocal = function(command, options = {}) {
const { assignFn, showOutputFn } = options;
// report the output once every 5 seconds
let lastOutput = { s: '', time: new Date().getTime() };
let displayIfRequired = function(text) {
if (showProgress) {
console.info(text);
}
lastOutput.s = lastOutput.s + text;
if (showOutputFn && showOutputFn()) {
if (lastOutput.done || new Date().getTime() > lastOutput.time + 5 * 1000) {
console.info(lastOutput.s);
lastOutput.s = "";
lastOutput.time = new Date().getTime();
};
}
}
return new Promise(function(resolve) {
var spawn = require('child_process').spawn;
var child = spawn('bash', ['-lc',`set -e \n${command}`]);
if (assignFn) {
assignFn(child);
}
let output = [];
child.stdout.on('data', function(data) {
const text = maskSecrets(data.toString('utf-8'));
@ -84,7 +93,7 @@ ${(process.env.BUILDBOT_KEY || '').replace(/\s/g,'\n')}
require('fs').writeFileSync('/tmp/buildbot', key);
require('fs').chmodSync('/tmp/buildbot', 0o600);
const runRemote = async function(command, count = 3) {
const runRemote = async function(command, options) {
const bashCommand = `
nocheck=" -o StrictHostKeyChecking=no "
ssh -i /tmp/buildbot $nocheck ${remote} << 'EOSSH'
@ -92,12 +101,7 @@ const runRemote = async function(command, count = 3) {
${command}
EOSSH
`
const result = await runLocal(bashCommand, true);
if (result.exitCode === 255 && count > 0) {
console.info(`Attempts to retry more: ${count}`);
return await runRemote(command, count - 1);
}
return result;
return await runLocal(bashCommand, options);
};
const runRemoteWithoutErrors = async function(command) {
@ -110,8 +114,16 @@ const runRemoteWithoutErrors = async function(command) {
const makeRemoteBuildWithCache = async function() {
await runLocalWithoutErrors(`
rm -rf packageRemote || true
git clone -b deploy --single-branch https://github.com/cncf/landscapeapp packageRemote
echo extracting
mkdir tmpRemote
cd tmpRemote
rm -rf package || true
# npm pack github:cncf/landscapeapp#vanilla
npm pack interactive-landscape${LANDSCAPEAPP}
tar xzf interactive*.tgz
cd ..
mv tmpRemote/package packageRemote
cp packageRemote/_yarn.lock packageRemote/yarn.lock
`);
//how to get a hash based on our files
@ -144,6 +156,56 @@ const makeRemoteBuildWithCache = async function() {
const hash = getHash();
const tmpHash = require('crypto').createHash('sha256').update(getTmpFile()).digest('hex');
// lets guarantee npm install for this folder first
{
const buildCommand = [
"(ls . ~/.nvm/nvm.sh || (curl -s -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash >/dev/null))",
". ~/.nvm/nvm.sh",
`nvm install ${nvmrc}`,
`echo 0`,
`nvm use ${nvmrc}`,
`echo 1`,
`npm install -g agentkeepalive --save`,
`npm install -g npm --no-progress`,
`npm install -g yarn@latest`,
`cd /opt/repo/packageRemote`,
`yarn >/dev/null`
].join(' && ');
const npmInstallCommand = `
mkdir -p /root/builds/node_cache
ls -l /root/builds/node_cache/${hash}/yarnLocal/unplugged 2>/dev/null || (
mkdir -p /root/builds/node_cache/${tmpHash}/{yarnLocal,nvm,yarnGlobal}
cp -r /root/builds/${folder}/packageRemote/.yarn/* /root/builds/node_cache/${tmpHash}/yarnLocal
chmod -R 777 /root/builds/node_cache/${tmpHash}
docker run --shm-size 1G --rm -t \
-v /root/builds/node_cache/${tmpHash}/yarnLocal:/opt/repo/packageRemote/.yarn \
-v /root/builds/node_cache/${tmpHash}/nvm:${dockerHome}/.nvm \
-v /root/builds/node_cache/${tmpHash}/yarnGlobal:${dockerHome}/.yarn \
-v /root/builds/${folder}:/opt/repo \
${dockerImage} /bin/bash -lc "${buildCommand}"
ln -s /root/builds/node_cache/${tmpHash} /root/builds/node_cache/${hash} || (
rm -rf /root/builds/node_cache/${tmpHash}
)
echo "packages for ${hash} had been installed"
)
chmod -R 777 /root/builds/node_cache/${hash}
`;
debug(npmInstallCommand);
console.info(`Remote with cache: Installing npm packages if required`);
const output = await runRemote(npmInstallCommand);
console.info(`Remote with cache: Output from npm install: exit code: ${output.exitCode}`);
if (output.exitCode !== 0) {
console.info(output.text);
throw new Error('Remote with cahce: npm install failed');
}
const lines = output.text.split('\n');
const index = lines.indexOf(lines.filter( (line) => line.match(/added \d+ packages in/))[0]);
const filteredLines = lines.slice(index !== -1 ? index : 0).join('\n');
console.info(filteredLines || 'Reusing an existing folder for node');
}
// do not pass REVIEW_ID because on failure we will run it locally and report
// from there
const vars = [
@ -159,14 +221,9 @@ const makeRemoteBuildWithCache = async function() {
const outputFolder = 'landscape' + getTmpFile();
const buildCommand = [
`cd /opt/repo/packageRemote`,
"(ls . ~/.nvm/nvm.sh || (curl -s -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash >/dev/null))",
`. ~/.nvm/nvm.sh`,
`cat .nvmrc`,
`nvm install ${nvmrc}`,
`nvm use ${nvmrc}`,
`npm install -g agentkeepalive --save`,
`npm install -g npm@9 --no-progress`,
`npm install -g yarn@latest`,
`yarn`,
`git config --global --add safe.directory /opt/repo`,
`export NODE_OPTIONS="--unhandled-rejections=strict"`,
@ -178,12 +235,16 @@ const makeRemoteBuildWithCache = async function() {
mkdir -p /root/builds/${outputFolder}
chmod -R 777 /root/builds/${outputFolder}
chmod -R 777 /root/builds/${folder}
chmod -R 777 /root/builds/node_cache/${hash}
docker run --shm-size 1G --rm -t \
${vars.map( (v) => ` -e ${v}="${process.env[v]}" `).join(' ')} \
-e NVM_NO_PROGRESS=1 \
-e NETLIFY=1 \
-e PARALLEL=TRUE \
-v /root/builds/node_cache/${hash}/yarnLocal:/opt/repo/packageRemote/.yarn \
-v /root/builds/node_cache/${hash}/nvm:${dockerHome}/.nvm \
-v /root/builds/node_cache/${hash}/yarnGlobal:${dockerHome}/.yarn \
-v /root/builds/${folder}:/opt/repo \
-v /root/builds/${outputFolder}:/dist \
${dockerImage} /bin/bash -lc "${buildCommand}"
@ -215,7 +276,7 @@ const makeRemoteBuildWithCache = async function() {
rsync -az --chmod=a+r -p -e "ssh -i /tmp/buildbot -o StrictHostKeyChecking=no " ${remote}:/root/builds/${outputFolder}/dist/* distRemote
`
));
await runRemote(
await runRemoteWithoutErrors(
`
rm -rf /root/builds/${folder}
rm -rf /root/builds/${outputFolder}
@ -238,13 +299,15 @@ const makeRemoteBuildWithCache = async function() {
}
async function main() {
const path = require('path');
console.info('starting', process.cwd());
process.chdir('..');
await runLocal('rm package*.json');
const cleanPromise = runRemoteWithoutErrors(`
find builds/node_cache -maxdepth 1 -mtime +1 -exec rm -rf {} +;
find builds/ -maxdepth 1 -not -path "builds/node_cache" -mtime +1 -exec rm -rf {} +;
`).catch(function() {
`).catch(function(ex) {
console.info('Failed to clean up a builds folder');
});

View File

@ -1,17 +1,9 @@
if (process.env.KEY3) {
require('fs').mkdirSync(process.env.HOME + '/.ssh', { recursive: true});
require('fs').writeFileSync(process.env.HOME + '/.ssh/bot3',
"-----BEGIN RSA PRIVATE KEY-----\n" +
process.env.KEY3.replace(/\s/g,"\n") +
"\n-----END RSA PRIVATE KEY-----\n\n"
);
require('fs').chmodSync(process.env.HOME + '/.ssh/bot3', 0o600);
console.info('Made a bot3 file');
}
const path = require('path')
const { readdirSync, writeFileSync } = require('fs')
const generateIndex = require('./generateIndex')
const run = function(x) {
console.info(require('child_process').execSync(x).toString())
}
const debug = function() {
if (process.env.DEBUG_BUILD) {
console.info.apply(console, arguments);
@ -31,7 +23,6 @@ const landscapesInfo = yaml.load(require('fs').readFileSync('landscapes.yml'));
const dockerImage = 'netlify/build:focal';
const dockerHome = '/opt/buildhome';
async function main() {
const nvmrc = require('fs').readFileSync('.nvmrc', 'utf-8').trim();
const secrets = [
@ -65,7 +56,7 @@ ${process.env.BUILDBOT_KEY.replace(/\s/g,'\n')}
// now our goal is to run this on a remote server. Step 1 - xcopy the repo
const folder = new Date().getTime();
const remote = 'root@147.75.199.15';
const remote = 'root@147.75.76.177';
const runRemote = async function(command) {
const bashCommand = `
@ -176,6 +167,7 @@ EOSSH
await runRemoteWithoutErrors(`chmod -R 777 /root/builds/${folder}`);
// lets guarantee npm install for this folder first
const branch = process.env.BRANCH;
{
const buildCommand = [
"(ls . ~/.nvm/nvm.sh || (curl -s -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash >/dev/null))",
@ -184,8 +176,7 @@ EOSSH
`nvm use ${nvmrc}`,
`npm install -g yarn --no-progress --silent`,
`cd /opt/repo`,
`yarn >/dev/null`,
`yarn eslint`
`yarn >/dev/null`
].join(' && ');
const npmInstallCommand = `
mkdir -p /root/builds/${folder}_node
@ -216,7 +207,6 @@ EOSSH
const outputFolder = landscape.name + new Date().getTime();
const buildCommand = [
`cd /opt/repo`,
`git config --global --add safe.directory /opt/repo`,
`. ~/.nvm/nvm.sh`,
`nvm use`,
`export NODE_OPTIONS="--unhandled-rejections=strict"`,
@ -324,18 +314,24 @@ EOSSH
await runLocalWithoutErrors('cp -r dist netlify');
if (process.env.BRANCH === 'master') {
console.info(await runLocal('git remote -v'));
await runLocalWithoutErrors(`
git config --global user.email "info@cncf.io"
git config --global user.name "CNCF-bot"
git remote rm github 2>/dev/null || true
git remote add github "git@github.com:cncf/landscapeapp.git"
echo 1
GIT_SSH_COMMAND='ssh -i ~/.ssh/bot3 -o IdentitiesOnly=yes' git fetch github
echo 2
git --no-pager show HEAD
echo 3
GIT_SSH_COMMAND='ssh -i ~/.ssh/bot3 -o IdentitiesOnly=yes' git push github github/master:deploy
git remote add github "https://$GITHUB_USER:$GITHUB_TOKEN@github.com/cncf/landscapeapp"
git fetch github
# git diff # Need to comment this when a diff is too large
git checkout -- .
npm version patch || npm version patch || npm version patch
git commit -m 'Update to a new version [skip ci]' --allow-empty --amend
git branch -D tmp || true
git checkout -b tmp
git push github HEAD:master || true
git push github HEAD:master --tags --force
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
git diff
npm -q publish || (sleep 5 && npm -q publish) || (sleep 30 && npm -q publish)
echo 'Npm package published'
`);
// just for debug purpose
//now we have a different hash, because we updated a version, but for build purposes we have exactly same npm modules

View File

@ -1,176 +0,0 @@
// this "server.js" script should be able to run everything itself, without
// having to bother with any packages or similar problems.
const fs = require('fs/promises');
const path = require('path');
const oldCreateConnection = require('https').globalAgent.createConnection;
require('https').globalAgent.createConnection = function(options, cb) {
options.highWaterMark = 1024 * 1024;
options.readableHighWaterMark = 1024 * 1024;
return oldCreateConnection.apply(this, [options, cb]);
}
// we will get a content of all files, in a form of entries
// "file", "content", "md5"
async function getContent() {
const dirs = ["images", "hosted_logos", "cached_logos"];
const files = ["landscape.yml", "settings.yml", "processed_landscape.yml", "guide.md"];
const all = [];
for (let dir of dirs) {
const filesInDir = await fs.readdir(dir);
for (let file of filesInDir) {
if (file !== '.' && file !== '..') {
const content = await fs.readFile(`${dir}/${file}`, { encoding: 'base64'});
const md5 = require('crypto').createHash('md5').update(content).digest("hex");
all.push({
file: `${dir}/${file}`,
content: content,
md5: md5
});
}
}
}
for (let file of files) {
let content;
try {
content = await fs.readFile(file, { encoding: 'base64'});
} catch(ex) {
}
if (content) {
const md5 = require('crypto').createHash('md5').update(content).digest("hex");
all.push({
file: file,
content: content,
md5: md5
});
}
}
return all;
}
function get(path) {
return new Promise(function(resolve) {
const base = process.env.DEBUG_SERVER ? 'http://localhost:3000' : 'https://weblandscapes.ddns.net';
const http = require(base.indexOf('http://') === 0 ? 'http' : 'https');
const originalPath = path;
path = `${base}/api/console/download/${path}`;
const req = http.request(path, function(res) {
const path1 = originalPath.replace('?', '.html?');
if (res.statusCode === 404 && path.indexOf('.html') === -1) {
get(path1).then( (x) => resolve(x));
} else {
resolve({
res: res,
headers: res.headers,
statusCode: res.statusCode
});
}
});
req.end();
});
}
function post({path, request}) {
return new Promise(function(resolve) {
const base = process.env.DEBUG_SERVER ? 'http://localhost:3000' : 'https://weblandscapes.ddns.net';
const http = require(base.indexOf('http://') === 0 ? 'http' : 'https');
let data = '';
const req = http.request({
hostname: base.split('://')[1].replace(':3000', ''),
port: base.indexOf('3000') !== -1 ? '3000' : 443,
path: path,
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json; charset=UTF-8'
}
}, function(res) {
res.on('data', function(chunk) {
data += chunk;
});
res.on('close', function() {
resolve(JSON.parse(data));
});
});
req.write(JSON.stringify(request));
req.end();
});
}
// a build is done on a server side, instead of a client side, this
// is a key moment
// 1. get content (that is fast)
// 2. send a list of hashes
// 3. get back a list of existing hashes
// 4. send a list of (file, content, hash) , but skip the content when a hash is on the server
// 5. get a build result on a server
let previousGlobalHash;
let lastOutput;
let currentPath;
async function build() {
const files = await getContent();
if (!previousGlobalHash) {
console.info(`Starting a new build...`);
}
if (JSON.stringify(files.map( (x) => x.md5)) === previousGlobalHash) {
return;
}
if (previousGlobalHash) {
console.info(`Changes detected, starting a new build`);
}
previousGlobalHash = JSON.stringify(files.map( (x) => x.md5));
const availableIds = await post({path: '/api/console/ids', request: { ids: files.map( (x) => x.md5 ) }});
const availableSet = new Set(availableIds.existingIds);
const filesWithoutExtraContent = files.map( (file) => ({
file: file.file,
md5: file.md5,
content: availableSet.has(file.md5) ? '' : file.content
}));
const result = await post({path: '/api/console/preview', request: { files: filesWithoutExtraContent }});
if (result.success) {
currentPath = result.path;
console.info(`${new Date().toISOString()} build result: ${result.success ? 'success' : 'failure'} `);
} else {
lastOutput = result.output;
console.info(`${new Date().toISOString()} build result: ${result.success ? 'success' : 'failure'} `);
console.info(result.output);
}
}
function server() {
const http = require('http');
http.createServer(async function (request, response) {
if (!currentPath) {
response.writeHead(404);
if (lastOutput) {
response.end(lastOutput);
} else {
response.end('Site is not ready');
}
} else {
let filePath = request.url.split('?')[0];
const url = path.join(currentPath, 'dist', filePath + '?' + request.url.split('?')[1]);
console.info(`Fetching ${url}`);
const output = await get(url);
response.writeHead(output.statusCode, output.headers);
output.res.pipe(response);
}
}).listen(process.env.PORT || 8001);
console.log(`Development server running at http://127.0.0.1:${process.env.PORT || 8001}/`);
}
async function main() {
server();
//eslint-disable-next-line no-constant-condition
while(true) {
await build();
}
}
main().catch(function(ex) {
console.info(ex);
});
// how will a server work?

View File

@ -1,42 +1,41 @@
{
"name": "interactive-landscape",
"version": "1.0.657",
"version": "1.0.649",
"description": "Visualization tool for building interactive landscapes",
"engines": {
"npm": ">=3",
"node": ">= 10.5"
},
"scripts": {
"eslint": "eslint src tools specs netlify",
"autocrop-images": "node tools/autocropImages",
"autocrop-images": "babel-node tools/autocropImages",
"dev": "yarn server",
"open:src": "yarn server",
"server": "node server.js",
"landscapes": "node tools/landscapes.js",
"server": "babel-node server.js",
"landscapes": "babel-node tools/landscapes.js",
"update-github-colors": "curl https://raw.githubusercontent.com/Diastro/github-colors/master/github-colors.json > tools/githubColors.json",
"fetch": "node tools/validateLandscape && node tools/checkWrongCharactersInFilenames && node tools/addExternalInfo.js && yarn yaml2json",
"fetch": "babel-node tools/validateLandscape && babel-node tools/checkWrongCharactersInFilenames && babel-node tools/addExternalInfo.js && yarn yaml2json",
"fetchAll": "LEVEL=complete yarn fetch",
"light-update": "(rm /tmp/landscape.json || true) && node tools/validateLandscape && yarn remove-quotes && LEVEL=crunchbase node tools/addExternalInfo.js && yarn prune && yarn yaml2json && node tools/updateTimestamps",
"update": "(rm /tmp/landscape.json || true) && node tools/validateLandscape && yarn remove-quotes && LEVEL=medium node tools/addExternalInfo.js && yarn prune && yarn check-links && yarn yaml2json && node tools/updateTimestamps",
"yaml2json": "node tools/generateJson.js",
"remove-quotes": "node tools/removeQuotes",
"prune": "node tools/pruneExtraEntries",
"check-links": "node tools/checkLinks",
"update": "(rm /tmp/landscape.json || true) && babel-node tools/validateLandscape && yarn remove-quotes && LEVEL=medium babel-node tools/addExternalInfo.js && yarn prune && yarn check-links && yarn yaml2json && babel-node tools/calculateNumberOfTweets && babel-node tools/updateTimestamps",
"yaml2json": "babel-node tools/generateJson.js",
"remove-quotes": "babel-node tools/removeQuotes",
"prune": "babel-node tools/pruneExtraEntries",
"check-links": "babel-node tools/checkLinks",
"remove-dist": "rimraf \"$PROJECT_PATH\"/dist",
"precommit": "yarn fetch",
"start-ci": "yarn exec bash -c \"(node tools/distServer.js &) && sleep 10\"",
"stop-old-ci": "node tools/stopOldDistServer.js",
"start-ci": "yarn exec bash -c \"(yarn run babel-node tools/distServer.js &) && sleep 10\"",
"stop-old-ci": "yarn run babel-node tools/stopOldDistServer.js",
"stop-ci": "yarn exec bash -c \"kill -9 `cat /tmp/ci.pid` >/dev/null 2>/dev/null && rm /tmp/ci.pid \"",
"integration-test": "jest --runInBand --reporters=jest-standard-reporter",
"test": "jest",
"check-landscape": "node tools/checkLandscape",
"render-landscape": "node tools/renderLandscape",
"funding": "node tools/fundingForMasterBranch",
"prepare-landscape": "node tools/prepareLandscape.js && node tools/renderItems.js",
"setup-robots": "node tools/sitemap && node tools/addRobots",
"build": "yarn fetch && yarn prepare-landscape && node tools/renderAcquisitions && yarn setup-robots && yarn export-functions && yarn stop-old-ci && yarn start-ci && node tools/parallelWithRetry integration-test check-landscape render-landscape funding && yarn stop-ci",
"export-functions": "node ./tools/exportFunctions",
"check-landscape": "babel-node tools/checkLandscape",
"render-landscape": "babel-node tools/renderLandscape",
"funding": "babel-node tools/fundingForMasterBranch",
"prepare-landscape": "babel-node tools/prepareLandscape.js && babel-node tools/renderItems.js",
"setup-robots": "babel-node tools/sitemap && babel-node tools/addRobots",
"build": "yarn fetch && yarn prepare-landscape && yarn setup-robots && yarn export-functions && yarn stop-old-ci && yarn start-ci && babel-node tools/parallelWithRetry integration-test check-landscape render-landscape funding && yarn stop-ci",
"export-functions": "babel-node ./tools/exportFunctions",
"latest": "yarn",
"reset-tweet-count": "node tools/resetTweetCount.js",
"reset-tweet-count": "babel-node tools/resetTweetCount.js",
"prepublish": "cp yarn.lock _yarn.lock",
"postpublish": "rm _yarn.lock || true",
"preview": "yarn fetch && yarn prepare-landscape && yarn export-functions"
@ -44,15 +43,26 @@
"author": "CNCF",
"license": "Apache-2.0",
"dependencies": {
"@vercel/ncc": "^0.34.0",
"@babel/core": "^7.17.10",
"@babel/node": "^7.17.10",
"@babel/plugin-transform-react-jsx": "^7.17.3",
"@babel/plugin-transform-runtime": "^7.17.10",
"@babel/preset-env": "^7.17.10",
"@babel/register": "^7.17.7",
"@babel/runtime": "^7.17.9",
"@vercel/ncc": "^0.33.4",
"anchorme": "^2.1.2",
"axe-puppeteer": "^1.1.1",
"axios": "^0.27.2",
"babel-plugin-module-resolver": "^4.1.0",
"bluebird": "3.7.2",
"change-case": "^4.1.2",
"cheerio": "^1.0.0-rc.11",
"cheerio": "^1.0.0-rc.10",
"chokidar": "^3.5.3",
"classnames": "^2.3.1",
"colors": "1.4.0",
"compression": "^1.7.4",
"debug": "^4.3.4",
"eslint": "latest",
"event-emitter": "0.3.5",
"expect-puppeteer": "^6.1.0",
"feed": "^4.2.2",
@ -68,8 +78,13 @@
"lodash": "^4.17.21",
"node-emoji": "^1.11.0",
"oauth-1.0a": "^2.2.6",
"puppeteer": "^14.2.1",
"open": "^8.4.0",
"puppeteer": "^13.7.0",
"query-string": "^7.1.1",
"raf": "3.4.1",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"regenerator-runtime": "^0.13.9",
"relative-date": "1.1.3",
"sanitize-html": "^2.7.0",
"showdown": "^2.1.0",
@ -93,5 +108,10 @@
"type": "git",
"url": "https://github.com/cncf/landscapeapp"
},
"packageManager": "yarn@3.2.1"
"dependenciesMeta": {
"open": {
"unplugged": true
}
},
"packageManager": "yarn@3.2.0"
}

View File

@ -7,12 +7,6 @@ const http = require('http');
const fs = require('fs');
const path = require('path');
const fnFile = (file) => {
const functionsPath = path.join(projectPath, 'dist', process.env.PROJECT_NAME || '', 'functions' );
const destFile = [process.env.PROJECT_NAME, file].filter(_ => _).join('--');
return path.join(functionsPath, destFile);
}
http.createServer(function (request, response) {
if (request.url.indexOf('/api/ids') !== -1) {
@ -20,8 +14,7 @@ http.createServer(function (request, response) {
const query = request.url.split('?')[1] || '';
if (!process.env.INLINE_API) {
require('child_process').exec(`node ${fnFile("ids.js")} '${query}'`, {}, function(e, output, err) {
console.info(err);
require('child_process').exec(`babel-node src/api/ids.js '${query}'`, {}, function(e, output, err) {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(output);
});
@ -37,7 +30,7 @@ http.createServer(function (request, response) {
const query = request.url.split('?')[1] || '';
if (!process.env.INLINE_API) {
require('child_process').exec(`node ${fnFile("export.js")} '${query}'`, {}, function(e, output, err) {
require('child_process').exec(`babel-node src/api/export.js '${query}'`, {}, function(e, output, err) {
response.writeHead(200, {
'Content-Type': 'text/css',
'Content-Disposition': 'attachment; filename=interactive-landscape.csv'
@ -54,7 +47,6 @@ http.createServer(function (request, response) {
}
return;
}
let filePath = path.join(process.env.PROJECT_PATH, 'dist', request.url.split('?')[0]);
if (fs.existsSync(path.resolve(filePath, 'index.html'))) {
filePath = path.resolve(filePath, 'index.html');

View File

@ -1,9 +0,0 @@
module.exports = {
globals: {
jest: true,
test: true,
it: true,
describe: true,
expect: true
}
};

View File

@ -1,11 +1,13 @@
const puppeteer = require("puppeteer");
require('expect-puppeteer');
const { paramCase } = require('change-case');
const { settings } = require('../tools/settings');
const { projects } = require('../tools/loadData');
const { landscapeSettingsList } = require("../src/utils/landscapeSettings");
const { appUrl, pathPrefix } = require('../tools/distSettings');
import 'regenerator-runtime/runtime';
import puppeteer from "puppeteer";
import 'expect-puppeteer';
import { paramCase } from 'change-case';
import { settings } from '../tools/settings';
import { projects } from '../tools/loadData';
import { landscapeSettingsList } from "../src/utils/landscapeSettings";
import { appUrl, pathPrefix } from '../tools/distSettings'
const devicesMap = puppeteer.devices;
const width = 1920;
const height = 1080;
@ -38,7 +40,7 @@ async function makePage(initialUrl) {
console.info('retrying...', ex);
browser.close();
} catch(ex2) {
console.info('failed to close browser', ex2);
}
return await makePage(initialUrl);
}
@ -56,29 +58,29 @@ async function waitForHeaderText(page, text) {
await page.waitForFunction(`[...document.querySelectorAll('.sh_wrapper')].find( (x) => x.innerText.includes('${text}'))`);
}
// describe("Embed test", () => {
// describe("I visit an example embed page", () => {
// let frame;
// test('page is open and has a frame', async function(){
// page = await makePage(appUrl + '/embed');
// frame = await page.frames()[1];
// await frame.waitForSelector('.cards-section .mosaic');
// await waitForSelector(frame, '#embedded-footer');
// });
describe("Embed test", () => {
describe("I visit an example embed page", () => {
let frame;
test('page is open and has a frame', async function(){
page = await makePage(appUrl + '/embed');
frame = await page.frames()[1];
await frame.waitForSelector('.cards-section .mosaic');
await frame.waitForXPath(`//h1[contains(text(), 'full interactive landscape')]`);
});
// test('Do not see a content from a main mode', async function() {
// const title = await frame.$('h1', { text: settings.test.header })
// expect(await title.boundingBox()).toBe(null)
// });
test('Do not see a content from a main mode', async function() {
const title = await frame.$('h1', { text: settings.test.header })
expect(await title.boundingBox()).toBe(null)
});
// // ensure that it is clickable
// test('I can click on a tile in a frame and I get a modal after that', async function() {
// await waitForSelector(frame, ".cards-section .mosaic img");
// await frame.click(`.mosaic img`);
// });
// close();
// }, 6 * 60 * 1000); //give it up to 1 min to execute
// });
// ensure that it is clickable
test('I can click on a tile in a frame and I get a modal after that', async function() {
await waitForSelector(frame, ".cards-section .mosaic img");
await frame.click(`.mosaic img`);
});
close();
}, 6 * 60 * 1000); //give it up to 1 min to execute
});
describe("Main test", () => {
describe("I visit a main page and have all required elements", () => {

View File

@ -1,4 +1,5 @@
const { actualTwitter } = require('../../tools/actualTwitter');
import 'regenerator-runtime/runtime';
import actualTwitter from '../../tools/actualTwitter';
describe('Twitter URL', () => {
describe('when crunchbase data not set', () => {

View File

@ -1,30 +1,25 @@
const { flattenItems } = require('../utils/itemsCalculator');
const { getGroupedItems, expandSecondPathItems } = require('../utils/itemsCalculator');
const { parseParams } = require('../utils/routing');
const Parser = require('json2csv/lib/JSON2CSVParser');
const { readJsonFromDist } = require('../utils/readJson');
import path from 'path';
import fs from 'fs';
const allItems = readJsonFromDist('data/items-export');
const projects = readJsonFromDist('data/items');
import allItems from 'dist/data/items-export';
import items from 'dist/data/items';
import settings from 'dist/settings'
const processRequest = module.exports.processRequest = query => {
import { flattenItems } from '../utils/itemsCalculator';
import getGroupedItems from '../utils/itemsCalculator';
import getSummary, { getSummaryText } from '../utils/summaryCalculator';
import { parseParams } from '../utils/routing';
import Parser from 'json2csv/lib/JSON2CSVParser';
export const processRequest = query => {
const params = parseParams(query);
const p = new URLSearchParams(query);
params.format = p.get('format');
let items = projects;
if (params.grouping === 'landscape' || params.format !== 'card') {
items = expandSecondPathItems(items);
}
// extract alias - if grouping = category
// extract alias - if params != card-mode (big_picture - always show)
// i.e. make a copy to items here - to get a list of ids
const selectedItems = flattenItems(getGroupedItems({data: items, skipDuplicates: params.format === 'card', ...params}))
const selectedItems = flattenItems(getGroupedItems({data: items, ...params}))
.reduce((acc, item) => ({ ...acc, [item.id]: true }), {})
const fields = allItems[0].map(([label]) => label !== 'id' && label).filter(_ => _);
const fields = allItems[0].map(([label, _]) => label !== 'id' && label).filter(_ => _);
const itemsForExport = allItems
.map(item => item.reduce((acc, [label, value]) => ({ ...acc, [label]: value }), {}))
.filter(item => selectedItems[item.id]);
@ -35,7 +30,7 @@ const processRequest = module.exports.processRequest = query => {
}
// Netlify function
module.exports.handler = async function(event) {
export async function handler(event, context) {
const body = processRequest(event.queryStringParameters)
const headers = {
'Content-Type': 'text/css',
@ -44,6 +39,6 @@ module.exports.handler = async function(event) {
return { statusCode: 200, body: body, headers }
}
if (__filename === process.argv[1]) {
console.info(processRequest(process.argv[2]), null, 4);
console.info(JSON.stringify(processRequest(process.argv[2]), null, 4));
}

View File

@ -1,23 +1,19 @@
const { expandSecondPathItems } = require('../utils/itemsCalculator');
const { getGroupedItems } = require('../utils/itemsCalculator');
const { getSummary, getSummaryText } = require('../utils/summaryCalculator');
const { parseParams } = require('../utils/routing');
const { readJsonFromDist } = require('../utils/readJson');
import path from 'path';
import fs from 'fs';
import items from 'dist/data/items';
import settings from 'dist/settings'
const projects = readJsonFromDist('data/items');
import { flattenItems } from '../utils/itemsCalculator'
import getGroupedItems from '../utils/itemsCalculator'
import getSummary, { getSummaryText } from '../utils/summaryCalculator';
import { parseParams } from '../utils/routing'
const processRequest = module.exports.processRequest = query => {
export const processRequest = query => {
const params = parseParams(query);
const p = new URLSearchParams(query);
params.format = p.get('format');
let items = projects;
if (params.grouping === 'landscape' || params.format !== 'card') {
items = expandSecondPathItems(items);
}
const summary = getSummary({data: items, ...params});
const groupedItems = getGroupedItems({data: items, skipDuplicates: params.format === 'card', ...params })
const groupedItems = getGroupedItems({data: items, ...params})
.map(group => {
const items = group.items.map(({ id }) => ({ id } ))
return { ...group, items }
@ -30,7 +26,7 @@ const processRequest = module.exports.processRequest = query => {
}
// Netlify function
module.exports.handler = async function(event) {
export async function handler(event, context) {
const body = processRequest(event.queryStringParameters)
const headers = { 'Content-Type': 'application/json' }
return { statusCode: 200, body: JSON.stringify(body), headers }

View File

@ -1,35 +1,25 @@
const { flattenItems, expandSecondPathItems } = require('../utils/itemsCalculator');
const { getGroupedItems } = require('../utils/itemsCalculator');
const { parseParams } = require('../utils/routing');
const { readJsonFromDist } = require('../utils/readJson');
import path from 'path';
import fs from 'fs';
import items from 'dist/data/items';
import settings from 'dist/settings'
const projects = readJsonFromDist('data/items');
const settings = readJsonFromDist('settings');
import { flattenItems } from '../utils/itemsCalculator'
import getGroupedItems from '../utils/itemsCalculator'
import { parseParams } from '../utils/routing'
const processRequest = module.exports.processRequest = query => {
export const processRequest = query => {
const params = parseParams({ mainContentMode: 'card-mode', ...query })
// extract alias - if grouping = category
// extract alias - if params != card-mode (big_picture - always show)
// i.e. make a copy to items here - to get a list of ids
let items = projects;
if (params.grouping === 'landscape') {
items = expandSecondPathItems(items);
}
const groupedItems = getGroupedItems({data: items, ...params, skipDuplicates: true})
const groupedItems = getGroupedItems({items: items, ...params})
.map(group => {
const items = group.items.map(({ id, name, href }) => ({ id, name, logo: `${settings.global.website}/${href}` }))
return { ...group, items }
})
return params.grouping === 'no' ? flattenItems(groupedItems) : groupedItems
}
// Netlify function
module.exports.handler = async function(event) {
export async function handler(event, context) {
const body = processRequest(event.queryStringParameters)
const headers = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true
}
const headers = { 'Content-Type': 'application/json' }
return { statusCode: 200, body: JSON.stringify(body), headers }
}

View File

@ -1,64 +1,83 @@
const _ = require('lodash');
const { assetPath } = require('../utils/assetPath');
const { millify, h } = require('../utils/format');
const { fields } = require('../types/fields');
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import _ from 'lodash';
import assetPath from '../utils/assetPath';
import { millify } from '../utils/format';
import fields from '../types/fields';
function getRelationStyle(relation) {
const relationInfo = fields.relation.valuesMap[relation]
if (relationInfo && relationInfo.color) {
return `border: 4px solid ${relationInfo.color};`;
return {
border: '4px solid ' + relationInfo.color
};
} else {
return '';
return {};
}
}
module.exports.renderDefaultCard = function renderDefaultCard({item}) {
return `
<div data-id="${h(item.id)}" class="mosaic-wrap">
<div class="mosaic ${item.oss ? '' : 'nonoss' }" style="${getRelationStyle(item.relation)}">
<div class="logo_wrapper">
<img loading="lazy" src="${assetPath(item.href)}" class="logo" max-height="100%" max-width="100%" alt="${h(item.name)}" />
export function renderDefaultCard({item}) {
return ReactDOMServer.renderToStaticMarkup(getDefaultCard({item}));
}
export function getDefaultCard({item}) {
const card = (
<div data-id={item.id} className="mosaic-wrap" key={item.id}>
<div className={`mosaic ${item.oss ? '' : 'nonoss' }`} style={getRelationStyle(item.relation)}>
<div className="logo_wrapper">
<img loading="lazy" src={assetPath(item.href)} className='logo' max-height='100%' max-width='100%' alt={item.name} />
</div>
<div class="mosaic-info">
<div class="mosaic-title">
<h5>${h(item.name)}</h5>
${h(item.organization)}
<div className="mosaic-info">
<div className="mosaic-title">
<h5>{item.name}</h5>
{item.organization}
</div>
<div class="mosaic-stars">
${_.isNumber(item.stars) && item.stars ?
`<div>
<div className="mosaic-stars">
{ _.isNumber(item.stars) && item.stars &&
<div>
<span></span>
<span>${h(item.starsAsText)}</span>
</div>` : ''
<span style={{position: 'relative', top: -3}}>{item.starsAsText}</span>
</div>
}
${Number.isInteger(item.amount) ?
`<div class="mosaic-funding">${item.amountKind === 'funding' ? 'Funding: ': 'MCap: '} ${'$'+ h(millify(item.amount))}</div>` : ''
{ Number.isInteger(item.amount) &&
<div className="mosaic-funding">{item.amountKind === 'funding' ? 'Funding: ': 'MCap: '} {'$'+ millify( item.amount )}</div>
}
</div>
</div>
</div>
</div>
`;
);
return card;
}
module.exports.renderFlatCard = function renderFlatCard({item}) {
return `
<div data-id="${item.id}" class="mosaic-wrap">
<div class="mosaic">
<img loading="lazy" src="${assetPath(item.href)}" class="logo" alt="${h(item.name)}" />
<div class="separator"></div>
<h5>${h(item.flatName)}</h5>
</div>
</div>
`;
export function renderFlatCard({item}) {
return ReactDOMServer.renderToStaticMarkup(getFlatCard({item}));
}
module.exports.renderBorderlessCard = function renderBorderlessCard({item}) {
return `
<div data-id="${item.id}" class="mosaic-wrap">
<div class="mosaic">
<img loading="lazy" src="${assetPath(item.href)}" class="logo" alt="${h(item.name)}" />
export function getFlatCard({item}) {
const card = (
<div data-id={item.id} className="mosaic-wrap" key={item.id} >
<div className="mosaic">
<img loading="lazy" src={assetPath(item.href)} className='logo' alt={item.name} />
<div className="separator"/>
<h5>{item.flatName}</h5>
</div>
</div>
`;
);
return card;
}
export function renderBorderlessCard({item}) {
return ReactDOMServer.renderToStaticMarkup(getBorderlessCard({item}));
}
export function getBorderlessCard({item}) {
const card = (
<div data-id={item.id} className="mosaic-wrap" key={item.id}>
<div className="mosaic">
<img loading="lazy" src={assetPath(item.href)} className='logo' alt={item.name} />
</div>
</div>
);
return card;
}

View File

@ -1,40 +1,56 @@
const getContrastRatio = require('get-contrast-ratio').default;
import React from 'react'
import GuideLink from './GuideLink'
import { categoryTitleHeight } from '../utils/landscapeCalculations'
import getContrastRatio from 'get-contrast-ratio'
const { h } = require('../utils/format');
const { renderGuideLink } = require('./GuideLink');
const { categoryTitleHeight } = require('../utils/landscapeCalculations');
const InternalLink = ({to, className, children, ...props}) =>
(<a data-type="internal" href={to} className={className} {...props}>{children}</a>)
module.exports.renderCategoryHeader = function renderCategoryHeader({ href, label, guideAnchor, background, rotate = false }) {
const CategoryHeader = ({ href, label, guideAnchor, background, rotate = false }) => {
const lowContrast = getContrastRatio('#ffffff', background) < 4.5
const color = lowContrast ? '#282828' : '#ffffff'
const backgroundColor = lowContrast ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'
// const infoEl = css.resolve`
// a {
// }
return `
<a data-type="internal"
href="${href}"
style="
display: flex;
justify-content: center;
align-items: center;
text-align: center;
width: 100%;
flex: 1;
font-size: 12px;
color: ${color};
background: none
"
>${h(label)}</a>
${ guideAnchor ? renderGuideLink({label, anchor: guideAnchor, style: `
width: ${categoryTitleHeight - 4}px;
height: ${categoryTitleHeight - 4}px;
margin: 2px;
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
color: ${color};
background: ${backgroundColor};
transform: ${rotate ? 'rotate(180deg)' : 'none' };
`}) : '' }
`;
// a:hover {
// color: ${color};
// background: none;
// }
// a :global(svg) {
// stroke: ${color};
// }
// `
return <>
<InternalLink to={href} style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
width: '100%',
flex: 1,
fontSize: '12px',
color: color,
background: 'none'
}}>{label}</InternalLink>
{ guideAnchor && <
GuideLink label={label} anchor={guideAnchor} style={{
width: categoryTitleHeight - 4,
height: categoryTitleHeight - 4,
margin: '2px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '18px',
color: color,
background: backgroundColor,
transform: rotate ? 'rotate(180deg)' : 'none'
}}
/> }
</>
}
export default CategoryHeader

View File

@ -1,64 +1,61 @@
const _ = require('lodash');
// Render only for an export
const { saneName } = require('../utils/saneName');
const { h } = require('../utils/format');
const { getGroupedItems, expandSecondPathItems } = require('../utils/itemsCalculator');
const { parseParams } = require('../utils/routing');
const { renderDefaultCard, renderBorderlessCard, renderFlatCard } = require('./CardRenderer');
const icons = require('../utils/icons');
import _ from 'lodash';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import getGroupedItems from '../utils/itemsCalculator'
import { parseParams } from '../utils/routing'
import { getDefaultCard, getBorderlessCard, getFlatCard } from './CardRenderer';
module.exports.render = function({items, exportUrl}) {
export function render({settings, items, exportUrl}) {
const params = parseParams(exportUrl.split('?').slice(-1)[0]);
if (params.grouping === 'landscape') {
items = expandSecondPathItems(items);
}
const groupedItems = getGroupedItems({data: items, ...params})
const cardStyle = params.cardStyle || 'default';
const cardFn = cardStyle === 'borderless' ? renderBorderlessCard : cardStyle === 'flat' ? renderFlatCard : renderDefaultCard;
const cardFn = cardStyle === 'borderless' ? getBorderlessCard : cardStyle === 'flat' ? getFlatCard : getDefaultCard;
const linkUrl = exportUrl.replace('&embed=yes', '').replace('embed=yes', '')
const result = `
<div class="modal" style="display: none;">
<div class="modal-shadow"></div>
<div class="modal-container">
<div class="modal-body">
<div class="modal-buttons">
<a class="modal-close">x</a>
<span class="modal-prev">${icons.prev}</span>
<span class="modal-next">${icons.next}</span>
const result = <>
<div className="modal" style={{display: "none"}}>
<div className="modal-shadow" />
<div className="modal-container">
<div className="modal-body">
<div className="modal-buttons">
<a className="modal-close">x</a>
<span className="modal-prev"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path></svg></span>
<span className="modal-next"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"></path></svg></span>
</div>
<div class="modal-content"></div>
<div className="modal-content"></div>
</div>
</div>
</div>
<div id="home" class="app ${cardStyle}-mode">
<div class="app-overlay"></div>
<div class="main-parent">
<div class="app-overlay"></div>
<div class="main">
<div class="cards-section">
<div class="column-content" >
${ groupedItems.map( (groupedItem) => {
const uniqItems = _.uniqBy(groupedItem.items, (x) => x.name + x.logo);
const cardElements = uniqItems.map( (item) => cardFn({item}));
const header = items.length > 0 ? `
<div class="sh_wrapper" data-wrapper-id="${h(saneName(groupedItem.header))}">
<div style="font-size: 24px; padding-left: 16px; line-height: 48px; font-weight: 500;">
<span>${h(groupedItem.header)}</span>
<span class="items-cont">&nbsp;(${uniqItems.length})</span>
<div id="home" className={"app " + cardStyle + "-mode" }>
<div className="app-overlay" />
<div className="main-parent">
<div className="app-overlay"></div>
<div className="main">
<div className="cards-section">
<div className="column-content" >
{ groupedItems.map( (groupedItem) => {
const cardElements = groupedItem.items.map( (item) => cardFn({item}));
const header = items.length > 0 ?
<div className="sh_wrapper">
<div style={{fontSize: '24px', paddingLeft: '16px', lineHeight: '48px', fontWeight: 500}}>
<span>{groupedItem.header}</span>
<span className="items-cont">&nbsp;({groupedItem.items.length})</span>
</div>
</div>` : '';
return [ header, `<div data-section-id="${h(saneName(groupedItem.header))}">${cardElements.join('')}</div>`].join('');
}) }
</div> : null
return [ header, ...cardElements];
})
}
</div>
</div>
<div id="embedded-footer">
<h1 style="margin-top: 20px; width: 100%; text-align: center;">
<a data-type="external" target="_blank" href="${linkUrl}">View</a> the full interactive landscape
<h1 style={{ marginTop: 20, width: '100%', textAlign: 'center' }}>
<a data-type="external" target="_blank" href={linkUrl}>View</a> the full interactive landscape
</h1>
</div>
</div>
</div>
</div>`;
return result;
</div>
</>
return ReactDOMServer.renderToStaticMarkup(result);
};

View File

@ -1,61 +1,68 @@
const { calculateSize } = require("../utils/landscapeCalculations");
import React, { Fragment } from 'react';
import ReactDOMServer from 'react-dom/server';
import { calculateSize } from "../utils/landscapeCalculations";
import _ from 'lodash';
const headerHeight = 40;
module.exports.render = function({landscapeSettings, landscapeContent, version}) {
export function render({landscapeSettings, landscapeContent, version}) {
const { fullscreenWidth, fullscreenHeight } = calculateSize(landscapeSettings);
return `
<div class="gradient-bg" style="
width: ${fullscreenWidth}px;
height: ${fullscreenHeight}px;
overflow: hidden;
"><div class="inner-landscape" style="
width: ${fullscreenWidth}px;
height: ${fullscreenHeight}px;
padding-top: ${headerHeight + 20}px;
padding-left: 20px;
position: relative;
">
${landscapeContent }
<div style="
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
font-size: 18px,
background: rgb(64,89,163);
color: white;
padding-left: 20px;
padding-right: 20px;
padding-top: 3px;
padding-bottom: 3px;
border-radius: 5px;
">${landscapeSettings.fullscreen_header}</div>
${ !landscapeSettings.fullscreen_hide_grey_logos ? `<div style="
position: absolute;
top: 15px;
right: 12px;
font-size: 11px;
background: #eee;
color: rgb(100,100,100);
padding-left: 20px;
padding-right: 20px;
padding-top: 3px;
padding-bottom: 3px;
border-radius: 5px;
">Greyed logos are not open source</div>` : '' }
<div style="
position: absolute;
top: 10px;
left: 15px;
font-size: 14px;
color: white;
">${landscapeSettings.title} </div>
<div style="
position: absolute;
top: 30px;
left: 15px;
font-size: 12px;
color: #eee;
">${version}</div>
const zoom = 1;
return ReactDOMServer.renderToStaticMarkup(
<div className="gradient-bg" style={{
width: fullscreenWidth,
height: fullscreenHeight,
overflow: 'hidden'
}}>
<div className="inner-landscape" style={{
width: fullscreenWidth,
height: fullscreenHeight,
paddingTop: headerHeight + 20,
paddingLeft: 20,
position: 'relative',
}}>
{ landscapeContent }
<div style={{
position: 'absolute',
top: 10,
left: '50%',
transform: 'translateX(-50%)',
fontSize: 18,
background: 'rgb(64,89,163)',
color: 'white',
paddingLeft: 20,
paddingRight: 20,
paddingTop: 3,
paddingBottom: 3,
borderRadius: 5
}}>{landscapeSettings.fullscreen_header}</div>
{ !landscapeSettings.fullscreen_hide_grey_logos && <div style={{
position: 'absolute',
top: 15,
right: 12,
fontSize: 11,
background: '#eee',
color: 'rgb(100,100,100)',
paddingLeft: 20,
paddingRight: 20,
paddingTop: 3,
paddingBottom: 3,
borderRadius: 5
}}>Greyed logos are not open source</div> }
<div style={{
position: 'absolute',
top: 10,
left: 15,
fontSize: 14,
color: 'white',
}}>{landscapeSettings.title} </div>
<div style={{
position: 'absolute',
top: 30,
left: 15,
fontSize: 12,
color: '#eee',
}}>{version}</div>
</div>
</div>`;
</div>
);
}

View File

@ -1,10 +1,18 @@
const { h } = require('../utils/format');
const { guideLink } = require('../utils/icons');
module.exports.renderGuideLink = function({anchor, label, style }) {
const ariaLabel = `Read more about ${label} on the guide`
const to = h(`guide#${anchor}`);
import React from 'react'
return `<a data-type="external" target="_blank" style="${style}" aria-label="${h(ariaLabel)}" href="${to}">
${guideLink}
</a>`;
const OutboundLink = ({to, className, children, ...props}) =>
(<a data-type="external" target="_blank" href={to} className={className} {...props}>{children}</a>)
const GuideLink = ({ anchor, label, className="", ...props }) => {
const svg = <svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"></path>
</svg>;
const ariaLabel = `Read more about ${label} on the guide`
const to = `guide#${anchor}`;
return <OutboundLink className={className} to={to} aria-label={ariaLabel} {...props}>
{svg}
</OutboundLink>
}
export default GuideLink

View File

@ -1,94 +1,99 @@
const _ = require('lodash');
import React from 'react';
import ReactDOMServer from 'react-dom/server';
const { sizeFn } = require('../utils/landscapeCalculations');
const { renderItem } = require('./Item.js');
const { h } = require('../utils/format');
const { assetPath } = require('../utils/assetPath');
const icons = require('../utils/icons');
import { isLargeFn } from '../utils/landscapeCalculations'
import Item from './Item.js'
import _ from 'lodash';
import { guideIcon } from '../icons';
import assetPath from '../utils/assetPath';
// guide is a guide index
module.exports.render = function({settings, items, guide}) {
const currentBranch = require('child_process').execSync(`git rev-parse --abbrev-ref HEAD`, {
cwd: require('../../tools/settings').projectPath
}).toString().trim();
const title = `<h1 className="title" style="margin-top: -5px;">${h(settings.global.short_name)} Landscape Guide</h1>`;
const renderSubcategoryMetadata = ({ node, entries }) => {
const orderedEntries = _.orderBy(entries, (x) => -x.size);
export function render({settings, items, guide }) {
const Title = () => <h1 className="title">{settings.global.short_name} Landscape Guide</h1>;
const SubcategoryMetadata = ({ node, entries }) => {
const orderedEntries = _.orderBy(entries, (x) => !x.isLarge);
const projectEntries = entries.filter(entry => entry.project)
return `
${ (node.buzzwords.length > 0 || projectEntries.length > 0) ? `<div class="metadata">
<div class="header">
return <>
{ (node.buzzwords.length > 0 || projectEntries.length > 0) && <div className="metadata">
<div className="header">
<div>Buzzwords</div>
<div>${h(settings.global.short_name)} Projects</div>
<div>{settings.global.short_name} Projects</div>
</div>
<div class="body">
<div className="body">
<div>
<ul>
${ node.buzzwords.map(str => `<li>${h(str)}</li>`).join('') }
{ node.buzzwords.map(str => <li key={str}>{str}</li>) }
</ul>
</div>
<div>
<ul>
${ projectEntries.map(entry => `<li>${h(entry.name)} (${h(entry.project)})</li>`).join('') }
{ projectEntries.map(entry => <li key={entry.name}>{entry.name} ({entry.project})</li>) }
</ul>
</div>
</div>
</div> ` : '' }
</div> }
<div class="items">
${ orderedEntries.map(entry => renderItem(entry)).join('') }
<div className="items">
{ orderedEntries.map(entry => <Item item={entry} key={entry.id} />) }
</div>
`;
};
</>
}
const renderNavigation = ({ nodes }) => {
const SidebarLink = ({ anchor, level, className = "", children }) => {
const paddingLeft = 20 + (level - 1) * 10;
return <a href={`#${anchor}`} data-level={level} className={`sidebar-link ${className}`} style={{ paddingLeft }}>
{ children }
</a>;
}
const Navigation = ({ nodes }) => {
const links = nodes.filter(node => node.anchor)
const parents = links
.map(n => n.anchor.split('--')[0])
.reduce((acc, n) => ({ ...acc, [n]: (acc[n] || 0) + 1}), {})
return links
.filter(({ title }) => {
.filter(({ title, level, anchor }) => {
return title
})
.map(node => {
const hasChildren = (parents[node.anchor] || 0) > 1
return `
<a href="#${node.anchor}" data-level="${node.level}" class="sidebar-link expandable" style="padding-left: ${10 + node.level * 10}px;">
${h(node.title)} ${hasChildren ? icons.expand : ''}
</a>
${hasChildren ? `
<a href="#${node.anchor}" data-level=${node.level + 1} class="sidebar-link" style="padding-left: 30px;"> Overview </a>
` : ''}
`}).join('');
return <React.Fragment key={node.anchor}>
<SidebarLink className="expandable" anchor={node.anchor} level={node.level}>
{node.title} { hasChildren && <svg viewBox="0 0 24 24"><path d="M10 17l5-5-5-5v10z"></path></svg> }
</SidebarLink>
{hasChildren && <SidebarLink anchor={node.anchor} level={node.level + 1} >
Overview
</SidebarLink>}
</React.Fragment>
})
}
const renderLandscapeLink = ({ landscapeKey, title }) => {
const LandscapeLink = ({ landscapeKey, title }) => {
const href = `card-mode?category=${landscapeKey}`
return `<a href="${href}" target="_blank" class="permalink">${icons.guide} ${h(title)} </a>`;
return <a href={href} target="_blank" className="permalink">
{guideIcon} {title}
</a>
}
const renderContent = ({ nodes, enhancedEntries }) => {
return nodes.map((node) => {
const subcategoryEntries = node.subcategory && enhancedEntries.filter(entry => entry.path.split(' / ')[1].trim() === node.title) || [];
return `<div>
${ node.title ? `<div class="section-title" id="${h(node.anchor)}">
<h2 data-variant="${node.level + 1}">
${ node.landscapeKey
? renderLandscapeLink({landscapeKey: node.landscapeKey, title: node.title})
: h(node.title)
}
</h2>
</div>
` : ''}
${ node.content ? `<div class="guide-content">${node.content}</div>` : ''}
${ node.subcategory ? renderSubcategoryMetadata({entries: subcategoryEntries,node:node}) : '' }
</div>`;
}).join('');
const Content = ({ nodes, enhancedEntries }) => {
return nodes.map((node, idx) => {
const subcategoryEntries = node.subcategory && enhancedEntries.filter(entry => entry.path.split(' / ')[1].trim() === node.title) || []
return <div key={idx}>
{ node.title && <div className="section-title" id={node.anchor}>
<h2 data-variant={node.level + 1}>
{ node.landscapeKey ?
<LandscapeLink landscapeKey={node.landscapeKey} title={node.title} /> :
node.title }
</h2>
</div>}
{ node.content && <div className="guide-content" dangerouslySetInnerHTML={{ __html: node.content }} /> }
{ node.subcategory && <SubcategoryMetadata entries={subcategoryEntries} node={node} /> }
</div>
})
}
const enhancedEntries = items.map( (entry) => {
@ -110,66 +115,58 @@ module.exports.render = function({settings, items, guide}) {
return null;
}
const enhanced = { ...entry, categoryAttrs }
return { ...enhanced, size: sizeFn(enhanced) }
return { ...enhanced, isLarge: isLargeFn(enhanced) }
}).filter( (x) => !!x);
return `
<div class="links">
<div>
<a href="${(settings.global.self_hosted_repo || false) ? "" : "https://github.com/"}${settings.global.repo}/edit/${currentBranch}/guide.md" target="_blank">
${icons.edit}
Edit this page</a>
</div>
<div style="height: 5px;"></div>
<div>
<a href="${(settings.global.self_hosted_repo || false) ? "" : "https://github.com/"}${settings.global.repo}/issues/new?title=Guide Issue" target="_blank">
${icons.github}
Report issue</a>
</div>
</div>
<div class="side-content">
<span class="landscape-logo">
<a aria-label="reset filters" class="nav-link" href="/">
<img alt="landscape logo" src="${assetPath("images/left-logo.svg")} ">
return ReactDOMServer.renderToStaticMarkup (
<>
<div className="side-content">
<span className="landscape-logo">
<a className="nav-link" href="/">
<img src={assetPath("images/left-logo.svg")} />
</a>
</span>
<div class="guide-sidebar">
<div class="sidebar-collapse">+</div>
<div class="guide-toggle">
<span class="toggle-item "><a href="./">Landscape</a></span>
<span class="toggle-item active">Guide</span>
<div className="guide-sidebar">
<div className="sidebar-collapse">+</div>
<div className="guide-toggle">
<span className="toggle-item "><a href="./">Landscape</a></span>
<span className="toggle-item active">Guide</span>
</div>
${renderNavigation({nodes: guide})}
<Navigation nodes={guide} />
</div>
</div>
<div class="guide-header">
<div class="container">
<div class="content">
<button class="sidebar-show" role="none" aria-label="show sidebar">${icons.sidebar}</button>
<span class="landscape-logo">
<a aria-label="reset filters" class="nav-link" href="/">
<img alt="landscape logo" src="${assetPath("/images/left-logo.svg")}">
<div className="guide-header">
<div className="container">
<div className="content">
<button className="sidebar-show">
<svg viewBox="0 0 24 24"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"></path></svg>
</button>
<span className="landscape-logo">
<a className="nav-link" href="/">
<img src={assetPath("/images/left-logo.svg")} />
</a>
</span>
${title}
<Title />
</div>
</div>
<a rel="noopener noreferrer noopener noreferrer"
class="landscapeapp-logo"
title="${h(settings.global.short_name)}"
className="landscapeapp-logo"
title={settings.global.short_name}
target="_blank"
href="${settings.global.company_url}">
<img src="${assetPath("images/right-logo.svg")}" title="${settings.global.short_name}">
href={settings.global.company_url}>
<img src={assetPath("images/right-logo.svg")} title={settings.global.short_name}/>
</a>
</div>
<div class="main-content">
<div class="container">
<div class="content">
${renderContent({nodes: guide,enhancedEntries: enhancedEntries})}
<div className="main-content">
<div className="container">
<div className="content">
<Title />
<Content nodes={guide} enhancedEntries={enhancedEntries} />
</div>
</div>
</div>
`;
</>
);
}

View File

@ -1,79 +1,80 @@
const _ = require('lodash');
const { h } = require('../utils/format');
const { fields, sortOptions, options } = require('../types/fields');
const { assetPath } = require('../utils/assetPath');
const icons = require('../utils/icons');
import _ from 'lodash';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import fields, { sortOptions, options } from '../types/fields'
import assetPath from '../utils/assetPath';
const renderSingleSelect = ({name, options, title}) => (
`
<div class="select" data-type="single" data-name="${name}" data-options="${h(JSON.stringify(options))}">
<select class="select-text" required>
<option value="1" selected>Value</option>
</select>
<span class="select-highlight"></span>
<span class="select-bar"></span>
<label class="select-label">${h(title)}</label>
</div>
`
const OutboundLink = ({to, className, children}) =>
(<a data-type="external" target="_blank" href={to} className={className}>{children}</a>)
const SingleSelect = ({name, options, title}) => (
<div className="select" data-type="single" data-name={name} data-options={JSON.stringify(options)}>
<select className="select-text" required>
<option value="1" selected>Value</option>
</select>
<span className="select-highlight"></span>
<span className="select-bar"></span>
<label className="select-label">{title}</label>
</div>
)
const renderMultiSelect = ({name, options, title}) => (
`
<div class="select" data-type="multi" data-name="${name}" data-options="${h(JSON.stringify(options))}">
<select class="select-text" required>
<option value="1" selected>Value</option>
</select>
<span class="select-highlight"></span>
<span class="select-bar"></span>
<label class="select-label">${h(title)}</label>
</div>
`
const MultiSelect = ({name, options, title}) => (
<div className="select" data-type="multi" data-name={name} data-options={JSON.stringify(options)}>
<select className="select-text" required>
<option value="1" selected>Value</option>
</select>
<span className="select-highlight"></span>
<span className="select-bar"></span>
<label className="select-label">{title}</label>
</div>
)
const renderGroupingSelect = function() {
const GroupingSelect = function() {
const groupingFields = ['landscape', 'relation', 'license', 'organization', 'headquarters'];
const options = [{
id: 'no',
label: 'No Grouping',
}].concat(groupingFields.map(id => ({ id: fields[id].url, label: (fields[id].groupingLabel) })))
return renderSingleSelect({name: "grouping", options, title: "Grouping" });
}].concat(groupingFields.map(id => ({ id: fields[id].url, label: fields[id].groupingLabel })))
return <SingleSelect name="grouping" options={options} title="Grouping" />
}
const renderSortBySelect = function() {
const SortBySelect = function() {
const options = sortOptions.filter( (x) => !x.disabled).map( (x) => ({
id: (fields[x.id] || { url: x.id}).url || x.id, label: x.label
}))
return renderSingleSelect({name: "sort", options, title: "Sort By" });
return <SingleSelect name="sort" options={options} title="Sort By" />
}
const renderFilterCategory = function() {
return renderMultiSelect({name:"category", options: options('landscape'), title: 'Category'});
const FilterCategory = function() {
return <MultiSelect name="category" options={options('landscape')} title="Category" />;
}
const renderFilterProject = function() {
return renderMultiSelect({name:"project", options: options('relation'), title: 'Project'});
const FilterProject = function() {
return <MultiSelect name="project" options={options('relation')} title="Project" />;
}
const renderFilterLicense = function() {
return renderMultiSelect({name:"license", options: options('license'), title: "License"});
const FilterLicense = function() {
return <MultiSelect name="license" options={options('license')} title="License" />;
}
const renderFilterOrganization = function() {
return renderMultiSelect({name: "organization", options: options('organization'), title: "Organization"});
const FilterOrganization = function() {
return <MultiSelect name="organization" options={options('organization')} title="Organization" />;
}
const renderFilterHeadquarters = function() {
return renderMultiSelect({name: "headquarters", options: options('headquarters'), title: "Headquarters"});
const FilterHeadquarters = function() {
return <MultiSelect name="headquarters" options={options('headquarters')} title="Headquarters" />;
}
const renderFilterCompanyType = function() {
return renderMultiSelect({name: "company-type", options: options('companyType'), title: "Company Type"});
const FilterCompanyType = function() {
return <MultiSelect name="company-type" options={options('companyType')} title="Company Type" />;
}
const renderFilterIndustries = function() {
return renderMultiSelect({name: "industries", options: options('industries'), title: "Industry"});
const FilterIndustries = function() {
return <MultiSelect name="industries" options={options('industries')} title="Industry" />;
}
module.exports.render = function({settings, guidePayload, hasGuide, bigPictureKey}) {
export function render({settings, guidePayload, hasGuide, bigPictureKey}) {
const mainCard = [{shortTitle: 'Card', title: 'Card Mode', mode: 'card', url: 'card-mode', tabIndex: 0}]
const landscapes = Object.values(settings.big_picture).map(function(section) {
return {
@ -87,151 +88,164 @@ module.exports.render = function({settings, guidePayload, hasGuide, bigPictureKe
const tabs = _.orderBy(mainCard.concat(landscapes), 'tabIndex').map( item => _.pick(item, ['title', 'mode', 'shortTitle', 'url']))
return `
<div class="select-popup" style="display: none;">
<div class="select-popup-body" ></div>
const result = <>
<div className="select-popup" style={{display: "none"}}>
<div className="select-popup-body"/>
</div>
<div class="modal" style="display: none;">
<div class="modal-shadow" ></div>
<div class="modal-container">
<div class="modal-body">
<div class="modal-buttons">
<a class="modal-close">x</a>
<span class="modal-prev">${icons.prev}</span>
<span class="modal-next">${icons.next}</span>
<div className="modal" style={{display: "none"}}>
<div className="modal-shadow" />
<div className="modal-container">
<div className="modal-body">
<div className="modal-buttons">
<a className="modal-close">x</a>
<span className="modal-prev"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path></svg></span>
<span className="modal-next"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"></path></svg></span>
</div>
<div class="modal-content"></div>
<div className="modal-content"></div>
</div>
</div>
</div>
<div id="guide-page" style="display: ${guidePayload ? "" : "none"};" data-loaded="${guidePayload ? "true" : ""}">
${ !guidePayload ? `<div class="side-content">
<span class="landscape-logo">
<a aria-label="reset filters" class="nav-link" href="/">
<img alt="landscape logo" src="${assetPath("images/left-logo.svg")}" />
<div id="guide-page" style={{display: guidePayload ? "" : "none"}} data-loaded={guidePayload ? "true" : ""}>
{ !guidePayload && <div className="side-content">
<span className="landscape-logo">
<a className="nav-link" href="/">
<img src={assetPath("images/left-logo.svg")} />
</a>
</span>
<div class="guide-sidebar">
<div class="sidebar-collapse">X</div>
<div class="guide-toggle">
<span class="toggle-item "><a href="./">Landscape</a></span>
<span class="toggle-item active">Guide</span>
<div className="guide-sidebar">
<div className="sidebar-collapse">X</div>
<div className="guide-toggle">
<span className="toggle-item "><a href="./">Landscape</a></span>
<span className="toggle-item active">Guide</span>
</div>
</div>
</div>` : ''
</div>
}
${ guidePayload ? "$$guide$$" : ''}
{ guidePayload && "$$guide$$" }
</div>
<div id="home" style="display: ${guidePayload ? "none" : ""}" class="app">
<div class="app-overlay"></div>
<div class="main-parent">
<button class="sidebar-show" role="none" aria-label="show sidebar">${icons.sidebar}</button>
<div class="header_container">
<div class="header">
<span class="landscape-logo">
<a aria-label="reset filters" class="nav-link" href="/">
<img alt="landscape logo" src="${assetPath("images/left-logo.svg")}" />
<div id="home" style={{display: guidePayload ? "none" : ""}} className="app">
<div className="app-overlay" />
<div className="main-parent">
<button className="sidebar-show">
<svg viewBox="0 0 24 24"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"></path></svg>
</button>
<div className="header_container">
<div className="header">
<span className="landscape-logo">
<a className="nav-link" href="/">
<img src={assetPath("images/left-logo.svg")} />
</a>
</span>
<a rel="noopener noreferrer noopener noreferrer"
class="landscapeapp-logo"
title="${h(settings.global.short_name)}"
className="landscapeapp-logo"
title={settings.global.short_name}
target="_blank"
href="${settings.global.company_url}">
<img src="${assetPath("/images/right-logo.svg")}" title="${h(settings.global.short_name)}" />
href={settings.global.company_url}>
<img src={assetPath("/images/right-logo.svg")} title={settings.global.short_name}/>
</a>
</div>
</div>
<div class="sidebar">
<div class="sidebar-scroll">
<div class="sidebar-collapse">+</div>
${ hasGuide ? `
<div class="guide-toggle">
<span class="toggle-item active">Landscape</span>
<span class="toggle-item "><a href="/guide">Guide</a></span>
</div> ` : ''
<div className="sidebar">
<div className="sidebar-scroll">
<div className="sidebar-collapse">+</div>
{ hasGuide &&
<div className="guide-toggle">
<span className="toggle-item active">Landscape</span>
<span className="toggle-item "><a href="/guide">Guide</a></span>
</div>
}
<a class="filters-action reset-filters">${icons.reset}<span>Reset Filters</span>
<a className="filters-action reset-filters">
<svg viewBox="0 0 24 24"><path d="M14 12c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2zm-2-9c-4.97 0-9 4.03-9 9H0l4 4 4-4H5c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.51 0-2.91-.49-4.06-1.3l-1.42 1.44C8.04 20.3 9.94 21 12 21c4.97 0 9-4.03 9-9s-4.03-9-9-9z"/></svg>
<span>Reset Filters</span>
</a>
${renderGroupingSelect()}
${renderSortBySelect()}
${renderFilterCategory()}
${renderFilterProject()}
${renderFilterLicense()}
${renderFilterOrganization()}
${renderFilterHeadquarters()}
${renderFilterCompanyType()}
${renderFilterIndustries()}
<div class="sidebar-presets">
<GroupingSelect />
<SortBySelect />
<FilterCategory />
<FilterProject />
<FilterLicense />
<FilterOrganization />
<FilterHeadquarters />
<FilterCompanyType />
<FilterIndustries />
<a className="filters-action export">
<svg viewBox="0 0 24 24">
<path d="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14zm-1-6h-3V8h-2v5H8l4 4 4-4z" />
</svg>
<span>Download as CSV</span>
</a>
<div className="sidebar-presets">
<h4>Example filters</h4>
${ (settings.presets || []).map(preset => `
<a data-type="internal" class="preset" href="${preset.url}">
${h(preset.label)}
</a> `
).join('')}
</div>
${ (settings.ads || []).map( (entry) => `
<a data-type="external" target="_blank" class="sidebar-event" href="${entry.url}" title="${h(entry.title)}">
<img src="${assetPath(entry.image)}" alt="${entry.title}" />
{ (settings.presets || []).map(preset =>
<a data-type="internal" className="preset" href={preset.url}>
{preset.label}
</a>
`).join('') }
)}
{ (settings.ads || []).map( (entry) => (
<OutboundLink className="sidebar-event" key={entry.image} to={entry.url} title={entry.title}>
<img src={assetPath(entry.image)} alt={entry.title} />
</OutboundLink>
)) }
</div>
</div>
</div>
<div class="app-overlay"></div>
<div className="app-overlay"></div>
<div class="main">
<div class="disclaimer">
<span> ${settings.home.header} </span>
Please <a data-type="external" target="_blank" href="${(settings.global.self_hosted_repo || false) ? "" : "https://github.com/"}${settings.global.repo}">open</a> a pull request to
correct any issues. Greyed logos are not open source. Last Updated: ${process.env.lastUpdated}
<div className="main">
<div className="disclaimer">
<span dangerouslySetInnerHTML={{__html: settings.home.header}} />
Please <OutboundLink to={`https://github.com/${settings.global.repo}`}>open</OutboundLink> a pull request to
correct any issues. Greyed logos are not open source. Last Updated: {process.env.lastUpdated}
</div>
<h4 class="summary"></h4>
<div class="cards-section">
<div class="big-picture-switch big-picture-switch-normal">
${ tabs.map( (tab) => `
<a href="${tab.url}" data-mode="${tab.mode}"><div>${h(tab.title)}</div></a>
`).join('')}
<h4 className="summary" />
<div className="cards-section">
<div className="big-picture-switch big-picture-switch-normal">
{ tabs.map( (tab) => <a href={tab.url} data-mode={tab.mode} key={tab.mode}><div>{tab.title}</div></a> )}
</div>
<div class="right-buttons">
<div class="fullscreen-exit">${icons.fullscreenExit}</div>
<div class="fullscreen-enter">${icons.fullscreenEnter}</div>
<div class="zoom-out">${icons.zoomOut}</div>
<div class="zoom-reset"></div>
<div class="zoom-in">${icons.zoomIn}</div>
<div className="right-buttons">
<div className="fullscreen-exit">
<svg viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"></path></svg>
</div>
<div className="fullscreen-enter">
<svg viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"></path></svg>
</div>
<div className="zoom-out">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11H7v-2h10v2z"></path></svg>
</div>
<div className="zoom-reset"></div>
<div className="zoom-in">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"></path></svg>
</div>
</div>
${ tabs.filter( (x) => x.mode !== 'card').map( (tab) => `
<div data-mode="${tab.mode}" class="landscape-flex">
<div class="landscape-wrapper">
<div class="inner-landscape" style="padding: 10px; display: none;">
${ bigPictureKey === tab.mode ? '$$' + bigPictureKey + '$$' : ''}
</div>
{ tabs.filter( (x) => x.mode !== 'card').map( (tab) =>
<div data-mode={tab.mode} className="landscape-flex">
<div className="landscape-wrapper">
<div className="inner-landscape" style={{padding: 10, display: "none"}} >
{ bigPictureKey === tab.mode && '$$' + bigPictureKey + '$$'}
</div>
</div>
`).join('')}
</div>
)}
<div class="column-content"></div>
<div className="column-content" />
</div>
<div id="footer" style="
margin-top: 10px;
font-size: 9pt;
width: 100%;
text-align: center;">
${h(settings.home.footer)} For more information, please see the&nbsp;
<a data-type="external" target="_blank" eventLabel="crunchbase-terms" href="${(settings.global.self_hosted_repo || false) ? "" : "https://github.com/"}${settings.global.repo}/blob/HEAD/README.md#license">
license
</a> info.
<div id="footer" style={{ marginTop: 10, fontSize:'9pt', width: '100%', textAlign: 'center' }}>
{settings.home.footer} For more information, please see the&nbsp;
<OutboundLink eventLabel="crunchbase-terms" to={`https://github.com/${settings.global.repo}/blob/HEAD/README.md#license`}>
license
</OutboundLink> info.
</div>
<div id="embedded-footer">
<h1 style="margin-top: 20px; width: 100%; text-align: center;">
<a data-type="external" target="_blank" href="url">View</a> the full interactive landscape
<h1 style={{ marginTop: 20, width: '100%', textAlign: 'center' }}>
<OutboundLink to="url">View</OutboundLink> the full interactive landscape
</h1>
</div>
</div>
</div>
</div>
`;
}
</>
return ReactDOMServer.renderToStaticMarkup(result);
};

View File

@ -1,6 +1,10 @@
const { renderItem } = require("./Item");
const { h } = require('../utils/format');
const {
import React, { Fragment, useContext } from "react";
import Item from "./Item";
const InternalLink = ({to, className, children}) =>
(<a data-type="internal" href={to} className={className}>{children}</a>)
import {
calculateHorizontalCategory,
categoryBorder,
categoryTitleHeight,
@ -10,111 +14,107 @@ const {
smallItemHeight,
subcategoryMargin,
subcategoryTitleHeight
} = require("../utils/landscapeCalculations");
const { renderSubcategoryInfo } = require('./SubcategoryInfo');
const { renderCategoryHeader } = require('./CategoryHeader');
} from "../utils/landscapeCalculations";
import SubcategoryInfo from './SubcategoryInfo'
import CategoryHeader from './CategoryHeader'
const renderDivider = (color) => {
const width = dividerWidth;
const marginTop = 2 * subcategoryMargin;
const height = `calc(100% - ${2 * marginTop}px)`;
const Divider = ({ color }) => {
const width = dividerWidth
const marginTop = 2 * subcategoryMargin
const height = `calc(100% - ${2 * marginTop}px)`
return `<div style="
width: ${width}px;
margin-top: ${marginTop}px;
height: ${height};
border-left: ${width}px solid ${color}
"></div>`;
return <div style={{ width, marginTop, height, borderLeft: `${width}px solid ${color}` }}/>
}
module.exports.renderHorizontalCategory = function({ header, guideInfo, subcategories, width, height, top, left, color, href, fitWidth }) {
const HorizontalCategory = ({ header, guideInfo, subcategories, width, height, top, left, color, href, fitWidth }) => {
const addInfoIcon = !!guideInfo;
const subcategoriesWithCalculations = calculateHorizontalCategory({ height, width, subcategories, fitWidth, addInfoIcon })
const totalRows = Math.max(...subcategoriesWithCalculations.map(({ rows }) => rows))
return `
<div style="
width: ${width}px;
left: ${left}px;
height: ${height}px;
top: ${top}px;
position: absolute;
" class="big-picture-section">
return (
<div style={{ width, left, height, top, position: 'absolute' }} className="big-picture-section">
<div
style="
position: absolute;
background: ${color};
top: ${subcategoryTitleHeight}px;
bottom: 0;
left: 0;
right: 0;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2);
padding: ${categoryBorder}px;
"
style={{
position: 'absolute',
background: color,
top: subcategoryTitleHeight,
bottom: 0,
left: 0,
right: 0,
boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2)',
padding: categoryBorder
}}
>
<div style="
top: 0;
bottom: 0;
left: 0;
width: ${categoryTitleHeight}px;
position: absolute;
writing-mode: vertical-rl;
transform: rotate(180deg);
text-align: center;
display: flex;
align-items: center;
justify-content: center;
">
${renderCategoryHeader({href, label: header, guideAnchor: guideInfo, background: color,rotate: true})}
<div style={{
top: 0,
bottom: 0,
left: 0,
width: categoryTitleHeight,
position: 'absolute',
writingMode: 'vertical-rl',
transform: 'rotate(180deg)',
textAlign: 'center',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<CategoryHeader href={href} label={header} guideAnchor={guideInfo} background={color} rotate={true} />
</div>
<div style="
margin-left: 30px;
height: 100%;
display: flex;
justify-content: space-evenly;
background: white;
">
${subcategoriesWithCalculations.map((subcategory, index) => {
<div style={{
marginLeft: 30,
height: '100%',
display: 'flex',
justifyContent: 'space-evenly',
background: 'white'
}}>
{subcategoriesWithCalculations.map((subcategory, index) => {
const lastSubcategory = index !== subcategories.length - 1
const { allItems, guideInfo, columns, width, name, href } = subcategory
const padding = fitWidth ? 0 : `${subcategoryMargin}px 0`;
const style = `
display: grid;
height: 100%;
grid-template-columns: repeat(${columns}, ${smallItemWidth}px);
grid-auto-rows: ${smallItemHeight}px;
`;
const extraStyle = fitWidth ? `justify-content: space-evenly; align-content: space-evenly;` : `grid-gap: ${itemMargin}px;`;
return `
<div style="
width: ${width}px;
position: relative;
overflow: visible;
padding: ${padding};
box-sizing: border-box;
">
<div style="
position: absolute;
top: ${-1 * categoryTitleHeight}px;
left: 0;
right: 0;
height: ${categoryTitleHeight}px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
">
<a data-type="internal" href="${href}" class="white-link">${h(name)}</a>
</div>
<div style="${style} ${extraStyle}">
${allItems.map(renderItem).join('')}
${guideInfo ? renderSubcategoryInfo({label: name, anchor: guideInfo,column: columns, row:totalRows}) : ''}
const padding = fitWidth ? 0 : `${subcategoryMargin}px 0`
const style = {
display: 'grid',
height: '100%',
gridTemplateColumns: `repeat(${columns}, ${smallItemWidth}px)`,
gridAutoRows: `${smallItemHeight}px`
}
const extraStyle = fitWidth ? { justifyContent: 'space-evenly', alignContent: 'space-evenly' } : { gridGap: itemMargin }
const path = [header, name].join(' / ')
return <Fragment key={name}>
<div style={{
width,
position: 'relative',
overflow: 'visible',
padding,
boxSizing: 'border-box'
}}>
<div style={{
position: 'absolute',
top: -1 * categoryTitleHeight,
left: 0,
right: 0,
height: categoryTitleHeight,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center'
}}>
<InternalLink to={href} className="white-link">{name}</InternalLink>
</div>
<div style={{...style, ...extraStyle}}>
{
allItems.map(item => <Item item={item} key={item.name}/>)
}
{ guideInfo && <SubcategoryInfo label={name} anchor={guideInfo} column={columns} row={totalRows}/> } </div>
</div>
${lastSubcategory ? renderDivider(color) : ''}
`
}).join('')}
{lastSubcategory && <Divider color={color}/>}
</Fragment>
})}
</div>
</div>
</div>`;
</div>);
}
export default HorizontalCategory

View File

@ -1,78 +1,64 @@
const { assetPath } = require('../utils/assetPath');
const { fields } = require("../types/fields");
const { h } = require('../utils/format');
import path from 'path';
import fs from 'fs';
import settings from 'dist/settings';
import fields from "../types/fields";
import {
largeItemHeight,
largeItemWidth,
smallItemHeight,
smallItemWidth
} from "../utils/landscapeCalculations";
import assetPath from '../utils/assetPath'
const { readJsonFromDist } = require('../utils/readJson');
const settings = readJsonFromDist('settings');
const largeItem = function(item) {
const isMember = item.category === settings.global.membership;
const LargeItem = ({ item, onClick }) => {
const relationInfo = fields.relation.valuesMap[item.relation]
if (!relationInfo) {
console.error(`no map for ${item.relation} on ${item.name}`);
}
const color = relationInfo.big_picture_color;
const label = relationInfo.big_picture_label;
const textHeight = label ? 10 : 0
const padding = 2
const isMultiline = h(label).length > 20;
const formattedLabel = isMultiline ? h(label).replace(' - ', '<br>') : h(label);
return <div data-id={item.id} className="large-item item" onClick={onClick} style={{ background: color }}>
if (isMember) {
return `
<div data-id="${item.id}" class="large-item large-item-${item.size} item">
<img loading="lazy" src="${assetPath(item.href)}" alt="${item.name}" style="
width: calc(100% - ${2 * padding}px);
height: calc(100% - ${2 * padding + textHeight}px);
padding: 5px;
margin: ${padding}px ${padding}px 0 ${padding}px;
"/>
</div>`;
}
return `
<div data-id="${item.id}" class="large-item large-item-${item.size} item" style="background: ${color}">
<img loading="lazy" src="${assetPath(item.href)}" alt="${item.name}" style="
width: calc(100% - ${2 * padding}px);
height: calc(100% - ${2 * padding + textHeight}px);
padding: 5px;
margin: ${padding}px ${padding}px 0 ${padding}px;
"/>
<div class="label" style="
position: absolute;
bottom: 0;
width: 100%;
height: ${textHeight + padding + (isMultiline ? 6 : 0) }px;
text-align: center;
vertical-align: middle;
background: ${color};
color: white;
font-size: 6.7px;
line-height: ${isMultiline ? 9 : 13 }px;
">${ formattedLabel }</div>
</div>`;
<img loading="lazy" src={assetPath(item.href)} alt={item.name} style={{
width: `calc(100% - ${2 * padding}px)`,
height: `calc(100% - ${2 * padding + textHeight}px)`,
padding: 5,
margin: `${padding}px ${padding}px 0 ${padding}px`
}}/>
<div className="label" style={{
position: 'absolute',
bottom: 0,
width: '100%',
height: `${textHeight + padding}px`,
textAlign: 'center',
verticalAlign: 'middle',
background: color,
color: 'white',
fontSize: '6.7px',
lineHeight: '13px'
}}>{label}</div>
</div>;
}
const smallItem = function(item) {
const SmallItem = ({ item, onClick }) => {
const isMember = item.category === settings.global.membership;
return `
<img data-id="${item.id}"
loading="lazy"
class="item small-item"
src="${assetPath(item.href)}"
alt="${h(item.name)}"
style="border-color: ${isMember ? 'white' : ''};"
/>`
return <>
<img data-id={item.id} loading="lazy" className="item small-item" src={assetPath(item.href)} onClick={onClick} alt={item.name} style={{
borderColor: isMember ? 'white' : undefined
}}/>
</>
}
module.exports.renderItem = function (item) {
const {size, category, oss, categoryAttrs } = item;
const Item = props => {
const { isLarge, category, oss, categoryAttrs } = props.item
const isMember = category === settings.global.membership;
const ossClass = isMember || oss || (categoryAttrs.isLarge && !settings.global.flags?.gray_large_items) ? 'oss' : 'nonoss';
const isLargeClass = size > 1 ? `wrapper-large-${size}` : '';
return `<div class="${isLargeClass + ' item-wrapper ' + ossClass}">
${size > 1 ? largeItem({isMember, ...item}) : smallItem({...item})}
</div>`;
const ossClass = isMember || oss || categoryAttrs.isLarge ? 'oss' : 'nonoss';
const isLargeClass = isLarge ? 'wrapper-large' : '';
return <div className={isLargeClass + ' item-wrapper ' + ossClass}>
{isLarge ? <LargeItem {...props} isMember={isMember} /> : <SmallItem {...props} />}
</div>
}
export default Item

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,13 @@
const _ = require('lodash');
import React, { Fragment } from 'react';
import ReactDOMServer from 'react-dom/server';
import _ from 'lodash';
// Render all items here!
const { renderHorizontalCategory } = require('./HorizontalCategory');
const { renderVerticalCategory } = require('./VerticalCategory');
const { renderLandscapeInfo } = require('./LandscapeInfo');
const { renderOtherLandscapeLink } = require('./OtherLandscapeLink');
import HorizontalCategory from './HorizontalCategory'
import VerticalCategory from './VerticalCategory'
import LandscapeInfo from './LandscapeInfo';
import OtherLandscapeLink from './OtherLandscapeLink';
const extractKeys = (obj, keys) => {
const attributes = _.pick(obj, keys)
@ -14,32 +16,34 @@ const extractKeys = (obj, keys) => {
}
module.exports.render = function({landscapeSettings, landscapeItems}) {
export function getElement({landscapeSettings, landscapeItems}) {
const elements = landscapeSettings.elements.map(element => {
if (element.type === 'LandscapeLink') {
return renderOtherLandscapeLink(element)
return <OtherLandscapeLink {..._.pick(element, ['width','height','top','left','color', 'layout', 'title', 'url', 'image']) }
key={JSON.stringify(element)}
/>
}
if (element.type === 'LandscapeInfo') {
return renderLandscapeInfo(element)
}
const category = landscapeItems.find(c => c.key === element.category);
if (!category) {
console.info(`Can not find the ${element.category}`);
console.info(`Valid values: ${landscapeItems.map( (x) => x.key).join('; ')}`);
return <LandscapeInfo {..._.pick(element, ['width', 'height', 'top', 'left']) } childrenInfo={element.children}
key={JSON.stringify(element)}
/>
}
const category = landscapeItems.find(c => c.key === element.category) || {}
const attributes = extractKeys(element, ['width', 'height', 'top', 'left', 'color', 'fit_width', 'is_large'])
const subcategories = category.subcategories.map(subcategory => {
const allItems = subcategory.allItems.map(item => ({ ...item, categoryAttrs: attributes }))
return { ...subcategory, allItems }
})
if (element.type === 'HorizontalCategory') {
return renderHorizontalCategory({...category, ...attributes, subcategories: subcategories});
}
if (element.type === 'VerticalCategory') {
return renderVerticalCategory({...category, ...attributes, subcategories: subcategories});
}
}).join('');
const Component = element.type === 'HorizontalCategory' ? HorizontalCategory : VerticalCategory
return <Component {...category} subcategories={subcategories} {...attributes} />
});
return `<div style="position: relative;">${elements}</div>`;
return <div style={{ position: 'relative' }}>
{elements}
</div>
};
export function render() {
return ReactDOMServer.renderToStaticMarkup(getElement.apply(this, arguments));
}

View File

@ -1,56 +1,74 @@
const { h } = require('../utils/format');
const { assetPath } = require('../utils/assetPath');
import React from 'react';
import _ from 'lodash';
import assetPath from '../utils/assetPath';
module.exports.renderLandscapeInfo = function({width, height, top, left, children}) {
children = children.map(function(info) {
const positionStyle = `
position: absolute;
top: ${info.top}px;
left: ${info.left}px;
right: ${info.right}px;
bottom: ${info.bottom}px;
width: ${info.width}px;
height: ${info.height}px;
`;
const LandscapeInfo = ({width, height, top, left, childrenInfo}) => {
const children = childrenInfo.map(function(info) {
const positionProps = {
position: 'absolute',
top: _.isUndefined(info.top) ? null : info.top,
left: _.isUndefined(info.left) ? null : info.left,
right: _.isUndefined(info.right) ? null : info.right,
bottom: _.isUndefined(info.bottom) ? null : info.bottom,
width: _.isUndefined(info.width) ? null : info.width,
height: _.isUndefined(info.height) ? null : info.height
};
if (info.type === 'text') {
return `<div key='text' style="
${positionStyle}
font-size: ${info.font_size * 4}px;
font-style: italic;
text-align: justify;
z-index: 1;
"><div style="
position: absolute;
left: 0;
top: 0;
width: 400%;
height: 100%;
transform: scale(0.25);
transform-origin: left;
"> ${h(info.text)} </div></div>`;
// pdf requires a normal version without a zoom trick
const isPdf = false;
if (isPdf) {
return <div key='text' style={{
...positionProps,
fontSize: info.font_size,
fontStyle: 'italic',
textAlign: 'justify',
zIndex: 1
}}>{info.text}</div>
// while in a browser we use a special version which renders fonts
// properly on a small zoom
} else {
return <div key='text' style={{
...positionProps,
fontSize: info.font_size * 4,
fontStyle: 'italic',
textAlign: 'justify',
zIndex: 1
}}><div style={{
position: 'absolute',
left: 0,
top: 0,
width: '400%',
height: '100%',
transform: 'scale(0.25)',
transformOrigin: 'left'
}}> {info.text} </div></div>
}
}
if (info.type === 'title') {
return `<div key='title' style="
${positionStyle}
font-size: ${info.font_size}px;
color: #666;
">${h(info.title)}</div>`;
return <div key='title' style= {{
...positionProps,
fontSize: info.font_size,
color: '#666'
}}>{info.title}</div>
}
if (info.type === 'image') {
return `<img src="${assetPath(`images/${info.image}`)}" style="${positionStyle}" alt="${info.title || info.image}" />`;
return <img src={assetPath(`images/${info.image}`)} style={{...positionProps}} key={info.image} alt={info.title || info.image} />
}
}).join('');
});
return `<div style="
position: absolute;
width: ${width}px;
height: ${height - 20}px;
top: ${top}px;
left: ${left}px;
border: 1px solid black;
background: white;
border-radius: 10px;
margin-top: 20px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
">${children}</div>`
return <div style={{
position: 'absolute',
width: width,
height: height - 20,
top: top,
left: left,
border: '1px solid black',
background: 'white',
borderRadius: 10,
marginTop: 20,
boxShadow: `0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)`
}}>{children}</div>
}
export default LandscapeInfo;

View File

@ -1,78 +0,0 @@
const _ = require('lodash');
const { h } = require('../utils/format');
const l = function(x) {
return h((x || "").replace("https://", ""));
}
const { formatNumber } = require('../utils/formatNumber');
function highlightLinks(s) {
if (!s) {
return '';
}
// markdown styles
s = s.replace(/\[(.*?)\]\((https?:.*?)\)/g, '<a target="_blank" href="$2">$1</a>')
s = s.replace(/(\s|^)(https?:.*?)(\s|$)/g, ' <a target="_blank" href="$2">$2</a> ')
return s;
}
const getDate = function(date) {
if (!date) {
return '';
}
return new Date(date).toISOString().substring(0, 10);
}
const today = getDate(new Date());
module.exports.render = function({items}) {
console.info(items[0], items[0].latestCommitDate);
const old = _.orderBy( items.filter( (x) => x.latestCommitDate && new Date(x.latestCommitDate).getTime() + 3 * 30 * 86400 * 1000 < new Date().getTime()), 'latestCommitDate');
console.info(old.map( (x) => x.name));
return `
<head>
<link rel="icon" type="image/png" href="/favicon.png" />
<style>
</style>
</head>
<body>
<h1>List of obsolete items</h1>
${old.map(function(item) {
return `
<div style="display: flex; flex-direction: row;">
<div style="width: 200px; overflow: hidden; padding: 5px;">
<img src="logos/${item.image_data.fileName}"></img>
</div>
<div style="width: 300px; overflow: hidden; padding: 5px;">
<h3>${item.name}</h1>
<h3>${item.path}</h2>
<h3><span><b>Latest Commit: </b></span> ${getDate(item.latestCommitDate)}</h3>
<h3><span><b>Repo: </b></span> ${highlightLinks(item.repo_url)}</h3>
</div>
<div style="width: 600px; padding: 5px;">
<h4>Manual Actions</h4>
<pre>
1. Create an issue in their repo, inform that that a repo is scheduled for deletion.
2. Mark this item in a <b>landscape.yml</b> with an <b>extra</b> property <b>obsolete_since</b> equal to ${today}
3. After a month remove the entry from the repo
===
Issue Template:
Title: ${item.name} is going to be unreferenced from the interactive landscape because of no activity since ${getDate(item.latestCommitDate)}
Body:
<div style="font-size: 8px;">
Dear project maintainers of ${item.name},
I hope this message finds you well.
I noticed that your project has had no activity for the last 3 months and is about to be removed from the interactive landscape https://landscape.cncf.io/?selected=${item.id}
As a maintainer of an interactive landscape, we have included your project as a reference for our users and would like to ensure that the information we provide is up-to-date.
We understand that maintaining a project can be challenging and time-consuming, and we would like to offer any assistance that we can. Please let us know if there are any plans to continue the development of the project or if there is anything we can do to help.
Thank you for your time and efforts in creating and maintaining this project. We appreciate the value it has provided to the community and hope to continue to reference it in our interactive landscape.
Best regards, CNCF Landscape Team
</div>
</pre>
</div>
</div>
`;
}).join('<hr>')}
</body>
`
}

View File

@ -1,97 +1,78 @@
const { assetPath } = require('../utils/assetPath');
const { h } = require('../utils/format');
import React from 'react';
import assetPath from '../utils/assetPath';
const { stringifyParams } = require('../utils/routing');
const { categoryBorder, categoryTitleHeight, subcategoryTitleHeight } = require('../utils/landscapeCalculations');
const OutboundLink = ({to, className, children}) =>
(<a data-type="external" href={to} className={className}>{children}</a>)
const InternalLink = ({to, className, children}) =>
(<a data-type="tab" href={to} className={className}>{children}</a>)
const renderCardLink = ({ url, children }) => {
if (url.indexOf('http') === 0) {
return `<a data-type=external target=_blank href="${url}" style="display: flex; flex-direction: column;">${children}</a>`;
} else {
url = stringifyParams({ mainContentMode: url });
return `<a data-type=tab href="${url}" style="display: flex; flex-direction: column;">${children}</a>`;
}
};
import { stringifyParams } from '../utils/routing'
import { categoryBorder, categoryTitleHeight, subcategoryTitleHeight } from '../utils/landscapeCalculations'
module.exports.renderOtherLandscapeLink = function({top, left, height, width, color, title, image, url, layout}) {
title = title || ''; //avoid undefined!
const CardLink = ({ url, children }) => {
const Component = url.indexOf('http') === 0 ? OutboundLink : InternalLink
const to = url.indexOf('http') === 0 ? url : stringifyParams({ mainContentMode: url })
return <Component to={to} style={{ display: 'flex', flexDirection: 'column' }}>{children}</Component>
}
const OtherLandscapeLink = function({top, left, height, width, color, title, image, url, layout}) {
const imageSrc = image || assetPath(`images/${url}_preview.png`);
if (layout === 'category') {
return `<div style="
position: absolute;
top: ${top}px;
left: ${left}px;
height: ${height}px;
width: ${width}px;
background: ${color};
overflow: hidden;
cursor: pointer;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
padding: 1px;
display: flex;
">
${renderCardLink({url: url, children: `
<div style="width: ${width}px;height: 30px; line-height: 28px; text-align: center; color: white; font-size: 12px;">${h(title)}</div>
<div style="flex: 1; background: white; position: relative; display: flex; justify-content: center; align-items: center;">
<img loading="lazy" src="${imageSrc}" style="
width: ${width - 12}px;
height: ${height - 42}px;
object-fit: contain;
background-position: center;
background-repeat: no-repeat;" alt="${h(title)}" />
</div>`})}
</div>`;
return <div style={{
position: 'absolute', top, left, height, width, background: color,
overflow: 'hidden',
cursor: 'pointer',
boxShadow: `0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)`,
padding: 1,
display: 'flex'
}}>
<CardLink url={url}>
<div style={{ width, height: 30, lineHeight: '28px', textAlign: 'center', color: 'white', fontSize: 12}}>{title}</div>
<div style={{ flex: 1, background: 'white', position: 'relative', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<img loading="lazy" src={imageSrc} style={{ width: width - 12, height: height - 42,
objectFit: 'contain', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }} alt={title} />
</div>
</CardLink>
</div>
}
if (layout === 'subcategory') {
return `<div style="
width: ${width}px;
left: ${left}px;
height: ${height}px;
top: ${top}px;
position: absolute;
overflow: hidden;">
${renderCardLink({url: url, children: `
return <div style={{ width, left, height, top, position: 'absolute', overflow: 'hidden' }}>
<CardLink url={url}>
<div
style="
position: absolute;
background: ${color};
top: ${subcategoryTitleHeight}px;
bottom: 0;
left: 0;
right: 0;
boxShadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2);
padding: ${categoryBorder}px;
display: flex;
"
style={{
position: 'absolute',
background: color,
top: subcategoryTitleHeight,
bottom: 0,
left: 0,
right: 0,
boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2)',
padding: categoryBorder,
display: 'flex'
}}
>
<div style="
width: ${categoryTitleHeight}px;
writing-mode: vertical-rl;
transform: rotate(180deg);
text-align: center;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 13px;
color: white;
">
${h(title)}
<div style={{
width: categoryTitleHeight,
writingMode: 'vertical-rl',
transform: 'rotate(180deg)',
textAlign: 'center',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12,
lineHeight: '13px',
color: 'white'
}}>
{title}
</div>
<div style="
display: flex;
flex: 1;
background: white;
justify-content: center;
align-items: center; ">
<img loading="lazy" src="${imageSrc}" alt="${h(title)}"
style="width: ${width - 42}px;
height: ${height - 32}px;
object-fit: contain;
background-position: center;
background-repeat: no-repeat;" />
<div style={{ display: 'flex', flex: 1, background: 'white', justifyContent: 'center', alignItems: 'center' }}>
<img loading="lazy" src={imageSrc} alt={title}
style={{ width: width - 42, height: height - 32, objectFit: 'contain', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }} />
</div>
</div>`})}
</div>`
</div>
</CardLink>
</div>;
}
}
export default OtherLandscapeLink;

View File

@ -1,16 +1,19 @@
const { renderGuideLink } = require('./GuideLink');
const { smallItemHeight, smallItemWidth } = require('../utils/landscapeCalculations');
import GuideLink from './GuideLink'
import { smallItemHeight, smallItemWidth } from '../utils/landscapeCalculations'
module.exports.renderSubcategoryInfo = function({ label, anchor, row, column }) {
const style=`
width: ${smallItemWidth}px;
height: ${smallItemHeight}px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
grid-column-start: ${column || 'auto'};
grid-row-start: ${row || 'auto'};
`;
return renderGuideLink({label: label, anchor: anchor, style: style})
const SubcategoryInfo = ({ label, anchor, row, column }) => {
const style={
width: smallItemWidth,
height: smallItemHeight,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '18px',
gridColumnStart: column || 'auto',
gridRowStart: row || 'auto'
};
return <GuideLink label={label} anchor={anchor} style={style}/>
}
export default SubcategoryInfo;

View File

@ -1,624 +0,0 @@
const _ = require('lodash');
const { h } = require('../utils/format');
const l = function(x) {
return h((x || "").replace("https://", ""));
}
const { formatNumber } = require('../utils/formatNumber');
const getLanguages = function(item) {
if (item.extra && item.extra.summary_languages) {
return item.extra.summary_languages;
}
if (item.github_data && item.github_data.languages) {
const total = _.sum(item.github_data.languages.map( (x) => x.value));
const matching = item.github_data.languages.filter( (x) => x.value > total * 0.3).map( (x) => x.name);
return matching.join(', ');
} else {
return '';
}
}
function highlightLinks(s) {
if (!s) {
return '';
}
// markdown styles
s = s.replace(/\[(.*?)\]\((https?:.*?)\)/g, '<a target="_blank" href="$2">$1</a>')
s = s.replace(/(\s|^)(https?:.*?)(\s|$)/g, ' <a target="_blank" href="$2">$2</a> ')
return s;
}
const getDate = function(date) {
if (!date) {
return '';
}
return new Date(date).toISOString().substring(0, 10);
}
module.exports.render = function({items}) {
const projects = items.filter( (x) => !!x.relation && x.relation !== 'member');
const categories = _.uniq(projects.map( (x) => x.path.split(' / ')[0]));
const categoriesCount = {};
const categoryItems = {};
const subcategories = {};
for (let k of categories) {
categoriesCount[k] = projects.filter( (x) => x.path.split(' / ')[0] === k).length;
categoryItems[k] = projects.filter( (x) => x.path.split(' / ')[0] === k).map( (x) => projects.indexOf(x));
const arr = _.uniq(projects.filter( (x) => x.path.split(' / ')[0] === k).map( (x) => x.path.split(' / ')[1]));
for (let subcategory of arr) {
categoryItems[k + ':' + subcategory] = projects.filter( (x) => x.path === k + ' / ' + subcategory).map( (x) => projects.indexOf(x));
}
subcategories[k] = arr;
}
const columnWidth = 250;
return `
<head>
<link rel="icon" type="image/png" href="/favicon.png" />
<style>
${require('fs').readFileSync('src/fonts.css', 'utf-8')}
::root {
--navy: #38404a;
--navy-light: #696D70;
--blue: #2E67BF;
--blue-hover: #1D456B;
--spacing: 1em;
}
body {
color: rgba(0, 0, 0, 0.87);
margin: 0;
font-size: 0.875rem;
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
font-weight: 400;
line-height: 1.43;
letter-spacing: 0.01071em;
background-color: #fafafa;
}
td a {
text-decoration: none;
color: #2E67BF;
}
.category {
font-size: 24px;
font-weight: bold;
}
.categories {
display: inline-block;
margin: 5px;
font-size: 14px;
}
.subcategories {
display: inline-block;
margin: 5px;
font-size: 14px;
}
table {
width: ${columnWidth * projects.length}px;
table-layout: fixed;
border-collapse: separate;
border-spacing: 0;
border-top: 1px solid white;
}
#headers {
width: 152px;
position: sticky;
left: 0;
}
#data {
position: absolute;
left: 155px;
top: 0px;
z-index: -1;
}
td,
th {
white-space: pre-wrap;
width: ${columnWidth}px;
margin: 0;
border: 1px solid white;
border-top-width: 0px;
height: 53px;
padding: 5px;
overflow: hidden;
font-size: 0.8em;
color: var(--navy);
}
.project-name {
text-align: center;
font-size: 15px;
font-weight: bold;
}
html, body {
height: 100%;
overflow: hidden;
padding: 0;
margin: 0;
outline: 0;
}
.table-wrapper {
position: absolute;
top: 165px;
width: calc(100% - 16px);
bottom: 0;
overflow-x: scroll;
overflow-y: scroll;
padding: 0;
left: 16px;
}
.sticky {
position: relative;
background-color: #1b446c;
color: white;
width: 152px;
top: auto;
font-size: 0.8em;
font-weight: bold;
padding: 0px;
}
.first-line {
background-color: #1b446c;
color: white;
}
.alternate-line {
background-color: #e5e5e5;
}
.sticky span {
white-space: nowrap;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
h1 {
font-size: 2.1rem;
line-height: 30px;
display: block;
margin: 0 0 14px;
color: var(--navy);
}
.landscape-logo {
width: 160px;
height: 48px;
display: inline-block;
}
.landscapeapp-logo {
position: absolute;
right: 5px;
top: 14px;
}
.landscapeapp-logo img {
width: 200px;
}
.main-header {
padding: 16px;
}
/* select starting stylings ------------------------------*/
.select {
font-family: 'Roboto';
position: relative;
width: 240px;
margin-bottom: 10px;
}
.select-disabled {
opacity: 0.35;
pointer-events: none;
}
.select-text {
position: relative;
font-family: inherit;
background-color: transparent;
width: 100%;
font-size: 11px;
padding: 10px 22px 10px 0;
border-radius: 0;
border: none;
border-bottom: 1px solid rgba(0,0,0, 0.12);
}
/* Remove focus */
.select-text:focus {
outline: none;
border-bottom: 1px solid rgba(0,0,0, 0);
}
/* Use custom arrow */
.select .select-text {
appearance: none;
-webkit-appearance:none
}
.select:after {
position: absolute;
top: 18px;
right: 10px;
/* Styling the down arrow */
width: 0;
height: 0;
padding: 0;
content: '';
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid rgba(0, 0, 0, 0.12);
pointer-events: none;
}
/* LABEL ======================================= */
.select-label {
color: rgb(105, 109, 112);
font-size: 10px;
font-weight: normal;
position: absolute;
pointer-events: none;
left: 0;
top: 10px;
transition: 0.2s ease all;
}
/* active state */
.select-text:focus ~ .select-label, .select-text ~ .select-label {
top: -10px;
transition: 0.2s ease all;
font-size: 11px;
}
/* BOTTOM BARS ================================= */
.select-bar {
position: relative;
display: block;
width: 100%;
}
.select-bar:before, .select-bar:after {
content: '';
height: 2px;
width: 0;
bottom: 1px;
position: absolute;
background: #2F80ED;
transition: 0.2s ease all;
}
.select-bar:before {
left: 50%;
}
.select-bar:after {
right: 50%;
}
/* active state */
.select-text:focus ~ .select-bar:before, .select-text:focus ~ .select-bar:after {
width: 50%;
}
.select-highlight {
position: absolute;
height: 60%;
width: 100px;
top: 25%;
left: 0;
pointer-events: none;
opacity: 0.5;
}
.info {
position: absolute;
top: 0px;
left: 540px;
font-size: 12px;
line-height: 1.2;
max-width: 500px;
}
</style>
</head>
<body>
<div class="main-header">
<span class="landscape-logo">
<a aria-label="reset filters" class="nav-link" href="/">
<img alt="landscape logo" src="/images/left-logo.svg">
</a>
</span>
<span style="display: inline-block; position: relative; top: -8px; left: 20px;">
<h1>CNCF Project Summary Table</h1>
</span>
<a rel="noopener noreferrer noopener noreferrer" class="landscapeapp-logo" title="CNCF" target="_blank" href="https://www.cncf.io">
<img src="/images/right-logo.svg" title="CNCF">
</a>
</div>
<div style="padding: 16px; position: relative; top: -19px;">
<div class="categories">
<div class="select">
<select class="select-text" required="">
<option value="" selected="">All: ${projects.length}</option>
${categories.map( (name) => `<option value="${name}">${name}: ${categoriesCount[name]}</option>`).join('')}
</select>
<span class="select-highlight"></span>
<span class="select-bar"></span>
<label class="select-label">Category</label>
</div>
</div>
<div class="subcategories" style="display: none">
<div class="select">
<select class="select-text" required="">
</select>
<span class="select-highlight"></span>
<span class="select-bar"></span>
<label class="select-label">Subcategory</label>
</div>
</div>
<div class="info">
The <i>CNCF Project Summary Table</i> provides a standardized, summary of CNCF projects.<br/><div style="height: 5px;"></div>
<b style="color: rgb(58,132,247);">The filters on the left side help refine your view.</b> Start by filtering by category (e.g., <i>orchestration and management</i>) and then subcategory (e.g., <i>service mesh</i> for an overview of all available CNCF service meshes).
</div>
</div>
<div class="table-wrapper">
<table id="headers">
<tr class="landscape first-line">
<td class="sticky">
<span> Project </span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Description</span>
</td>
</tr>
<tr class="landscape alternate-line">
<td class="sticky">
<span>Maturity</span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Target Users</span>
</td>
</tr>
<tr class="landscape alternate-line">
<td class="sticky">
<span>Tags</span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Use Case</span>
</td>
</tr>
<tr class="landscape alternate-line">
<td class="sticky">
<span>Business Use</span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Languages</span>
</td>
</tr>
<tr class="landscape alternate-line">
<td class="sticky">
<span>First Commit</span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Last Commit</span>
</td>
</tr>
<tr class="landscape alternate-line">
<td class="sticky">
<span>Release Cadence</span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Github Stars</span>
</td>
</tr>
<tr class="landscape alternate-line">
<td class="sticky">
<span>Integrations</span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Website</span>
</td>
</tr>
<tr class="landscape alternate-line">
<td class="sticky">
<span>Github</span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Overview Video</span>
</td>
</tr>
</table>
<table id="data">
<tr class="landscape first-line">
${projects.map( (project, index) => `
<td class="project-name" data-project-index="${index}">${h(project.name)}</td>
`).join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => `
<td>${h((project.github_data || project)['description'])}</td>
`).join('')}
</tr>
<tr class="landscape alternate-line">
${projects.map( (project) => `
<td>${h(project.relation)}</td>
`).join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => `
<td>${h((project.extra || {})['summary_personas']) || '&nbsp;'}</td>
`).join('')}
</tr>
<tr class="landscape alternate-line">
${projects.map( (project) => `
<td>${h((project.extra || {})['summary_tags'] || '').split(',').map( (tag) => `<div>- ${tag.trim()}</div>`).join('') }</td>
`).join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => `
<td>${h((project.extra || {})['summary_use_case']) || '&nbsp;'}</td>
`).join('')}
</tr>
<tr class="landscape alternate-line">
${projects.map( (project) => `
<td>${h((project.extra || {})['summary_business_use_case']) || '&nbsp;'}</td>
`).join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => `
<td>${h(getLanguages(project))}</td>
`).join('')}
</tr>
<tr class="landscape alternate-line">
${projects.map( (project) => `
<td>${h(getDate((project.github_start_commit_data || {}).start_date))}</td>
`).join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => `
<td>${h(getDate((project.github_data || {}).latest_commit_date))}</td>
`).join('')}
</tr>
<tr class="landscape alternate-line">
${projects.map( (project) => `
<td>${h((project.extra || {})['summary_release_rate']) || '&nbsp;'}</td>
`).join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => `
<td>${h(formatNumber((project.github_data || {}).stars))}</td>
`).join('')}
</tr>
<tr class="landscape alternate-line">
${projects.map( (project) => `
<td>${highlightLinks((project.extra || {})['summary_integrations']) || '&nbsp;'}</td>
`).join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => `
<td><a href="${h((project.homepage_url))}" target="_blank">${l(project.homepage_url)}</a></td>
`).join('')}
</tr>
<tr class="landscape alternate-line">
${projects.map( (project) => project.repo_url ? `
<td><a href="${h(project.repo_url)}" target="_blank">${l(project.repo_url)}</a></td>
`: '<td>&nbsp;</td>').join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => project.extra && project.extra.summary_intro_url ? `
<td><a href="${h(project.extra.summary_intro_url)}" target="_blank">${l(project.extra.summary_intro_url)}</a></td>
`: '<td>&nbsp;</td>').join('')}
</tr>
</table>
<div style="height: 20px;"></div>
</div>
<script>
function setHeight() {
const rows = [...document.querySelectorAll('#data tr')];
const headersRows = [...document.querySelectorAll('#headers tr')];
for (let row of rows) {
const index = rows.indexOf(row);
const headerEl = headersRows[index].querySelector('td');
const firstEl = [...row.querySelectorAll('td')].filter((x) => x.style.display !== 'none')[0];
headerEl.style.height = (firstEl.getBoundingClientRect().height) + 'px';
}
}
window.App = {
totalCount: ${projects.length},
categories: ${JSON.stringify(categories)},
categoryItems: ${JSON.stringify(categoryItems)},
subcategories: ${JSON.stringify(subcategories)}
};
document.querySelector('.categories select').addEventListener('change', function(e) {
const selectedOption = Array.from(document.querySelectorAll('.categories option')).find( (x) => x.selected);
const categoryId = selectedOption.value;
if (!categoryId) {
document.querySelector('#data').style.width = '';
document.querySelector('.subcategories').style.display = 'none';
} else {
document.querySelector('.subcategories').style.display = '';
const newWidth = ${columnWidth} * App.categoryItems[categoryId].length;
document.querySelector('#data').style.width = newWidth + 'px';
const subcategories = window.App.subcategories[categoryId];
const baseMarkup = '<option value="">All</option>';
const markup = subcategories.map( (s) => '<option value="' + s + '">' + s + ':&nbsp;' + window.App.categoryItems[categoryId + ':' + s].length + '</option>').join('');
document.querySelector('.subcategories select').innerHTML = baseMarkup + markup;
}
for (let tr of [...document.querySelectorAll('tr')]) {
let index = 0;
for (let td of [...tr.querySelectorAll('td')].slice(1)) {
const isVisible = categoryId ? App.categoryItems[categoryId].includes(index) : true;
td.style.display = isVisible ? '' : 'none';
index += 1;
}
}
setHeight();
});
document.querySelector('.subcategories select').addEventListener('change', function(e) {
const categoryId = Array.from(document.querySelectorAll('.categories option')).find( (x) => x.selected).value;
const subcategoryId = Array.from(document.querySelectorAll('.subcategories option')).find( (x) => x.selected).value;
let key = subcategoryId ? (categoryId + ':' + subcategoryId) : categoryId;
const newWidth = ${columnWidth} * App.categoryItems[key].length;
document.querySelector('#data').style.width = newWidth + 'px';
for (let tr of [...document.querySelectorAll('tr')]) {
let index = 0;
for (let td of [...tr.querySelectorAll('td')].slice(1)) {
const isVisible = App.categoryItems[key].includes(index);
td.style.display = isVisible ? '' : 'none';
index += 1;
}
}
setHeight();
});
setHeight();
</script>
</body>
`
}

View File

@ -1,66 +1,53 @@
const { renderItem } = require("./Item");
const { h } = require('../utils/format');
import React, { useContext } from "react";
import Item from "./Item";
const {
const InternalLink = ({to, className, children}) =>
(<a data-type="internal" href={to} className={className}>{children}</a>)
import {
calculateVerticalCategory,
categoryTitleHeight,
itemMargin, smallItemWidth,
subcategoryMargin
} = require("../utils/landscapeCalculations");
const { renderSubcategoryInfo } = require( './SubcategoryInfo');
const { renderCategoryHeader } = require('./CategoryHeader');
} from "../utils/landscapeCalculations";
import SubcategoryInfo from './SubcategoryInfo'
import CategoryHeader from './CategoryHeader'
module.exports.renderVerticalCategory = function({header, guideInfo, subcategories, top, left, width, height, color, href, fitWidth}) {
const subcategoriesWithCalculations = calculateVerticalCategory({ subcategories, fitWidth, width });
return `<div>
<div style="
position: absolute;
top: ${top}px;
left: ${left}px;
height: ${height}px;
width: ${width}px;
background: ${color};
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2);
padding: 0px;
display: flex;
flex-direction: column;
" class="big-picture-section">
<div style="height: ${categoryTitleHeight}px; width: 100%; display: flex;">
${renderCategoryHeader({href: href, label: header, guideAnchor: guideInfo, background: color})}
const VerticalCategory = ({header, guideInfo, subcategories, top, left, width, height, color, href, fitWidth}) => {
const subcategoriesWithCalculations = calculateVerticalCategory({ subcategories, fitWidth, width })
return <div>
<div style={{
position: 'absolute', top, left, height, width, background: color,
boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2)',
padding: 0,
display: 'flex',
flexDirection: 'column'
}} className="big-picture-section">
<div style={{ height: categoryTitleHeight, width: '100%', display: 'flex' }}>
<CategoryHeader href={href} label={header} guideAnchor={guideInfo && guideInfo[header]} background={color} />
</div>
<div style="
width: 100%;
position: relative;
flex: 1;
padding: ${subcategoryMargin}px 0;
display: flex;
flex-direction: column;
justify-content: space-between;
background: white
">
${subcategoriesWithCalculations.map(subcategory => {
const { guideInfo, width, columns, name } = subcategory;
const style = `
display: grid;
grid-template-columns: repeat(${columns}, ${smallItemWidth}px);
`;
const extraStyle = fitWidth ? `justify-content: space-evenly; flex: 1;` : `grid-gap: ${itemMargin}px; `
<div style={{ width: '100%', position: 'relative', flex: 1, padding: `${subcategoryMargin}px 0`, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', background: 'white' }}>
{subcategoriesWithCalculations.map(subcategory => {
const { guideInfo, width, columns, name } = subcategory
const style = { display: 'grid', gridTemplateColumns: `repeat(${columns}, ${smallItemWidth}px)` }
const extraStyle = fitWidth ? { justifyContent: 'space-evenly', flex: 1 } : { gridGap: itemMargin }
const path = [header, name].join(' / ')
return `<div style="
position: relative;
flex-grow: ${subcategory.rows};
display: flex;
flex-direction: column;">
<div style="line-height: 15px; text-align: center;">
<a data-type=internal href="${subcategory.href}">${h(name)}</a>
</div>
<div style="width: ${width}px; overflow: hidden; margin: 0 auto; ${style} ${extraStyle}">
${subcategory.allItems.map(renderItem).join('')}
${guideInfo ? renderSubcategoryInfo({label: name, anchor: guideInfo, column: columns}) : ''}
return <div key={subcategory.name} style={{position: 'relative', flexGrow: subcategory.rows, display: 'flex', flexDirection: 'column' }}>
<div style={{ lineHeight: '15px', textAlign: 'center'}}>
<InternalLink to={subcategory.href}>{name}</InternalLink>
</div>
</div>`
}).join('')}
<div style={{width, overflow: 'hidden', margin: '0 auto', ...style, ...extraStyle}}>
{subcategory.allItems.map(item => <Item item={item} key={item.name} fitWidth={fitWidth} />)}
{ guideInfo && <SubcategoryInfo label={name} anchor={guideInfo} column={columns}/> }
</div>
</div>
})}
</div>
</div>
</div>`;
</div>
}
export default VerticalCategory

View File

@ -1,27 +1,21 @@
// An embedded version of a script
const CncfLandscapeApp = {
init: function() {
// get initial state from the url
setInterval(function() {
window.parent.postMessage({
type: 'landscapeapp-resize',
height: document.body.scrollHeight
}, '*');
document.body.style.height = document.querySelector('.column-content').scrollHeight;
}, 1000);
this.state = {
selected: null
}
if (window.parentIFrame) {
document.addEventListener('keydown', (e) => {
if (e.keyCode === 27) {
if (CncfLandscapeApp.state.selected) {
this.state.selected = null;
this.hideSelectedItem();
}
document.addEventListener('keydown', (e) => {
if (e.keyCode === 27) {
if (CncfLandscapeApp.state.selected) {
this.state.selected = null;
this.hideSelectedItem();
}
});
}
}
});
document.body.addEventListener('click', (e) => {
const cardEl = e.target.closest('[data-id]');
@ -117,21 +111,9 @@ const CncfLandscapeApp = {
},
showSelectedItem: async function(selectedItemId) {
if (!window.parentIFrame) {
window.parent.postMessage({
type: 'landscapeapp-show',
selected: selectedItemId,
location: {
search: window.location.search,
pathname: window.location.pathname
}
}, '*');
return;
}
this.selectedItems = this.selectedItems || {};
if (!this.selectedItems[selectedItemId]) {
const result = await fetch(`${this.basePath}/data/items/info-${selectedItemId}.html`);
const result = await fetch(`/data/items/info-${selectedItemId}.html`);
const text = await result.text();
this.selectedItems[selectedItemId] = text;
}
@ -146,12 +128,16 @@ const CncfLandscapeApp = {
}
//calculate previous and next items;
const selectedItemEl = document.querySelector(`[data-id=${selectedItemId}]`);
const parent = selectedItemEl.closest('.cards-section');
const allItems = parent.querySelectorAll('[data-id]');
const index = [].indexOf.call(allItems, selectedItemEl);
const prevItem = index > 0 ? allItems[index - 1].getAttribute('data-id') : null;
const nextItem = index < allItems.length - 1 ? allItems[index + 1].getAttribute('data-id') : null;
let prevItem = null;
let nextItem = null;
if (true) {
const selectedItemEl = document.querySelector(`[data-id=${selectedItemId}]`);
const parent = selectedItemEl.closest('.cards-section');
const allItems = parent.querySelectorAll('[data-id]');
const index = [].indexOf.call(allItems, selectedItemEl);
prevItem = index > 0 ? allItems[index - 1].getAttribute('data-id') : null;
nextItem = index < allItems.length - 1 ? allItems[index + 1].getAttribute('data-id') : null;
}
this.nextItemId = nextItem;
this.prevItemId = prevItem;

View File

@ -1,4 +1,4 @@
<svg viewBox="0 0 24 24">
export const iconGithub = <svg viewBox="0 0 24 24">
<path d="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58
9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81
5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18
@ -7,4 +7,12 @@
13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39
18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68
14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" />
</svg>
</svg>;
export const iconStar = <svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"></path></svg>
export const guideIcon =
<svg className="guide-icon" viewBox="0 0 18.78 23.66">
<path d="M9.39,9.5a2.09,2.09,0,0,1,0-4.18,2.09,2.09,0,0,1,0,4.18Z"/>
<path d="M3,19.7H17.75a1,1,0,0,0,1-1V1a1,1,0,0,0-1-1H1A1,1,0,0,0,0,1V20.64a3,3,0,0,0,3,3H17.75a1,1,0,1,0,0-2.05H3A1,1,0,0,1,3,19.7ZM9.39,2.9a4.52,4.52,0,0,1,4.5,4.51c0,1.76-2.29,6-3.61,8.27a1,1,0,0,1-1.79,0C7.18,13.41,4.88,9.17,4.88,7.41A4.52,4.52,0,0,1,9.39,2.9Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 826 B

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,31 +1,13 @@
addEventListener('message', function(e) {
if (e.data && e.data.type === 'landscapeapp-resize') {
document.querySelector('#landscape').style.height = e.data.height + 'px';
}
if (e.data && e.data.type === 'landscapeapp-show') {
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
window.landscapeappModalIframe = iframe;
const url = new URL(document.querySelector('#landscape').src);
const search = url.search || '?a=a';
let src = url.origin + url.pathname.replace('/pages/', '/pages-modal/') + search + '&selected=' + e.data.selected;
if (src.indexOf('/pages-modal') === -1) {
//support a version with ?embed=yes
src = src + '&showmodal=yes'
}
iframe.src = src;
iframe.style.position = 'fixed';
iframe.style.left = 0;
iframe.style.top = 0;
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.zIndex = 10000;
iframe.focus();
}
if (e.data && e.data.type === 'landscapeapp-hide') {
const iframe = window.landscapeappModalIframe;
if (iframe) {
document.body.removeChild(iframe);
}
}
document.addEventListener('DOMContentLoaded', function() {
iFrameResize({
log: false,
onMessage : function(messageData){ // Callback fn when message is received
if (messageData.message.type === 'showModal') {
document.querySelector('body').style.overflow = 'hidden';
}
if (messageData.message.type === 'hideModal') {
document.querySelector('body').style.overflow = 'auto';
}
},
}, '#landscape');
});

View File

@ -1,150 +0,0 @@
// An embedded version of a script
const CncfLandscapeApp = {
init: function() {
// get initial state from the url
this.state = {
selected: new URLSearchParams(location.search).get('selected')
};
document.addEventListener('keydown', (e) => {
if (e.keyCode === 27) {
if (CncfLandscapeApp.state.selected) {
this.state.selected = null;
this.hideSelectedItem();
}
}
});
document.body.addEventListener('click', (e) => {
const modalBodyEl = e.target.closest('.modal-body');
const shadowEl = e.target.closest('.modal-container');
if (shadowEl && !modalBodyEl) {
this.state.selected = null;
this.hideSelectedItem();
e.preventDefault();
e.stopPropagation();
}
const modalClose = e.target.closest('.modal-close');
if (modalClose) {
this.state.selected = null;
this.hideSelectedItem();
e.preventDefault();
e.stopPropagation();
}
const nextItem = e.target.closest('.modal-next');
if (nextItem && CncfLandscapeApp.nextItemId) {
CncfLandscapeApp.state.selected = CncfLandscapeApp.nextItemId;
this.showSelectedItem(CncfLandscapeApp.state.selected);
e.preventDefault();
e.stopPropagation();
}
const prevItem = e.target.closest('.modal-prev');
if (prevItem && CncfLandscapeApp.prevItemId) {
CncfLandscapeApp.state.selected = CncfLandscapeApp.prevItemId;
this.showSelectedItem(CncfLandscapeApp.state.selected);
e.preventDefault();
e.stopPropagation();
}
const selectedItemInternalLinkEl = e.target.closest('.modal-body a[data-type=internal]')
if (selectedItemInternalLinkEl) {
e.preventDefault();
e.stopPropagation();
return
}
}, false);
// support custom css styles and custom js eval code through iframe
window.addEventListener('message', (event) => {
var data = event.data;
if (data.type === "css") {
var styles = data.css;
var el = document.createElement('style');
el.type = 'text/css';
if (el.styleSheet) {
el.styleSheet.cssText = styles;
} else {
el.appendChild(document.createTextNode(styles));
}
document.getElementsByTagName("head")[0].appendChild(el);
}
if (data.type === "js") {
eval(data.js);
}
});
// support css styles via param
const params = new URLSearchParams(window.location.search.substring(1));
if (params.get('css')) {
const element = document.createElement("link");
element.setAttribute("rel", "stylesheet");
element.setAttribute("type", "text/css");
element.setAttribute("href", params.get('css'));
document.getElementsByTagName("head")[0].appendChild(element);
}
if (params.get('style')) {
const element = document.createElement("style");
let style = params.get('style');
try {
style = JSON.parse(style)
} catch(ex) {
}
element.innerHTML = style;
document.getElementsByTagName("head")[0].appendChild(element);
}
this.showSelectedItem(this.state.selected);
},
showSelectedItem: async function(selectedItemId) {
const allIdElements = document.querySelectorAll(`[data-id]`);
let allIds = []
for (let element of allIdElements) {
allIds.push(element.getAttribute('data-id'));
}
this.selectedItems = this.selectedItems || {};
if (!this.selectedItems[selectedItemId]) {
const result = await fetch(`${this.basePath}/data/items/info-${selectedItemId}.html`);
const text = await result.text();
this.selectedItems[selectedItemId] = text;
}
document.querySelector('.modal').style.display="";
document.querySelector('.modal .modal-content').outerHTML = this.selectedItems[selectedItemId];
document.querySelector('body').style.overflow = 'hidden';
if (window.twttr) {
window.twttr.widgets.load();
} else {
setTimeout( () => window.twttr && window.twttr.widgets.load(), 1000);
}
//calculate previous and next items;
const index = allIds.indexOf(selectedItemId);
const prevItem = allIds[index - 1] || null;
const nextItem = allIds[index + 1] || null;
this.nextItemId = nextItem;
this.prevItemId = prevItem;
if (nextItem) {
document.querySelector('.modal-next').removeAttribute('disabled');
} else {
document.querySelector('.modal-next').setAttribute('disabled', '');
}
if (prevItem) {
document.querySelector('.modal-prev').removeAttribute('disabled');
} else {
document.querySelector('.modal-prev').setAttribute('disabled', '');
}
},
hideSelectedItem: function() {
window.parent.postMessage({type: 'landscapeapp-hide'}, '*');
}
}
document.addEventListener('DOMContentLoaded', () => CncfLandscapeApp.init());

View File

@ -14,18 +14,10 @@ const CncfLandscapeApp = {
if (CncfLandscapeApp.state.embed) {
document.querySelector('html').classList.add('embed');
if (CncfLandscapeApp.state.showModal) {
document.querySelector('html').classList.add('modal-embed')
} else {
setInterval(function() {
const realHeight = document.querySelector('.column-content').scrollHeight + 80;
window.parent.postMessage({
type: 'landscapeapp-resize',
height: realHeight
}, '*');
}, 1000);
document.querySelector('#embedded-footer a').href = this.stringifyBrowserUrl({...this.state, embed: false});
}
setInterval(function() {
document.body.style.height = document.querySelector('.column-content').scrollHeight;
}, 1000);
document.querySelector('#embedded-footer a').href = this.stringifyBrowserUrl({...this.state, embed: false});
}
if (CncfLandscapeApp.state.cardStyle === 'borderless') {
document.querySelector('html').classList.add('borderless-mode');
@ -50,16 +42,6 @@ const CncfLandscapeApp = {
} else if (document.querySelector('.select-popup').style.display === '') {
document.querySelector('.select-popup').style.display = "none";
}
} else {
if (document.querySelector('.select-popup').style.display === '') {
if (e.key >= 'a' && e.key <= 'z') {
const items = [...document.querySelectorAll('.select-popup .pure-material-checkbox span')];
const first = items.find( (i) => i.innerText.toLowerCase().startsWith(e.key.toLowerCase()));
if (first) {
first.scrollIntoView();
}
}
}
}
});
@ -177,7 +159,7 @@ const CncfLandscapeApp = {
const linkState = this.parseUrl({search: groupingInternalLinkEl.getAttribute('href'), pathname: '', hash: ''});
const f = (name, x) => this.calculateFullSelection(name, x);
const allowedProps = ['grouping', 'sort', 'bestpractices', 'enduser', 'parent', 'language', 'specification'];
const allowedProps = ['grouping', 'sort', 'bestpractices', 'enduser', 'parent', 'language'];
const otherProps = ['category', 'project', 'license', 'organization', 'headquarters', 'company-type', 'industries']
for (let key of allowedProps) {
newState[key] = linkState[key] || CncfLandscapeApp.initialState[key];
@ -205,7 +187,7 @@ const CncfLandscapeApp = {
// Only set certain properties: filterable + invisible filters
// for visible filter we need to always expand a current selection
const f = (name, x) => this.calculateFullSelection(name, x);
const allowedProps = ['grouping', 'sort', 'bestpractices', 'enduser', 'parent', 'language', 'specification'];
const allowedProps = ['grouping', 'sort', 'bestpractices', 'enduser', 'parent', 'language'];
const otherProps = ['category', 'project', 'license', 'organization', 'headquarters', 'company-type', 'industries']
for (let key of allowedProps) {
newState[key] = linkState[key] || CncfLandscapeApp.initialState[key];
@ -282,7 +264,7 @@ const CncfLandscapeApp = {
const newState = {...CncfLandscapeApp.state };
const f = (name, x) => this.calculateFullSelection(name, x);
const allowedProps = ['bestpractices', 'enduser', 'parent', 'language', 'specification'];
const allowedProps = ['bestpractices', 'enduser', 'parent', 'language'];
const otherProps = ['category', 'project', 'license', 'organization', 'headquarters', 'company-type', 'industries']
for (let key of allowedProps) {
newState[key] = CncfLandscapeApp.initialState[key];
@ -546,6 +528,7 @@ const CncfLandscapeApp = {
}
let hasTrue = false;
let hasChildren = false;
let children = [];
for (let i = parentIndex + 1; i < allItems.length; i++) {
const childEl = allItems[i];
if (childEl.getAttribute('data-level') === "1") {
@ -663,7 +646,6 @@ const CncfLandscapeApp = {
const parseMode = (x) => (x || '').indexOf('-mode') !== -1 ? 'card' : (x || CncfLandscapeApp.initialMode);
const parseCardStyle = (x) => (x || '').indexOf('-mode') !== -1 ? x.replace('-mode', '') : 'card';
const parseParamStyle = (x) => ['logo', 'borderless', 'flat'].indexOf(x) !== -1 ? x : '';
return {
zoom: +params.get('zoom') / 100 || 1,
@ -672,7 +654,7 @@ const CncfLandscapeApp = {
activeSection: hash,
mode: parseMode(params.get('format') || pathname) || CncfLandscapeApp.initialMode,
cardStyle: parseParamStyle(params.get('style')) || parseCardStyle(pathname),
cardStyle: params.get('style') || parseCardStyle(pathname),
grouping: params.get('grouping') || 'project',
sort: params.get('sort') || 'name',
@ -689,10 +671,9 @@ const CncfLandscapeApp = {
enduser: params.get('enduser') || '',
parent: params.get('parent') || '',
language: params.get('language') || '',
specification: params.get('specification') || '',
selected: params.get('selected') || null,
embed: params.has('embed'),
showModal: params.has('showmodal'),
};
},
propagateStateToUiAndUrl: function() {
@ -720,18 +701,6 @@ const CncfLandscapeApp = {
assignSingleSelect('sort');
assignSingleSelect('grouping');
assignMultiSelect('category');
const isCardMode = CncfLandscapeApp.state.mode === 'card';
if (isCardMode) {
document.querySelector('.select[data-name=sort]').classList.remove('select-disabled');
document.querySelector('.select[data-name=grouping]').classList.remove('select-disabled');
document.querySelector('.select[data-name=category]').classList.remove('select-disabled');
} else {
document.querySelector('.select[data-name=sort]').classList.add('select-disabled');
document.querySelector('.select[data-name=grouping]').classList.add('select-disabled');
document.querySelector('.select[data-name=category]').classList.add('select-disabled');
}
assignMultiSelect('project');
assignMultiSelect('license');
assignMultiSelect('organization');
@ -766,7 +735,7 @@ const CncfLandscapeApp = {
if (item.level === 1) {
let allChildren = [];
let selectedChildren = [];
for (let j = i + 1; j < wrapper.selectData.length; j++) {
for (j = i + 1; j < wrapper.selectData.length; j++) {
let childItem = wrapper.selectData[j];
if (childItem.level === 1) {
break;
@ -807,7 +776,7 @@ const CncfLandscapeApp = {
if (item.level === 1 && selectedIds.includes(item.id)) {
let children = [];
let totalChildren = 0;
for (let j = i + 1; j < wrapper.selectData.length; j++) {
for (j = i + 1; j < wrapper.selectData.length; j++) {
let childItem = wrapper.selectData[j];
if (childItem.level === 1) {
break;
@ -844,7 +813,7 @@ const CncfLandscapeApp = {
params[field] = CncfLandscapeApp.calculateShortSelection(field).url
}
// no fields for certain filters yet
for (let field of ['sort', 'grouping', 'bestpractices', 'enduser', 'parent', 'language', 'specification']) {
for (let field of ['sort', 'grouping', 'bestpractices', 'enduser', 'parent', 'language']) {
params[field] = state[field]
}
@ -877,13 +846,14 @@ const CncfLandscapeApp = {
}
}
// no fields for certain filters yet
for (let field of ['grouping', 'sort', 'selected', 'bestpractices', 'enduser', 'parent', 'language', 'specification', 'fullscreen', 'embed']) {
for (let field of ['grouping', 'sort', 'selected', 'bestpractices', 'enduser', 'parent', 'language',
'fullscreen', 'embed']) {
if (state[field] !== initialState[field]) {
params[field] = state[field]
}
}
if (state.zoom !== initialState.zoom) {
params.zoom = (state.zoom * 100).toFixed(0);
params.zoom = (state.zoom * 100).toFixed(0);;
}
@ -900,18 +870,6 @@ const CncfLandscapeApp = {
return url;
},
showSelectedItem: async function(selectedItemId) {
if (this.state.embed && !this.state.showModal) {
window.parent.postMessage({
type: 'landscapeapp-show',
selected: selectedItemId,
location: {
search: window.location.search,
pathname: window.location.pathname
}
}, '*');
return;
}
this.selectedItems = this.selectedItems || {};
if (!this.selectedItems[selectedItemId]) {
const result = await fetch(`${this.basePath}/data/items/info-${selectedItemId}.html`);
@ -932,7 +890,7 @@ const CncfLandscapeApp = {
let prevItem = null;
let nextItem = null;
if (this.state.mode === 'card') {
const items = (this.groupedItems || []).flatMap( (x) => x.items);
const items = this.groupedItems.flatMap( (x) => x.items);
for (let i = 0; i < items.length; i++) {
if (items[i].id === selectedItemId) {
prevItem = (items[i - 1] || {}).id;
@ -963,11 +921,35 @@ const CncfLandscapeApp = {
} else {
document.querySelector('.modal-prev').setAttribute('disabled', '');
}
if (window.parentIFrame) {
window.parentIFrame.sendMessage({type: 'showModal'});
window.parentIFrame.getPageInfo(function(info) {
var offset = info.scrollTop - info.offsetTop;
var maxHeight = info.clientHeight * 0.9;
if (maxHeight > 640) {
maxHeight = 640;
}
var defaultTop = (info.windowHeight - maxHeight) / 2;
var top = defaultTop + offset;
if (top < 0 && info.iframeHeight <= 600) {
top = 10;
}
setTimeout(function() {
const modal = document.querySelector('.modal-body');
if (modal) {
modal.style.top = top + 'px';
modal.style.marginTop = 0;
modal.style.marginBottom = 0;
modal.style.bottom = '';
modal.style.maxHeight = maxHeight + 'px';
}
}, 10);
});
}
},
updateUrl: function() {
if (CncfLandscapeApp.state.embed) {
return;
}
const newUrl = CncfLandscapeApp.stringifyBrowserUrl(CncfLandscapeApp.state);
if (newUrl !== this.previousUrl) {
history.pushState(CncfLandscapeApp.state, '', newUrl);
@ -976,12 +958,12 @@ const CncfLandscapeApp = {
}
},
hideSelectedItem: function() {
if (this.state.showModal) {
window.parent.postMessage({type: 'landscapeapp-hide'}, '*');
return;
}
document.querySelector('.modal').style.display="none";
document.querySelector('body').style.overflow = '';
if (window.parentIFrame && this.state.embed) {
window.parentIFrame.sendMessage({type: 'hideModal'})
}
this.updateUrl();
},
fetchApiData: async function() {
@ -1063,7 +1045,7 @@ const CncfLandscapeApp = {
// edge case: we just opened a tab without filters - then just display everything!
if (this.state.mode === this.initialMode) {
const allowedProps = ['bestpractices', 'enduser', 'parent', 'language', 'specification'];
const allowedProps = ['bestpractices', 'enduser', 'parent', 'language'];
const otherProps = ['project', 'license', 'organization', 'headquarters', 'company-type', 'industries']
let same = true;
for (let key of [...allowedProps, ...otherProps]) {
@ -1133,7 +1115,6 @@ const CncfLandscapeApp = {
result[card.getAttribute('data-id')] = card;
}
this.cards = result;
}
const apiData = await this.fetchApiData();
@ -1173,10 +1154,6 @@ const CncfLandscapeApp = {
}
document.querySelector('.column-content').replaceChildren(fragment);
this.lastCards = JSON.stringify(this.groupedItems);
if (this.state.selected) {
this.propagateStateToUi();
}
}
}
document.addEventListener('DOMContentLoaded', () => CncfLandscapeApp.init());

View File

@ -249,42 +249,6 @@ a:hover svg {
position: relative;
}
#guide-page .links {
position: fixed;
right: 10px;
top: 100px;
font-size: 16px;
width: 130px;
font-weight: bold;
}
@media (max-width: 1250px) {
#guide-page .links {
position: inherit;
width: 100%;
left: 0;
top: 0;
}
#guide-page .links > div {
display: inline-block;
padding-right: 100px;
}
}
#guide-page .links > div {
position: relative;
}
#guide-page .links svg {
fill: currentColor;
position: relative;
top: 2px;
width: 16px;
height: 16px;
}
#guide-page h1 {
font-size: 2.5em;
}
@ -682,11 +646,10 @@ a:hover svg {
font-size: 14px;
margin: 15px 0px;
max-width: 165px;
white-space: nowrap;
}
.guide-toggle .toggle-item {
user-select: none;
width: 49%;
width: 50%;
text-align: center;
padding: 2px;
color: rgb(46, 103, 191);
@ -1061,12 +1024,6 @@ html.embed body {
.column-content {
display: flex; flex-wrap: wrap; position: relative
}
.column-content [data-section-id] {
width: 100%;
display: flex;
flex-wrap: wrap;
position: relative;
}
.oss img { background: white; }
.nonoss img { background: #E4E4E4; }
@ -1329,7 +1286,6 @@ html.ios.has-selected-item, html.ios.has-selected-item body {
cursor: pointer;
min-width: 40px;
text-align: center;
margin-top: -6px;
}
.right-buttons .disabled {
cursor: default;
@ -1343,16 +1299,6 @@ html.ios.has-selected-item, html.ios.has-selected-item body {
display: none;
}
.embed.modal-embed .column-content {
display: none;
}
.embed.modal-embed {
background: inherit;
}
.embed.modal-embed #embedded-footer {
display: none;
}
.tweet-button {
width: 105px;
display: flex;
@ -1463,13 +1409,6 @@ html.ios.has-selected-item, html.ios.has-selected-item body {
height: 63px;
}
.inner-landscape .large-item.large-item-16, .items .large-item.large-item-16 {
cursor: pointer;
position: relative;
width: 145px;
height: 128px;
}
.inner-landscape .small-item, .items .small-item {
cursor: pointer;
position: relative;
@ -1488,16 +1427,11 @@ html.ios.has-selected-item, html.ios.has-selected-item body {
grid-row-end: span 1;
}
.inner-landscape .item-wrapper.wrapper-large-4, .items .item-wrapper.wrapper-large-4 {
.inner-landscape .item-wrapper.wrapper-large, .items .item-wrapper.wrapper-large {
grid-column-end: span 2;
grid-row-end: span 2;
}
.inner-landscape .item-wrapper.wrapper-large-16, .items .item-wrapper.wrapper-large-16 {
grid-column-end: span 4;
grid-row-end: span 4;
}
@media (max-width: var(--sm-screen)) {
@ -1695,7 +1629,6 @@ html.ios.has-selected-item, html.ios.has-selected-item body {
height: 180px;
top: 0;
left: 0;
user-select: none;
}
.modal-content .product-tags {
@ -1891,31 +1824,6 @@ html.ios.has-selected-item, html.ios.has-selected-item body {
height: 22px;
}
.twolines {
height: 22px !important;
width: 145px !important;
position: relative !important;
}
.twolines .tag-name {
position: absolute;
left: 0px;
top: -1px;
width: 145px;
padding: 0 !important;
margin: 0 !important;
text-align: center;
}
.twolines .tag-value {
position: absolute;
left: 0px;
width: 145px;
white-space: normal !important;
top: 5px;
text-align: center;
}
.modal-content .single-column .product-property .col:nth-child(1) {
display: inline-block;
width: 40%;
@ -1933,10 +1841,6 @@ html.ios.has-selected-item, html.ios.has-selected-item body {
width: 180px;
margin-bottom: 10px;
}
.select-disabled {
opacity: 0.35;
pointer-events: none;
}
.select-text {
position: relative;

View File

@ -1,7 +0,0 @@
<svg
viewBox="0 0 300 244">
<g transform="translate(-539.17946,-568.85777)" >
<path fillOpacity="1" fillRule="nonzero"
d="m 633.89823,812.04479 c 112.46038,0 173.95627,-93.16765 173.95627,-173.95625 0,-2.64628 -0.0539,-5.28062 -0.1726,-7.90305 11.93799,-8.63016 22.31446,-19.39999 30.49762,-31.65984 -10.95459,4.86937 -22.74358,8.14741 -35.11071,9.62551 12.62341,-7.56929 22.31446,-19.54304 26.88583,-33.81739 -11.81284,7.00307 -24.89517,12.09297 -38.82383,14.84055 -11.15723,-11.88436 -27.04079,-19.31655 -44.62892,-19.31655 -33.76374,0 -61.14426,27.38052 -61.14426,61.13233 0,4.79784 0.5364,9.46458 1.58538,13.94057 -50.81546,-2.55686 -95.87353,-26.88582 -126.02546,-63.87991 -5.25082,9.03545 -8.27852,19.53111 -8.27852,30.73006 0,21.21186 10.79366,39.93837 27.20766,50.89296 -10.03077,-0.30992 -19.45363,-3.06348 -27.69044,-7.64676 -0.009,0.25652 -0.009,0.50661 -0.009,0.78077 0,29.60957 21.07478,54.3319 49.0513,59.93435 -5.13757,1.40062 -10.54335,2.15158 -16.12196,2.15158 -3.93364,0 -7.76596,-0.38716 -11.49099,-1.1026 7.78383,24.2932 30.35457,41.97073 57.11525,42.46543 -20.92578,16.40207 -47.28712,26.17062 -75.93712,26.17062 -4.92898,0 -9.79834,-0.28036 -14.58427,-0.84634 27.05868,17.34379 59.18936,27.46396 93.72193,27.46396" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,3 +0,0 @@
<svg viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25a1.75 1.75 0 01.445-.758l8.61-8.61zm1.414 1.06a.25.25 0 00-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 000-.354l-1.086-1.086zM11.189 6.25L9.75 4.81l-6.286 6.287a.25.25 0 00-.064.108l-.558 1.953 1.953-.558a.249.249 0 00.108-.064l6.286-6.286z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 470 B

View File

@ -1 +0,0 @@
<svg viewBox="0 0 24 24"><path d="M10 17l5-5-5-5v10z"></path></svg>

Before

Width:  |  Height:  |  Size: 68 B

View File

@ -1 +0,0 @@
<svg viewBox="0 0 24 24"><path d="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14zm-1-6h-3V8h-2v5H8l4 4 4-4z"></path></svg>

Before

Width:  |  Height:  |  Size: 180 B

View File

@ -1 +0,0 @@
<svg viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"></path></svg>

Before

Width:  |  Height:  |  Size: 127 B

View File

@ -1 +0,0 @@
<svg viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"></path></svg>

Before

Width:  |  Height:  |  Size: 127 B

View File

@ -1,4 +0,0 @@
<svg class="guide-icon" viewBox="0 0 18.78 23.66">
<path d="M9.39,9.5a2.09,2.09,0,0,1,0-4.18,2.09,2.09,0,0,1,0,4.18Z"/>
<path d="M3,19.7H17.75a1,1,0,0,0,1-1V1a1,1,0,0,0-1-1H1A1,1,0,0,0,0,1V20.64a3,3,0,0,0,3,3H17.75a1,1,0,1,0,0-2.05H3A1,1,0,0,1,3,19.7ZM9.39,2.9a4.52,4.52,0,0,1,4.5,4.51c0,1.76-2.29,6-3.61,8.27a1,1,0,0,1-1.79,0C7.18,13.41,4.88,9.17,4.88,7.41A4.52,4.52,0,0,1,9.39,2.9Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 402 B

View File

@ -1,3 +0,0 @@
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 223 B

View File

@ -1 +0,0 @@
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"></path></svg>

Before

Width:  |  Height:  |  Size: 115 B

View File

@ -1 +0,0 @@
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path></svg>

Before

Width:  |  Height:  |  Size: 114 B

View File

@ -1,2 +0,0 @@
<svg viewBox="0 0 24 24"><path d="M14 12c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2zm-2-9c-4.97 0-9 4.03-9 9H0l4 4 4-4H5c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.51 0-2.91-.49-4.06-1.3l-1.42 1.44C8.04 20.3 9.94 21 12 21c4.97 0 9-4.03 9-9s-4.03-9-9-9z"/></svg>

Before

Width:  |  Height:  |  Size: 263 B

View File

@ -1 +0,0 @@
<svg viewBox="0 0 24 24"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"></path></svg>

Before

Width:  |  Height:  |  Size: 95 B

View File

@ -1 +0,0 @@
<svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"></path></svg>

Before

Width:  |  Height:  |  Size: 138 B

View File

@ -1 +0,0 @@
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"></path></svg>

Before

Width:  |  Height:  |  Size: 150 B

View File

@ -1 +0,0 @@
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11H7v-2h10v2z"></path></svg>

Before

Width:  |  Height:  |  Size: 132 B

View File

@ -9,13 +9,13 @@
// url(id by default): how the value is stored in the url
// sortOrder(element index by default): sort order when grouping
// match: function
const _ = require('lodash');
const { isParent } = require('../utils/isParent');
const { readJsonFromProject, readJsonFromDist } = require('../utils/readJson');
const lookups = readJsonFromProject('lookup');
const settings = readJsonFromDist('settings');
import path from 'path';
import fs from 'fs';
import _ from 'lodash';
import lookups from 'project/lookup'
import unpack from '../utils/unpackArray';
import settings from 'dist/settings'
import isParent from '../utils/isParent';
const relationField = (function() {
const additionalRelations = settings.relation.values.flatMap(({ children }) => children || [])
@ -86,7 +86,7 @@ const fields = {
id: 'license',
label: 'License',
isArray: true,
values: [].concat(lookups.license || []),
values: [].concat(unpack(lookups.license) || []),
processValuesBeforeSaving: function(values) {
return processValuesBeforeSaving({options: fields.license.values, values: values});
},
@ -102,13 +102,13 @@ const fields = {
id: 'organization',
label: 'Organization',
isArray: true,
values: [].concat(lookups.organization || [])
values: [].concat(unpack(lookups.organization) || [])
},
headquarters: {
id: 'headquarters',
label: 'Headquarters Location',
isArray: true,
values: [].concat(lookups.headquarters || []),
values: [].concat(unpack(lookups.headquarters) || []),
processValuesBeforeSaving: function(values) {
return processValuesBeforeSaving({options: fields.headquarters.values, values: values});
},
@ -121,7 +121,7 @@ const fields = {
url: 'company-type',
label: 'Company Type',
isArray: true,
values: [].concat(lookups.companyTypes || []),
values: [].concat(unpack(lookups.companyTypes) || []),
filterFn: function(filter, _, record) {
if (!filter || filter.length === 0) {
return true;
@ -135,7 +135,7 @@ const fields = {
id: 'industries',
label: 'Industries',
isArray: true,
values: [].concat(lookups.industries || []),
values: [].concat(unpack(lookups.industries) || []),
filterFn: function(filter, _, record) {
if (!filter || filter.length === 0) {
return true;
@ -150,7 +150,7 @@ const fields = {
url: 'category',
label: 'Category',
isArray: true,
values: [].concat(lookups.landscape || []),
values: [].concat(unpack(lookups.landscape) || []),
processValuesBeforeSaving: function(values) {
return processValuesBeforeSaving({options: fields.landscape.values, values: values});
},
@ -233,19 +233,6 @@ const fields = {
},
values: [{id: true, label: 'Yes', url: 'yes'}, {id: false, label: 'No', url: 'no'}]
},
specification: {
id: 'extra',
label: 'Specification',
url: 'specification',
filterFn: function(filter, value, item) {
if (filter === null) {
return true;
}
const isSpecification = item.extra && (item.extra.specification === true || item.extra.specification === 'yes' || item.extra.specification === 'true');
return filter === true ? isSpecification : !isSpecification;
},
values: [{id: true, label: 'Yes', url: 'yes'}, {id: false, label: 'No', url: 'no'}]
},
parents: {
id: 'parent',
url: 'parent',
@ -314,7 +301,7 @@ _.each(fields, function(field, key) {
field.valuesMap = _.keyBy(field.values, 'id')
});
module.exports.fields = fields;
export default fields;
const processValuesBeforeLoading = function({options, values}) {
return options.filter(function(option) {
@ -350,7 +337,7 @@ const processValuesBeforeSaving = function({options, values}) {
};
// passed to the client
function options(field) {
export function options(field) {
return fields[field].values.map(function(values) {
return {
id: values.url,
@ -359,9 +346,8 @@ function options(field) {
};
});
}
module.exports.options = options;
function filterFn({field, filters}) {
export function filterFn({field, filters}) {
const fieldInfo = fields[field];
const filter = filters[field];
return function(x) {
@ -383,15 +369,12 @@ function filterFn({field, filters}) {
}
};
}
module.exports.filterFn = filterFn;
function getGroupingValue({item, grouping, filters}) {
export function getGroupingValue({item, grouping, filters}) {
const { id, groupFn } = fields[grouping];
return groupFn ? groupFn({ item , filters }) : item[id];
}
module.exports.getGroupingValue = getGroupingValue;
const sortOptions = [{
export const sortOptions = [{
id: 'name',
direction: 'asc',
label: 'Alphabetical (a to z)',
@ -430,5 +413,4 @@ const sortOptions = [{
label: 'Date Joined',
disabled: true
}]
module.exports.sortOptions = sortOptions;

View File

@ -1,8 +1,10 @@
const { getBasePath } = require('../../tools/getBasePath');
import getBasePath from '../../tools/getBasePath'
module.exports.assetPath = path => {
const assetPath = path => {
if (path.startsWith('http://') || path.startsWith('https://')) {
return path;
}
return `${getBasePath()}${path[0] === '/' ? '' : '/'}${path}`
}
export default assetPath

View File

@ -1,4 +1,4 @@
module.exports.millify = function(value) {
export function millify(value) {
let base, suffix;
if (value < 1000 - 0.05) {
base = value;
@ -20,21 +20,3 @@ module.exports.millify = function(value) {
return digits + suffix;
}
module.exports.h = function(html) {
if (!html) {
return '';
}
var entityMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
};
return String(html).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
return entityMap[s];
});
}

View File

@ -1,6 +1,6 @@
const _ = require('lodash');
const formatNumber = require('format-number');
module.exports.formatAmount = function(v) {
import _ from 'lodash';
import formatNumber from 'format-number';
export default function formatAmount(v) {
if (_.isString(v)) {
return v;
}

View File

@ -1,4 +1,4 @@
module.exports.formatCity = function({city, region, country}) {
export default function formatCity({city, region, country}) {
if (!city) {
return null;
}

View File

@ -1,8 +1,8 @@
const _ = require('lodash');
const _formatNumber = require('format-number');
module.exports.formatNumber = function(v) {
import _ from 'lodash';
import formatNumber from 'format-number';
export default function _formatNumber(v) {
if (_.isString(v)) {
return '';
}
return _formatNumber({integerSeparator: ','})(v);
return formatNumber({integerSeparator: ','})(v);
}

View File

@ -0,0 +1,37 @@
import { getGroupedItemsForContentMode, flattenItems } from './itemsCalculator'
import { projects } from '../../tools/loadData'
const getPrerenderProps = params => {
const items = flattenItems(getGroupedItemsForContentMode(params, projects))
const entries = items.map(project => {
const keys = [
'name', 'stars', 'organization', 'path', 'landscape', 'category', 'oss', 'href', 'id',
'flatName', 'member', 'relation', 'project', 'isSubsidiaryProject', 'amount', 'amountKind',
'headquarters', 'license', 'bestPracticePercentage', 'enduser', 'joined', 'industries'
]
const entry = keys.reduce((hash, key) => {
const value = project[key]
return {
...hash,
...(value || value === false ? { [key]: project[key] } : {})
}
}, {})
const languages = ((project.github_data || {}).languages || []).map(({ name }) => ({ name }))
const crunchbaseData = project.crunchbaseData || {}
const parents = crunchbaseData.parents || []
const company_type = crunchbaseData.company_type || ''
return {
...entry,
github_data: { languages },
crunchbaseData: { parents, company_type }
}
})
return { entries }
}
export default getPrerenderProps

View File

@ -1,7 +1,8 @@
const { fields } = require('../types/fields');
const _ = require('lodash');
import fields from '../types/fields';
import _ from 'lodash';
module.exports.groupingLabel = function(field, id) {
export default function groupingLabel(field, id) {
const values = fields[field].answers;
const valueInfo = _.find(values, {id: id});
return valueInfo.groupingLabel;

View File

@ -1,6 +1,6 @@
const { fields } = require('../types/fields');
const _ = require('lodash');
module.exports.groupingOrder = function(field) {
import fields from '../types/fields';
import _ from 'lodash';
export default function groupFn(field) {
const values = fields[field].answers;
const sortedValues = _.orderBy(values, 'groupingSortOrder');
return function(x) {

View File

@ -1,7 +0,0 @@
const files = require('fs').readdirSync('src/svg');
for (let file of files) {
if (file !== '.' && file !== '..' && file.endsWith('.svg')) {
const iconName = file.split('.svg')[0];
module.exports[iconName] = require('fs').readFileSync(`src/svg/${file}`, 'utf-8');
}
}

View File

@ -1,7 +1,9 @@
module.exports.isParent = (urlOrSlug, project) => {
const isParent = (urlOrSlug, project) => {
if (urlOrSlug) {
const url = urlOrSlug.indexOf("crunchbase.com") > 0 ? urlOrSlug : `https://www.crunchbase.com/organization/${urlOrSlug}`;
return project.crunchbase === url || project.crunchbaseData.parents.includes(url);
}
}
export default isParent

View File

@ -1,10 +1,13 @@
const _ = require('lodash');
const { fields, filterFn, getGroupingValue } = require('../types/fields');
const { groupingLabel } = require('../utils/groupingLabel');
const { groupingOrder } = require('../utils/groupingOrder');
const { stringOrSpecial } = require('../utils/stringOrSpecial');
const { getLandscapeCategories } = require('./sharedItemsCalculator');
const { stringifyParams } = require('./routing');
import _ from 'lodash';
import fields, { filterFn, getGroupingValue } from '../types/fields';
import groupingLabel from '../utils/groupingLabel';
import groupingOrder from '../utils/groupingOrder';
import formatAmount from '../utils/formatAmount';
import formatNumber from 'format-number';
import stringOrSpecial from '../utils/stringOrSpecial';
import { getLandscapeCategories } from './sharedItemsCalculator';
import { findLandscapeSettings } from "./landscapeSettings";
import { stringifyParams } from './routing'
const landscape = fields.landscape.values;
@ -12,18 +15,7 @@ const groupAndSort = (items, sortCriteria) => {
return _.groupBy(_.orderBy(items, sortCriteria), 'landscape')
}
module.exports.expandSecondPathItems = function(data) {
const extraItems = data.filter( (x) => x.second_path).flatMap( (item) => [item.second_path].flat().map( (extraPath) => ({
...item,
category: extraPath.split('/')[0].trim(),
path: extraPath,
landscape: extraPath,
allPaths: [item.path.concat([item.second_path].flat())]
})));
return data.concat(extraItems);
}
const getFilteredItems = module.exports.getFilteredItems = function({data, filters}) {
export const getFilteredItems = function({data, filters}) {
var filterHostedProject = filterFn({field: 'relation', filters});
var filterByLicense = filterFn({field: 'license', filters});
var filterByOrganization = filterFn({field: 'organization', filters});
@ -35,14 +27,32 @@ const getFilteredItems = module.exports.getFilteredItems = function({data, filte
var filterByLanguage = filterFn({field: 'language', filters});
var filterByCompanyType = filterFn({field: 'companyType', filters});
var filterByIndustries = filterFn({field: 'industries', filters});
var filterBySpecification = filterFn({field: 'specification', filters});
return data.filter(function(x) {
return filterHostedProject(x) && filterByLicense(x) && filterByOrganization(x) && filterByHeadquarters(x) && filterByLandscape(x) && filterByBestPractices(x) && filterByEnduser(x) && filterByParent(x) && filterByLanguage(x) && filterByCompanyType(x) && filterByIndustries(x) && filterBySpecification(x);
return filterHostedProject(x) && filterByLicense(x) && filterByOrganization(x) && filterByHeadquarters(x) && filterByLandscape(x) && filterByBestPractices(x) && filterByEnduser(x) && filterByParent(x) && filterByLanguage(x) && filterByCompanyType(x) && filterByIndustries(x);
});
}
const addExtraFields = function(data) {
return _.map(data, function(data) {
const hasStars = data.stars !== 'N/A' && data.stars !== 'Not Entered Yet';
const hasMarketCap = data.amount !== 'N/A' && data.amount !== 'Not Entered Yet';
return { ...data,
starsPresent: hasStars ,
starsAsText: hasStars ? formatNumber({integerSeparator: ','})(data.stars) : '',
marketCapPresent: hasMarketCap,
marketCapAsText: formatAmount(data.amount)
};
});
}
const getExtraFields = function({data, filters}) {
const filtered = getFilteredItems({data, filters});
return addExtraFields(filtered);
}
const getSortedItems = function({data, filters, sortField, sortDirection}) {
data = getFilteredItems({data, filters});
data = getExtraFields({data, filters });
const fieldInfo = fields[sortField];
const nonPublic = (x) => (x.name || '').toString().indexOf('Non-Public Organization') === 0;
const emptyItemsNA = data.filter(function(x) {
@ -84,16 +94,14 @@ const getSortedItems = function({data, filters, sortField, sortDirection}) {
return sortedViaMainSort.concat(sortedViaName1).concat(sortedViaName2).concat(sortedViaName3).concat(sortedViaName4);
}
const getGroupedItems = module.exports.getGroupedItems = function({ data, filters, sortField, sortDirection, grouping, skipDuplicates }) {
const getGroupedItems = function({ data, filters, sortField, sortDirection, grouping }) {
const items = getSortedItems({ data, filters, sortField, sortDirection });
const uniq = skipDuplicates ? (x) => _.uniqBy(x, 'id') : (x) => x;
if (grouping === 'no') {
return [{
key: 'key',
header: 'No Grouping',
items: uniq(items)
items: items
}]
}
@ -108,7 +116,7 @@ const getGroupedItems = module.exports.getGroupedItems = function({ data, filter
return {
key: properKey,
header: groupingLabel(grouping, properKey),
items: uniq(value),
items: value,
href: stringifyParams({filters: newFilters, grouping, sortField})
}
}), (group) => groupingOrder(grouping)(group.key));
@ -130,7 +138,7 @@ const bigPictureSortOrder = [
}
];
module.exports.getLandscapeItems = function({landscapeSettings, items, guideIndex = {}}) {
export function getLandscapeItems({landscapeSettings, items, guideIndex = {}}) {
if (landscapeSettings.isMain) {
const categories = getLandscapeCategories({landscapeSettings, landscape });
const itemsMap = groupAndSort(items, bigPictureSortOrder);
@ -189,13 +197,15 @@ module.exports.getLandscapeItems = function({landscapeSettings, items, guideInde
}
}
module.exports.flattenItems = groupedItems => {
export const flattenItems = groupedItems => {
return groupedItems.flatMap(group => {
const { items, subcategories } = group
return group.hasOwnProperty('items') ? items : subcategories.flatMap(({ items }) => items)
})
}
module.exports.getItemsForExport = function(params) {
export function getItemsForExport(params) {
return _.flatten(getGroupedItems(params).map((x) => x.items));
}
export default getGroupedItems;

View File

@ -1,35 +1,29 @@
const { readJsonFromDist } = require('./readJson');
const { fields } = require("../types/fields");
import path from 'path';
import fs from 'fs';
import settings from 'dist/settings';
import fields from "../types/fields";
const settings = readJsonFromDist('settings');
/* eslint-disable no-unused-vars */
const itemMargin = module.exports.itemMargin = 3;
const smallItemWidth = module.exports.smallItemWidth = 34;
const smallItemHeight = module.exports.smallItemHeight = 30;
const subcategoryMargin = module.exports.subcategoryMargin = 6;
const subcategoryTitleHeight = module.exports.subcategoryTitleHeight = 20;
const dividerWidth = module.exports.dividerWidth = 2;
const categoryBorder = module.exports.categoryBorder = 1;
const categoryTitleHeight = module.exports.categoryTitleHeight = 30;
const outerPadding = module.exports.outerPadding = 20;
const headerHeight = module.exports.headerHeight = 40;
/* eslint-enable */
export const itemMargin = 3
export const smallItemWidth = 34
export const smallItemHeight = 30
export const largeItemWidth = 2 * smallItemWidth + itemMargin
export const largeItemHeight = 2 * smallItemHeight + itemMargin
export const subcategoryMargin = 6
export const subcategoryTitleHeight = 20
export const dividerWidth = 2
export const categoryBorder = 1
export const categoryTitleHeight = 30
export const outerPadding = 20
export const headerHeight = 40
// Check if item is large
const sizeFn = module.exports.sizeFn = ({ relation, category, member, categoryAttrs }) => {
export const isLargeFn = ({ relation, category, member, categoryAttrs }) => {
const relationInfo = fields.relation.valuesMap[relation]
if (!relationInfo) {
console.error(`No relation with name ${relation}`);
}
if (relationInfo.x4) {
return 16;
}
if (category === settings.global.membership) {
const membershipInfo = settings.membership[member];
return (membershipInfo && !!membershipInfo.is_large) ? 4 : 1;
return membershipInfo && !!membershipInfo.is_large;
}
return (!!categoryAttrs.isLarge || !!relationInfo.big_picture_order) ? 4 : 1;
return !!categoryAttrs.isLarge || !!relationInfo.big_picture_order;
}
// Compute if items are large and/or visible.
@ -38,15 +32,16 @@ const sizeFn = module.exports.sizeFn = ({ relation, category, member, categoryAt
const computeItems = (subcategories, addInfoIcon = false) => {
return subcategories.map(subcategory => {
const filteredItems = subcategory.items.reduce((acc, { id }) => ({ ...acc, [id]: true }), {})
const allItems = subcategory.allItems.map(item => ({ ...item, size: sizeFn(item), isVisible: filteredItems[item.id] }))
const itemsCount = allItems.reduce((count, item) => count + item.size, 0) + (addInfoIcon ? 1 : 0)
const largeItemsCount = allItems.reduce((count, item) => count + (item.size === 16 ? 4 : item.size === 4 ? 1 : 0), 0)
const allItems = subcategory.allItems.map(item => ({ ...item, isLarge: isLargeFn(item), isVisible: filteredItems[item.id] }))
const itemsCount = allItems.reduce((count, item) => count + (item.isLarge ? 4 : 1), 0) + (addInfoIcon ? 1 : 0)
const largeItemsCount = allItems.reduce((count, item) => count + (item.isLarge ? 1 : 0), 0)
return { ...subcategory, allItems, itemsCount, largeItemsCount }
})
}
// Calculate width and height of a given landscape
module.exports.calculateSize = landscapeSettings => {
export const calculateSize = landscapeSettings => {
const width = Math.max(...landscapeSettings.elements.map(({ left, width }) => left + width))
const height = Math.max(...landscapeSettings.elements.map(({ top, height }) => top + height))
const fullscreenWidth = width + 2 * outerPadding
@ -56,7 +51,7 @@ module.exports.calculateSize = landscapeSettings => {
}
// Calculate each subcategory width and the disposition of its items, assuming fixed padding for each item.
const calculateHorizontalFixedWidth = ({ subcategories, maxColumns, maxRows }) => {
const calculateHorizontalFixedWidth = ({ subcategories, maxColumns, maxRows, fitWidth }) => {
let availableColumns = maxColumns
subcategories.slice(0)
@ -134,10 +129,10 @@ const calculateHorizontalStretch = ({ subcategories, maxWidth, maxHeight }) => {
subcategories.forEach(subcategory => subcategory.width = Math.floor(maxWidth * subcategory.columns / totalColumns))
return subcategories;
return subcategories
}
module.exports.calculateHorizontalCategory = ({ height, width, subcategories, fitWidth, addInfoIcon = false }) => {
export const calculateHorizontalCategory = ({ height, width, subcategories, fitWidth, addInfoIcon = false }) => {
const subcategoriesWithCalculations = computeItems(subcategories, addInfoIcon)
const maxWidth = width - categoryTitleHeight - categoryBorder - (2 * subcategoryMargin - itemMargin + dividerWidth) * subcategories.length + dividerWidth
const maxHeight = height - 2 * (subcategoryMargin + categoryBorder) + itemMargin - 2 * categoryBorder
@ -151,7 +146,7 @@ module.exports.calculateHorizontalCategory = ({ height, width, subcategories, fi
}
}
module.exports.calculateVerticalCategory = ({ subcategories, fitWidth, width }) => {
export const calculateVerticalCategory = ({ subcategories, fitWidth, width }) => {
const subcategoriesWithCalculations = computeItems(subcategories)
const maxColumns = Math.floor((width - 2 * (categoryBorder + itemMargin)) / (smallItemWidth + itemMargin))

View File

@ -1,25 +1,26 @@
const { readJsonFromDist } = require('./readJson');
const settings = readJsonFromDist('settings');
import path from 'path';
import fs from 'fs';
import settings from 'dist/settings';
function calcLandscapeSettingsList(settingsObj) {
return Object.values(settingsObj.big_picture)
.sort((a, b) => a.tab_index - b.tab_index)
.map(({ url, ...rest }) => {
const basePath = url === 'landscape' ? null : url
const isMain = !rest.category;
const isMain = settingsObj.big_picture.main.url === url
return { url, basePath, isMain, ...rest }
})
}
};
// client side version
const landscapeSettingsList = module.exports.landscapeSettingsList = calcLandscapeSettingsList(settings);
export const landscapeSettingsList = calcLandscapeSettingsList(settings);
const landscapeSettingsDict = landscapeSettingsList.reduce((dict, landscapeSettings) => {
dict[landscapeSettings.url] = landscapeSettings;
return dict;
}, {});
}, {})
module.exports.findLandscapeSettings = (url) => {
export const findLandscapeSettings = (url) => {
if (url === 'main') {
url = 'landscape';
}
@ -27,6 +28,6 @@ module.exports.findLandscapeSettings = (url) => {
}
// server side version, with up to date information
module.exports.getLandscapeSettingsList = function(settingsObj) {
export function getLandscapeSettingsList(settingsObj) {
return calcLandscapeSettingsList(settingsObj);
}

7
src/utils/packArray.js Normal file
View File

@ -0,0 +1,7 @@
import _ from 'lodash';
export default function pack(records) {
const keys = _.uniq(_.flatten(_.map(records, (x) => _.keys(x))));
const compact = _.map( records, (x) => _.map(keys, (key) => x[key] || '!E'));
return [keys].concat(compact);
}

View File

@ -1,21 +0,0 @@
// allows to read json files from certain directores
module.exports.readJsonFromProject = function(file) {
if (global.lookups) {
if (global.lookups[file]) {
return global.lookups[file];
} else {
throw ('Can not read ', file);
}
}
const fullPath = require('path').resolve(process.env.PROJECT_PATH, file + '.json');
return JSON.parse(require('fs').readFileSync(fullPath, 'utf-8'));
}
module.exports.readJsonFromDist = function(file) {
if (global.lookups && global.lookups[file]) {
return global.lookups[file];
}
const fullPath = require('path').resolve(process.env.PROJECT_PATH, 'dist', process.env.PROJECT_NAME || '', file + '.json');
return JSON.parse(require('fs').readFileSync(fullPath, 'utf-8'));
}

View File

@ -1,10 +1,9 @@
const qs = require('query-string');
import qs, { stringifyUrl } from 'query-string'
import fields, { sortOptions } from '../types/fields'
import { isArray } from 'lodash'
const { fields, sortOptions } = require('../types/fields');
const { isArray } = require('lodash');
const defaultSort = 'name';
const defaultGrouping = 'relation';
const defaultSort = 'name'
const defaultGrouping = 'relation'
const compact = obj => {
return Object.entries(obj).reduce((result, [key, value]) => {
@ -120,7 +119,7 @@ const stringifyParams = (params = {}) => {
...filters
})
return qs.stringifyUrl({ url: `/${path}`, query },
return stringifyUrl({ url: `/${path}`, query },
{ arrayFormat: 'comma', skipNull: true, skipEmptyString: true })
}
@ -148,5 +147,4 @@ const parseParams = (query) => {
}
}
module.exports.stringifyParams = stringifyParams;
module.exports.parseParams = parseParams;
export { stringifyParams, parseParams }

View File

@ -1,12 +1,5 @@
const _ = require('lodash');
const { paramCase } = require('change-case');
let hash = {};
module.exports.saneName = function(x) {
let result = _.deburr(paramCase(x));
if (hash[result] && hash[result] !== x) {
result = result + '-2';
console.info(`Hash for ${x} is ${result}`);
}
hash[result] = x;
return result;
import _ from 'lodash';
import { paramCase } from 'change-case';
export default function(x) {
return _.deburr(paramCase(x));
};

View File

@ -0,0 +1,23 @@
import _ from 'lodash';
export default function selectedItemCalculator(groupedItems, selectedItemId, isBigPicture) {
const calcItems = function() {
if (!isBigPicture) {
return groupedItems.flatMap(group => group.items)
}
// if we are in a big picture mode, we want to allow prev/next button to work only inside a given category
const itemsByCategory = groupedItems.map(category => category.subcategories.flatMap(subcategory => subcategory.items))
return itemsByCategory.find(items => items.find(item => item.id === selectedItemId)) || []
}
const items = calcItems();
const index = _.findIndex(items, {id: selectedItemId});
const item = items[index];
const nextItem = items[index + 1];
const previousItem = items[index - 1];
return {
itemInfo: item,
hasSelectedItem: !!item,
nextItemId: (nextItem || {id: null}).id,
previousItemId: (previousItem || {id: null}).id
};
}

View File

@ -1,5 +1,11 @@
const _ = require('lodash');
module.exports.getLandscapeCategories = ({ landscape, landscapeSettings }) => {
// this file contains a code which can be used on both server and client side.
// client side uses this to get a list of items for a big picture
// server side uses this to build a sitemap, so we know which landscape a given
// element belongs to
import _ from 'lodash';
export const getLandscapeCategories = ({ landscape, landscapeSettings }) => {
if (landscapeSettings.isMain) {
return landscape.filter( ({ level }) => level === 1).filter((category) => {
return _.find(landscapeSettings.elements, (element) => element.category === category.id);

View File

@ -1,4 +1,4 @@
module.exports.shortRepoName = function(url) {
export default function shortRepoName(url) {
if (!url) {
return '';
}

View File

@ -1,4 +1,4 @@
module.exports.stringOrSpecial = function(x) {
export default function stringOrSpecial(x) {
if (x === 'true') {
return true;
}

View File

@ -1,12 +1,12 @@
const _ = require('lodash');
import _ from 'lodash';
import { getItemsForExport, getLandscapeItems } from './itemsCalculator';
import { millify } from '../utils/format';
import formatNumber from '../utils/formatNumber';
import { findLandscapeSettings } from "./landscapeSettings";
const { getItemsForExport, getLandscapeItems } = require('./itemsCalculator');
const { millify } = require('../utils/format');
const { formatNumber } = require('../utils/formatNumber');
const { findLandscapeSettings } = require("./landscapeSettings");
const { readJsonFromDist } = require('./readJson');
const settings = readJsonFromDist('settings');
import path from 'path';
import fs from 'fs';
import settings from 'dist/settings';
const getOrganizations = function(params) {
const filteredItems = getItemsForExport(params);
@ -36,7 +36,6 @@ const getSummary = function(params) {
} else {
filteredItemsByTab = filteredItems;
}
filteredItemsByTab = _.uniq(filteredItemsByTab, 'id');
const organizations = getOrganizations(params);
@ -46,9 +45,7 @@ const getSummary = function(params) {
const marketCap = settings.global.hide_funding_and_market_cap ? 0 :_.sumBy(organizations, 'marketCap');
return { total, stars, funding, marketCap };
}
module.exports.getSummary = getSummary;
const getSummaryText = function(summary) {
export const getSummaryText = function(summary) {
if (!summary.total) {
return 'There are no cards matching your filters';
}
@ -68,4 +65,4 @@ const getSummaryText = function(summary) {
return `${startText} ${text}.`;
};
module.exports.getSummaryText = getSummaryText;
export default getSummary;

8
src/utils/unpackArray.js Normal file
View File

@ -0,0 +1,8 @@
import _ from 'lodash';
export default function unpack(records) {
const keys = records[0];
const compact = records.slice(1);
const result = _.map(compact, (arr) => _.fromPairs(_.map(arr, (el, index) => [keys[index], el]).filter( (x) => x[1] !== '!E')));
return result;
}

View File

@ -1,4 +1,4 @@
module.exports.actualTwitter = function actualTwitter(node, crunchbaseEntry) {
export default function actualTwitter(node, crunchbaseEntry) {
const twitterUrl = 'twitter' in node ? node.twitter : (crunchbaseEntry || {}).twitter;
if (twitterUrl) {

View File

@ -1,27 +1,27 @@
const path = require('path');
const traverse = require('traverse');
const _ = require('lodash');
const { checkVersion } = require('./checkVersion');
const { hasFatalErrors, reportFatalErrors } = require('./fatalErrors');
const { errorsReporter } = require('./reporter');
const { projectPath, settings } = require('./settings');
const { actualTwitter } = require('./actualTwitter');
const { extractSavedImageEntries, fetchImageEntries, removeNonReferencedImages } = require('./fetchImages');
const { extractSavedCrunchbaseEntries, fetchCrunchbaseEntries } = require('./crunchbase');
const { fetchGithubEntries } = require('./fetchGithubStats');
const { getProcessedRepos, getProcessedReposStartDates } = require('./repos');
const { fetchStartDateEntries } = require('./fetchGithubStartDate');
const { fetchCloEntries } = require('./fetchCloData');
const { extractSavedTwitterEntries, fetchTwitterEntries } = require('./twitter');
const {
import checkVersion from './checkVersion';
import { hasFatalErrors, reportFatalErrors } from './fatalErrors';
import errorsReporter from './reporter';
import process from 'process';
import path from 'path';
import { projectPath, settings } from './settings';
import actualTwitter from './actualTwitter';
import { extractSavedImageEntries, fetchImageEntries, removeNonReferencedImages } from './fetchImages';
import { extractSavedCrunchbaseEntries, fetchCrunchbaseEntries } from './crunchbase';
import { fetchGithubEntries } from './fetchGithubStats';
import { getProcessedRepos, getProcessedReposStartDates } from './repos';
import { fetchStartDateEntries } from './fetchGithubStartDate';
import { extractSavedTwitterEntries, fetchTwitterEntries } from './twitter';
import {
extractSavedBestPracticeEntries,
fetchBestPracticeEntriesWithFullScan,
fetchBestPracticeEntriesWithIndividualUrls
} = require('./fetchBestPractices');
const { shortRepoName } = require('../src/utils/shortRepoName');
const { updateProcessedLandscape } = require("./processedLandscape");
} from './fetchBestPractices';
import shortRepoName from '../src/utils/shortRepoName';
import { updateProcessedLandscape } from "./processedLandscape";
const { landscape } = require('./landscape')
const traverse = require('traverse');
const _ = require('lodash');
const { addFatal } = errorsReporter('crunchbase');
var useCrunchbaseCache = true;
@ -30,8 +30,7 @@ var useGithubCache = true;
var useGithubStartDatesCache = true;
var useTwitterCache = true;
var useBestPracticesCache = true;
var key = process.env.LEVEL || 'easy';
var key = require('process').env.LEVEL || 'easy';
function reportOptions() {
console.info(`Running with a level=${key}. Settings:
Use cached crunchbase data: ${useCrunchbaseCache}
@ -44,15 +43,11 @@ function reportOptions() {
}
if (key.toLowerCase() === 'easy') {
reportOptions();
} else if (key.toLowerCase() === 'crunchbase') {
useCrunchbaseCache = false;
reportOptions();
}
else if (key.toLowerCase() === 'medium') {
useTwitterCache=false;
useGithubCache=false;
useCrunchbaseCache=true;
useCrunchbaseCache=false;
useBestPracticesCache=false;
reportOptions();
}
@ -169,12 +164,11 @@ async function main() {
console.info('Fetching last tweet dates');
const savedTwitterEntries = await extractSavedTwitterEntries();
// const twitterEntries = await fetchTwitterEntries({
// cache: savedTwitterEntries,
// preferCache: useTwitterCache,
// crunchbaseEntries: crunchbaseEntries
// });
const twitterEntries = await fetchTwitterEntries({
cache: savedTwitterEntries,
preferCache: useTwitterCache,
crunchbaseEntries: crunchbaseEntries
});
if (hasFatalErrors()) {
console.info('Reporting fatal errors');
@ -190,10 +184,6 @@ async function main() {
cache: savedBestPracticeEntries,
preferCache: useBestPracticesCache
});
require('fs').writeFileSync('/tmp/bp.json', JSON.stringify(bestPracticeEntries, null, 4));
console.info('Fetching CLOMonitor data');
const cloEntries = await fetchCloEntries();
const tree = traverse(landscape);
console.info('Processing the tree');
@ -319,9 +309,10 @@ async function main() {
}
node.best_practice_data = bestPracticeEntry;
delete node.best_practice_data.repo_url;
//twitter
// twitter
const twitter = actualTwitter(node, node.crunchbase_data);
const twitterEntry = _.clone(_.find(savedTwitterEntries, {
const twitterEntry = _.clone(_.find(twitterEntries, {
url: twitter
}));
if (twitterEntry) {
@ -329,11 +320,6 @@ async function main() {
delete twitterEntry.url;
}
const cloEntry = _.clone(_.find(cloEntries, { clomonitor_name: node.extra?.clomonitor_name }));
// svg clomonitor
if (cloEntry) {
node.extra.clomonitor_svg = cloEntry.svg
}
}
});

View File

@ -1,6 +1,6 @@
const path = require('path');
const { writeFileSync } = require('fs');
const { distPath, settings } = require('./settings');
import path from 'path';
import { writeFileSync } from 'fs'
import { distPath, settings } from './settings';
const isMainBranch = process.env.PULL_REQUEST !== 'true'

View File

@ -1,9 +1,9 @@
const { env } = require('process');
const { stringify, parse } = require('query-string');
const axios = require('axios');
const OAuth1 = require('oauth-1.0a');
const crypto = require('crypto');
const _ = require('lodash');
import { env } from 'process';
import { stringify, parse } from 'query-string';
import axios from 'axios'
import OAuth1 from 'oauth-1.0a'
import crypto from 'crypto'
import _ from 'lodash'
['GITHUB_KEY', 'TWITTER_KEYS'].forEach((key) => {
if (!env[key]) {
@ -62,12 +62,9 @@ const requestWithRetry = async ({ attempts = maxAttempts, resolveWithFullRespons
const response = await axios(rest);
return resolveWithFullResponse ? response : response.data
} catch (ex) {
const { response = {}, ...error } = ex
const { status } = response
const isGithubIssue = (response?.data?.message || '').indexOf('is too large to list') !== -1;
const message = [
`Attempt #${maxAttempts - attempts + 1}`,
`(Status Code: ${status || error.code})`,
@ -75,12 +72,10 @@ const requestWithRetry = async ({ attempts = maxAttempts, resolveWithFullRespons
].join(' ')
if (key === keys[keys.length - 1]) {
console.info(message);
} else {
console.info(`Failed to use key #${keys.indexOf(key)} of ${keys.length}`);
}
const rateLimited = retryStatuses.includes(status)
const dnsError = error.code === 'ENOTFOUND' && error.syscall === 'getaddrinfo'
if (attempts <= 0 || (!rateLimited && !dnsError) || isGithubIssue) {
if (attempts <= 0 || (!rateLimited && !dnsError)) {
throw ex;
}
lastEx = ex;
@ -122,15 +117,15 @@ const ApiClient = ({ baseURL, applyKey, keys, defaultOptions = {}, defaultParams
}
};
module.exports.CrunchbaseClient = ApiClient({
export const CrunchbaseClient = ApiClient({
baseURL: 'https://api.crunchbase.com/api/v4',
defaultParams: { user_key: env.CRUNCHBASE_KEY_4 },
defaultOptions: { followRedirect: true, maxRedirects: 5, timeout: 10 * 1000 }
});
module.exports.GithubClient = ApiClient({
export const GithubClient = ApiClient({
baseURL: 'https://api.github.com',
retryStatuses: [401, 403], // Github returns 403 when rate limiting.
retryStatuses: [403], // Github returns 403 when rate limiting.
delayFn: error => {
const rateLimitRemaining = parseInt(_.get(error, ['response', 'headers', 'x-ratelimit-remaining'], 1))
const rateLimitReset = parseInt(_.get(error, ['response', 'headers', 'x-ratelimit-reset'], 1)) * 1000
@ -155,7 +150,7 @@ module.exports.GithubClient = ApiClient({
const [consumerKey, consumerSecret, accessTokenKey, accessTokenSecret] = (env.TWITTER_KEYS || '').split(',');
module.exports.TwitterClient = ApiClient({
export const TwitterClient = ApiClient({
baseURL: 'https://api.twitter.com/1.1',
defaultOptions: {
oauth: {
@ -167,6 +162,6 @@ module.exports.TwitterClient = ApiClient({
}
});
module.exports.YahooFinanceClient = ApiClient({
export const YahooFinanceClient = ApiClient({
baseURL: 'https://query2.finance.yahoo.com',
});

View File

@ -0,0 +1,14 @@
//---------------------------------------------------------------------
// This is a fix for jest handling static assets like imported images
// when running tests. It's configured in jest section of package.json
//
// See:
// https://github.com/facebook/jest/issues/2663#issuecomment-317109798
//---------------------------------------------------------------------
const path = require('path');
module.exports = {
process(src, filename /*, config, options */) {
return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';';
},
};

View File

@ -1,17 +1,16 @@
const autoCropSvg = require('svg-autocrop');
const path = require('path');
const fs = require('fs');
const { projectPath } = require('./settings');
import { projectPath } from './settings';
import autoCropSvg from 'svg-autocrop';
import path from 'path';
import fs from 'fs';
async function main() {
const files = fs.readdirSync(path.resolve(projectPath, 'images'));
const files = require('fs').readdirSync(path.resolve(projectPath, 'images'));
const svgFiles = files.filter((x) => x.match(/\.svg$/));
for (const file of svgFiles) {
const fullPath = path.resolve(projectPath,'images', file);
const content = fs.readFileSync(fullPath, 'utf-8');
const content = require('fs').readFileSync(fullPath, 'utf-8');
const processed = await autoCropSvg(content);
fs.writeFileSync(fullPath, processed);
require('fs').writeFileSync(fullPath, processed);
console.info('Optimized an svg image: ', fullPath);
}
}

View File

@ -1,15 +1,16 @@
const _ = require('lodash');
const { TwitterClient } = require('./apiClients');
const { settings } = require('./settings');
const { updateProcessedLandscape } = require("./processedLandscape");
import _ from 'lodash';
import { TwitterClient } from './apiClients';
import { settings } from './settings';
import { updateProcessedLandscape } from "./processedLandscape";
// we need to know a latest since_id, otherwise we can only expect
async function getLatestTweets(sinceId) {
let count = 0;
async function getTweets(maxId) {
const params = {q: settings.twitter.search, count: 100, max_id: maxId, since_id: sinceId};
const result = await TwitterClient.request({ path: 'search/tweets.json', params });
const withoutLastId = result.statuses.filter( (x) => x.id_str !== maxId);
count += withoutLastId.length;
if (withoutLastId.length === 0) {
return [];
} else {

View File

@ -1,12 +1,11 @@
const Promise = require('bluebird');
const puppeteer = require('puppeteer');
require('./suppressAnnoyingWarnings');
const { landscapeSettingsList } = require('../src/utils/landscapeSettings');
const { setFatalError, reportFatalErrors } = require('./fatalErrors');
const { appUrl } = require('./distSettings');
import './suppressAnnoyingWarnings';
import Promise from 'bluebird';
import { landscapeSettingsList } from '../src/utils/landscapeSettings'
import { setFatalError, reportFatalErrors } from './fatalErrors';
import { appUrl } from './distSettings'
async function main() {
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});
const page = await browser.newPage();
var hasErrors = false;

View File

@ -1,11 +1,11 @@
const { exec } = require('child_process');
const colors = require('colors');
const Promise = require('bluebird');
const traverse = require('traverse');
const { landscape, saveLandscape } = require('./landscape');
const { updateProcessedLandscape } = require('./processedLandscape');
const { errorsReporter } = require('./reporter');
import { exec } from 'child_process'
import { writeFileSync, unlinkSync } from 'fs'
import colors from 'colors'
import Promise from 'bluebird'
import traverse from 'traverse'
import { landscape, saveLandscape } from './landscape'
import { updateProcessedLandscape } from './processedLandscape'
import errorsReporter from './reporter';
const { addError } = errorsReporter('link');
@ -35,10 +35,11 @@ async function checkViaPuppeteer(url, remainingAttempts = 3) {
const page = await browser.newPage();
page.setDefaultNavigationTimeout(120 * 1000);
let result = null;
try {
await page.goto(url);
await Promise.delay(5 * 1000);
const newUrl = await page.evaluate ( () => window.location.href );
const newUrl = await page.evaluate ( (x) => window.location.href );
await browser.close();
const withoutTrailingSlash = (x) => x.replace(/#(.*)/, '').replace(/\/$/, '');
if (withoutTrailingSlash(newUrl) === withoutTrailingSlash(url)) {
@ -56,7 +57,7 @@ async function checkViaPuppeteer(url, remainingAttempts = 3) {
}
}
const checkUrl = module.exports.checkUrl = (url, attempt = 1) => {
export const checkUrl = (url, attempt = 1) => {
return new Promise(resolve => {
const curlOptions = [
'--fail',

View File

@ -1,6 +1,6 @@
const axios = require('axios');
import axios from 'axios'
module.exports.checkVersion = async function() {
export default async function check() {
try {
const { data:result } = await axios({
url: `https://api.github.com/repos/cncf/landscapeapp/branches/master`,

View File

@ -1,9 +1,10 @@
const _ = require('lodash');
const path = require('path');
import _ from 'lodash';
import path from 'path';
import Promise from 'bluebird';
import { projectPath, settings } from './settings';
import { dump } from './yaml';
const { projectPath } = require('./settings');
const { dump } = require('./yaml');
const { hasFatalErrors, setFatalError, reportFatalErrors } = require('./fatalErrors');
import { hasFatalErrors, setFatalError, reportFatalErrors } from './fatalErrors';
function hasNonAscii(str) {
return ! /^[\x00-\x7F]*$/.test(str);

View File

@ -1,24 +1,22 @@
const colors = require('colors');
const Promise = require('bluebird');
const _ = require('lodash');
const path = require('path');
const debug = require('debug')('cb');
const { ensureHttps } = require('./ensureHttps');
const { errorsReporter } = require('./reporter');
const { projectPath } = require('./settings');
const { makeReporter } = require('./progressReporter');
const { CrunchbaseClient, YahooFinanceClient } = require('./apiClients');
import colors from 'colors';
import Promise from 'bluebird'
import _ from 'lodash';
import ensureHttps from './ensureHttps';
import errorsReporter from './reporter';
import { projectPath } from './settings';
import path from 'path';
import makeReporter from './progressReporter';
const error = colors.red;
const fatal = (x) => colors.red(colors.inverse(x));
const cacheMiss = colors.green;
const debug = require('debug')('cb');
import { CrunchbaseClient, YahooFinanceClient } from './apiClients';
const { addError, addFatal } = errorsReporter('crunchbase');
const EXCHANGE_SUFFIXES = {
'ams': 'AS', // Amsterdam
'bit': 'MI', // Milan
'bme': 'MC', // Madrid
'epa': 'PA', // Paris
'etr': 'DE', // XETRA
'fra': 'F', // Frankfurt
@ -43,7 +41,7 @@ const getSymbolWithSuffix = (symbol, exchange) => {
return [symbol, exchangeSuffix].filter(_ => _).join('.')
}
const getCrunchbaseOrganizationsList = module.exports.getCrunchbaseOrganizationsList = async function() {
export async function getCrunchbaseOrganizationsList() {
const traverse = require('traverse');
const source = require('js-yaml').load(require('fs').readFileSync(path.resolve(projectPath, 'landscape.yml')));
var organizations = [];
@ -67,7 +65,7 @@ const getCrunchbaseOrganizationsList = module.exports.getCrunchbaseOrganizations
return _.orderBy(_.uniq(organizations), 'name');
}
module.exports.extractSavedCrunchbaseEntries = async function() {
export async function extractSavedCrunchbaseEntries() {
const traverse = require('traverse');
let source = [];
try {
@ -136,24 +134,19 @@ const fetchCrunchbaseOrganization = async id => {
});
}
const fetchData = module.exports.fetchData = async function(name) {
export async function fetchData(name) {
const result = await fetchCrunchbaseOrganization(name)
const mapAcquisitions = function(a) {
let result1;
try {
result1 = {
date: a.announced_on.value,
acquiree: a.acquiree_identifier.value,
}
if (a.price) {
result.price = a.price.value_usd
}
} catch(ex) {
return null;
const result = {
date: a.announced_on.value,
acquiree: a.acquiree_identifier.value,
}
return result1;
if (a.price) {
result.price = a.price.value_usd
}
return result;
}
let acquisitions = result.cards.acquiree_acquisitions.map(mapAcquisitions).filter( (x) => !!x);
let acquisitions = result.cards.acquiree_acquisitions.map(mapAcquisitions);
const limit = 100;
let lastPage = result;
while (lastPage.cards.acquiree_acquisitions.length === limit) {
@ -171,8 +164,7 @@ const fetchData = module.exports.fetchData = async function(name) {
const parentOrganization = lastOrganization.cards.parent_organization[0].identifier.permalink
if (parents.map(p => p.identifier.permalink).includes(parentOrganization)) {
const { permalink } = lastOrganization.properties.identifier
console.info(`Circular dependency detected: ${permalink} and ${parentOrganization} are parents of each other`)
break;
throw new Error(`Circular dependency detected: ${permalink} and ${parentOrganization} are parents of each other`)
}
lastOrganization = await fetchCrunchbaseOrganization(parentOrganization)
parents.push({ ...lastOrganization.properties, delisted: isDelisted(lastOrganization) })
@ -185,9 +177,6 @@ const fetchData = module.exports.fetchData = async function(name) {
const totalFunding = firstWithTotalFunding ? + firstWithTotalFunding.funding_total.value_usd.toFixed() : undefined;
const getAddressPart = function(part) {
if (!result.cards.headquarters_address[0]) {
return " N/A";
}
return (result.cards.headquarters_address[0].location_identifiers.filter( (x) => x.location_type === part)[0] || {}).value
}
@ -202,6 +191,10 @@ const fetchData = module.exports.fetchData = async function(name) {
}
})();
if (!result.cards.headquarters_address[0]) {
return 'no address';
}
return {
name: result.properties.name,
description: result.properties.short_description,
@ -223,7 +216,7 @@ const fetchData = module.exports.fetchData = async function(name) {
}
}
module.exports.fetchCrunchbaseEntries = async function({cache, preferCache}) {
export async function fetchCrunchbaseEntries({cache, preferCache}) {
// console.info(organizations);
// console.info(_.find(organizations, {name: 'foreman'}));
const reporter = makeReporter();
@ -243,6 +236,11 @@ module.exports.fetchCrunchbaseEntries = async function({cache, preferCache}) {
}
try {
const result = await fetchData(c.name);
if (result === 'no address') {
fatalErrors.push(`no headquarter addresses for ${c.name} at ${c.crunchbase}`);
reporter.write(fatal("F"));
return null;
}
const entry = {
url: c.crunchbase,
@ -257,12 +255,8 @@ module.exports.fetchCrunchbaseEntries = async function({cache, preferCache}) {
if (!(c.ticker === null) && (entry.ticker || c.ticker)) {
// console.info('need to get a ticker?');
entry.effective_ticker = c.ticker || entry.ticker;
try {
entry.market_cap = await getMarketCap(entry.effective_ticker, entry.stockExchange);
entry.kind = 'market_cap';
} catch(ex) {
console.info(`Skipping market cap calculation`);
}
entry.market_cap = await getMarketCap(entry.effective_ticker, entry.stockExchange);
entry.kind = 'market_cap';
} else if (entry.funding) {
entry.kind = 'funding';
} else {
@ -272,7 +266,6 @@ module.exports.fetchCrunchbaseEntries = async function({cache, preferCache}) {
return entry;
// console.info(entry);
} catch (ex) {
console.info(ex);
if (cachedEntry) {
errors.push(`Using cached entry, because can not fetch: ${c.name} ` + ex.message.substring(0, 200));
reporter.write(error("E"));

View File

@ -1,3 +1,3 @@
const port = process.env.PORT || '4000';
const pathPrefix = module.exports.pathPrefix = process.env.PROJECT_NAME ? `/${process.env.PROJECT_NAME}` : '';
module.exports.appUrl = `http://localhost:${port}${pathPrefix}`;
export const pathPrefix = process.env.PROJECT_NAME ? `/${process.env.PROJECT_NAME}` : ''
export const appUrl = `http://localhost:${port}${pathPrefix}`

View File

@ -1,4 +1,4 @@
module.exports.ensureHttps = function(x) {
export default function(x) {
if (!x) {
return x;
}

View File

@ -2,19 +2,17 @@
// Netlify does not allow nested directories for functions
// so if we're building the preview for landscapeapp we will prefix function names with `$landscapeName--`
const path = require('path');
const fs = require('fs');
const ncc = require('@vercel/ncc');
import path from 'path';
import { distPath, projectPath } from './settings';
import fs from 'fs'
import ncc from '@vercel/ncc'
const { distPath } = require('./settings');
const { readJsonFromDist, readJsonFromProject } = require('../src/utils/readJson');
const { settings } = require('./settings');
import items from 'dist/data/items';
import itemsExport from 'dist/data/items-export';
import settings from 'dist/settings';
import lookup from 'project/lookup';
const items = readJsonFromDist('data/items');
const itemsExport = readJsonFromDist('data/items-export');
const lookup = readJsonFromProject('lookup');
const { PROJECT_NAME } = process.env
const { PROJECT_NAME, PROJECT_PATH } = process.env
const destFolder = path.resolve(distPath, 'functions');
const srcFolder = `${process.env.PWD}/src/api`;
@ -29,31 +27,30 @@ async function main() {
const destFile = [PROJECT_NAME, file].filter(_ => _).join('--')
const { code } = await ncc(`${srcFolder}/${file}`);
const finalCode = code
.replaceAll(`readJsonFromDist('settings')`, JSON.stringify(settings))
.replaceAll(`readJsonFromDist('data/items')`, JSON.stringify(items))
.replaceAll(`readJsonFromDist('data/items-export')`, JSON.stringify(itemsExport))
.replaceAll(`readJsonFromProject('lookup')`, JSON.stringify(lookup))
code = code.replace(
'module.exports = eval("require")("dist/data/items");',
`module.exports = ${JSON.stringify(items)};`
)
code = code.replace(
'module.exports = eval("require")("dist/data/items-export");',
`module.exports = ${JSON.stringify(itemsExport)};`
)
code = code.replace(
'module.exports = eval("require")("dist/settings");',
`module.exports = ${JSON.stringify(settings)};`
)
code = code.replace(
'module.exports = eval("require")("project/lookup");',
`module.exports = ${JSON.stringify(lookup)};`
)
fs.writeFileSync(`${destFolder}/${destFile}`, finalCode)
if (finalCode.includes('eval("')) {
console.info('forgot to embed a module: eval detected');
console.info(file);
process.exit(1);
}
if (finalCode.includes('readJsonFromDist(')) {
console.info('readJsonFromDist() detected');
console.info(file);
process.exit(1);
}
if (finalCode.includes('readJsonFromProject(')) {
console.info('readJsonFromProject() detected');
console.info(file);
fs.writeFileSync(`${destFolder}/${destFile}`, code)
if (code.includes('eval("')) {
console.info('forgot to embed a module');
process.exit(1);
}
}
}
main().catch(function(ex) {
console.info(ex);
process.exit(1);

View File

@ -1,18 +1,17 @@
const _ = require('lodash');
const axios = require('axios');
const { errorsReporter, getMessages } = require('./reporter');
import _ from 'lodash';
import axios from 'axios'
import errorsReporter, { getMessages } from './reporter';
const { addFatal } = errorsReporter('general');
module.exports.hasFatalErrors = function() {
export function hasFatalErrors() {
return getMessages().filter( (x) => x.type === 'fatal') > 0;
}
const setFatalError = module.exports.setFatalError = function(errorText) {
export function setFatalError(errorText) {
addFatal(errorText);
}
const reportFatalErrors = module.exports.reportFatalErrors = async function() {
export async function reportFatalErrors() {
if (!process.env.GITHUB_TOKEN) {
console.info(`Can not report fatal errors, GITHUB_TOKEN not provided`);
return;
@ -49,8 +48,7 @@ const reportFatalErrors = module.exports.reportFatalErrors = async function() {
}
async function main() {
setFatalError('FATAL: <div> *b* </div> error number 1');
setFatalError('FATAL: error number 2');
fatalErrors = ['FATAL: <div> *b* </div> error number 1', 'FATAL: error number 2'];
await reportFatalErrors();
}
// uncomment and set env vars to debug

Some files were not shown because too many files have changed in this diff Show More