diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..edfc9216 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +# Stage 1 +FROM node:8 as react-build +WORKDIR /www +COPY . ./ +RUN yarn +RUN yarn build + +# Stage 2 - the production environment +FROM nginx:alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=react-build /app/build /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/app.json b/app.json new file mode 100644 index 00000000..67d452d4 --- /dev/null +++ b/app.json @@ -0,0 +1,38 @@ +{ + "name": "foo-app", + "env": { + "BACKGROUND_COLOR": { + "description": "specify a css color", + "value": "#fefefe", + "required": false + }, + "TITLE": { + "description": "title for your site" + }, + "APP_SECRET": { + "generator": "secret" + } + }, + "options": { + "allow-unauthenticated": false, + "memory": "512Mi", + "cpu": "1" + }, + "build": { + "skip": false + }, + "hooks": { + "prebuild": { + "commands": ["./my-custom-prebuild"] + }, + "postbuild": { + "commands": ["./my-custom-postbuild"] + }, + "precreate": { + "commands": ["echo 'test'"] + }, + "postcreate": { + "commands": ["./setup.sh"] + } + } +} diff --git a/cloud_functions/firebase.json b/cloud_functions/firebase.json index 8846660a..67a15010 100644 --- a/cloud_functions/firebase.json +++ b/cloud_functions/firebase.json @@ -1,7 +1,6 @@ { "functions": { "predeploy": [ - "npm --prefix \"$RESOURCE_DIR\" run fetchConfig", "npm --prefix \"$RESOURCE_DIR\" run lint", "npm --prefix \"$RESOURCE_DIR\" run build" ] diff --git a/cloud_functions/functions/fetchConfig.ts b/cloud_functions/functions/fetchConfig.ts index 4a6cf129..e5d1abab 100644 --- a/cloud_functions/functions/fetchConfig.ts +++ b/cloud_functions/functions/fetchConfig.ts @@ -3,44 +3,57 @@ import * as fs from "fs"; import * as admin from "firebase-admin"; // Initialize Firebase Admin -const serviceAccount = require(`./firebase-credentials.json`); -console.log(`Running on ${serviceAccount.project_id}`); -admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), - databaseURL: `https://${serviceAccount.project_id}.firebaseio.com`, -}); -const db = admin.firestore(); +const serviceAccount = requireIfExists(`./firebase-credentials.json`); -const docConfig2json = async (docPath: string, jsonPath: string) => { - const doc = await db.doc(docPath).get(); - const data = doc.data(); - const jsonData = JSON.stringify(data ? data.config : ""); - fs.writeFileSync(jsonPath, jsonData); -}; +function requireIfExists(module) { + try { + return require(module); + } catch (error) { + // pass and try next file + console.log("serviceAccount json not found"); + return false; + } + // throw('None of the provided modules exist.') +} +if (serviceAccount) { + console.log(`Running on ${serviceAccount.project_id}`); + admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), + databaseURL: `https://${serviceAccount.project_id}.firebaseio.com`, + }); + const db = admin.firestore(); -// Initialize Cloud Firestore Database + const docConfig2json = async (docPath: string, jsonPath: string) => { + const doc = await db.doc(docPath).get(); + const data = doc.data(); + const jsonData = JSON.stringify(data ? data.config : "[]"); + fs.writeFileSync(jsonPath, jsonData); + }; -const main = async () => { - await docConfig2json( - "_FIRETABLE_/_SETTINGS_/_CONFIG_/_HISTORY_", - "./src/history/config.json" - ); - await docConfig2json( - "_FIRETABLE_/_SETTINGS_/_CONFIG_/_ALGOLIA_", - "./src/algolia/config.json" - ); - await docConfig2json( - "_FIRETABLE_/_SETTINGS_/_CONFIG_/_COLLECTION_SYNC_", - "./src/collectionSync/config.json" - ); - await docConfig2json( - "_FIRETABLE_/_SETTINGS_/_CONFIG_/_SNAPSHOT_SYNC_", - "./src/snapshotSync/config.json" - ); - return true; -}; + // Initialize Cloud Firestore Database -main() - .catch(err => console.log(err)) - .then(() => console.log("this will succeed")) - .catch(() => "obligatory catch"); + const main = async () => { + await docConfig2json( + "_FIRETABLE_/_SETTINGS_/_CONFIG_/_HISTORY_", + "./src/history/config.json" + ); + await docConfig2json( + "_FIRETABLE_/_SETTINGS_/_CONFIG_/_ALGOLIA_", + "./src/algolia/config.json" + ); + await docConfig2json( + "_FIRETABLE_/_SETTINGS_/_CONFIG_/_COLLECTION_SYNC_", + "./src/collectionSync/config.json" + ); + await docConfig2json( + "_FIRETABLE_/_SETTINGS_/_CONFIG_/_SNAPSHOT_SYNC_", + "./src/snapshotSync/config.json" + ); + return true; + }; + + main() + .catch(err => console.log(err)) + .then(() => console.log("this will succeed")) + .catch(() => "obligatory catch"); +} diff --git a/cloud_functions/functions/package.json b/cloud_functions/functions/package.json index 6a7d7be1..fcce2bf6 100644 --- a/cloud_functions/functions/package.json +++ b/cloud_functions/functions/package.json @@ -7,7 +7,8 @@ "serve": "npm run build && firebase serve --only functions", "shell": "npm run build && firebase functions:shell", "start": "npm run shell", - "deploy": "firebase deploy", + "deploy": "firebase deploy --only functions:exportTable", + "deployFT": "firebase deploy", "logs": "firebase functions:log" }, "engines": { diff --git a/cloud_functions/functions/src/collectionSync/index.ts b/cloud_functions/functions/src/collectionSync/index.ts index b2a986b7..f3bdd462 100644 --- a/cloud_functions/functions/src/collectionSync/index.ts +++ b/cloud_functions/functions/src/collectionSync/index.ts @@ -24,7 +24,6 @@ const cloneDoc = (targetCollection: string, fieldsToSync: string[]) => ( ) => { const docId = snapshot.id; const docData = snapshot.data(); - console.log(docData); if (!docData) return false; // returns if theres no data in the doc const syncData = fieldsToSync.reduce(docReducer(docData), {}); if (Object.keys(syncData).length === 0) return false; // returns if theres nothing to sync @@ -33,6 +32,10 @@ const cloneDoc = (targetCollection: string, fieldsToSync: string[]) => ( /\{\{(.*?)\}\}/g, replacer(docData) ); + if (collectionPath.includes("//")) { + console.log(`${collectionPath} is an invalid collection path`); + return false; + } db.collection(collectionPath) .doc(docId) .set({ ...syncData, syncedAt: new Date() }, { merge: true }) @@ -57,18 +60,17 @@ const syncDoc = (targetCollection: string, fieldsToSync: string[]) => async ( /\{\{(.*?)\}\}/g, replacer(docData) ); + + if (collectionPath.includes("//")) { + console.log(`${collectionPath} is an invalid collection path`); + return false; + } if (Object.keys(syncData).length === 0) return false; // returns if theres nothing to sync const targetDoc = await db .collection(collectionPath) .doc(docId) .get(); if (!targetDoc.exists) return false; - const targetDocData = targetDoc.data(); - console.log(`syncing ${docId}`, targetDoc.exists, { - targetDocData, - docData, - syncData, - }); db.collection(collectionPath) .doc(docId) .update({ ...syncData, syncedAt: new Date() }) diff --git a/cloud_functions/functions/src/index.ts b/cloud_functions/functions/src/index.ts index f4748373..16d95852 100644 --- a/cloud_functions/functions/src/index.ts +++ b/cloud_functions/functions/src/index.ts @@ -1,3 +1,5 @@ +export { exportTable } from "./export"; + import algoliaFnsGenerator from "./algolia"; import * as algoliaConfig from "./algolia/config.json"; import collectionSyncFnsGenerator from "./collectionSync"; @@ -12,31 +14,30 @@ import * as collectionHistoryConfig from "./history/config.json"; import permissionControlFnsGenerator from "./permissions"; import * as permissionsConfig from "./permissions/config.json"; -import synonymsFnsGenerator from "./synonyms"; -import synonymsConfig from "./synonyms/config"; - -export { exportTable } from "./export"; export { triggerCloudBuild } from "./buildTriggers"; export { scheduledFirestoreBackup, callableFirestoreBackup } from "./backup"; import * as callableFns from "./callable"; export const callable = callableFns; -export const FT_algolia = algoliaConfig.reduce((acc: any, collection) => { +export const FT_algolia = algoliaConfig.reduce((acc: any, collection: any) => { return { ...acc, [collection.name]: algoliaFnsGenerator(collection) }; }, {}); -export const FT_sync = collectionSyncConfig.reduce((acc: any, collection) => { - return { - ...acc, - [`${`${`${collection.source}` - .replace(/\//g, "_") - .replace(/_{.*?}_/g, "_")}`}2${`${`${collection.target}` - .replace(/\//g, "_") - .replace(/_{.*?}_/g, "_")}`}`]: collectionSyncFnsGenerator(collection), - }; -}, {}); +export const FT_sync = collectionSyncConfig.reduce( + (acc: any, collection: any) => { + return { + ...acc, + [`${`${`${collection.source}` + .replace(/\//g, "_") + .replace(/_{.*?}_/g, "_")}`}2${`${`${collection.target}` + .replace(/\//g, "_") + .replace(/_{.*?}_/g, "_")}`}`]: collectionSyncFnsGenerator(collection), + }; + }, + {} +); export const FT_snapshotSync = snapshotSyncConfig.reduce( - (acc: any, collection) => { + (acc: any, collection: any) => { if (collection.fnName) { return { ...acc, @@ -56,7 +57,7 @@ export const FT_snapshotSync = snapshotSyncConfig.reduce( ); export const FT_history = collectionHistoryConfig.reduce( - (acc: any, collection) => { + (acc: any, collection: any) => { return { ...acc, [collection.name @@ -68,7 +69,7 @@ export const FT_history = collectionHistoryConfig.reduce( ); export const FT_permissions = permissionsConfig.reduce( - (acc: any, collection) => { + (acc: any, collection: any) => { return { ...acc, [collection.name]: permissionControlFnsGenerator(collection), @@ -76,12 +77,3 @@ export const FT_permissions = permissionsConfig.reduce( }, {} ); - -export const FT_synonyms = synonymsConfig.reduce((acc: any, collection) => { - return { - ...acc, - [collection.name - .replace(/\//g, "_") - .replace(/_{.*?}_/g, "_")]: synonymsFnsGenerator(collection), - }; -}, {}); diff --git a/cloud_functions/functions/src/permissions/config.json b/cloud_functions/functions/src/permissions/config.json index cf97b77f..fe51488c 100644 --- a/cloud_functions/functions/src/permissions/config.json +++ b/cloud_functions/functions/src/permissions/config.json @@ -1 +1 @@ -[{ "name": "userPermissions", "customTokenFields": ["regions", "roles"] }] +[] diff --git a/cloud_functions/functions/src/synonyms/config.ts b/cloud_functions/functions/src/synonyms/config.ts deleted file mode 100644 index a03d8003..00000000 --- a/cloud_functions/functions/src/synonyms/config.ts +++ /dev/null @@ -1,265 +0,0 @@ -import * as algoliasearch from "algoliasearch"; - -import { env } from "../config"; -import * as _ from "lodash"; -const client = algoliasearch(env.algolia.app, env.algolia.key); - -const generateAlgoliaKey = (fieldName: string, value: string) => - client.generateSecuredApiKey( - env.algolia.search, // Make sure to use a search key - { - filters: `${fieldName}:${value}`, - } - ); - -const cohort2algoliaKey = (cohort: string) => - generateAlgoliaKey("cohort", cohort); - -const tableConnect2ids = records => { - if (records) { - return records.map(r => r.snapshot.objectID); - } else { - return []; - } -}; -const cohort2region = (cohort: string) => - cohort.toUpperCase().replace(/\d+.*$/, ""); - -const locations = [ - { location: "Sydney", region: "SYD" }, - { location: "Singapore", region: "SG" }, - { location: "Amsterdam", region: "AMS" }, - { location: "Oslo", region: "OSL" }, - { location: "New York", region: "NYC" }, - { location: "London", region: "LON" }, - { location: "Nairobi", region: "NAI" }, - { location: "Stockholm", region: "STO" }, -]; - -const location2region = (location: string) => { - const _location = _.find(locations, { location }); - - return _location ? _location.region : "GLOBAL"; -}; -const cohort2location = (cohort: string) => { - const _region = cohort2region(cohort); - switch (_region) { - case "SYD": - return "Sydney"; - case "SG": - return "Singapore"; - case "AMS": - return "Amsterdam"; - case "OSL": - return "Oslo"; - case "LON": - return "London"; - case "STO": - return "Stockholm"; - case "NYC": - return "New York"; - case "NAI": - return "Nairobi"; - default: - return ""; - } -}; - -const cohort2regionCollections = (collections: string[]) => - collections.map(collection => ({ - name: collection, - groups: [ - { - listenerField: "cohort", - synonymField: "region", - transformer: cohort2region, - }, - ], - })); - -const config = [ - { - name: "advisors", - groups: [ - { - listenerField: "location", - synonymField: "region", - transformer: location2region, - }, - ], - }, - { - name: "icManagement/{icId}/icMembers/{memberId}/votes", - groups: [ - { - listenerField: "team", - synonymField: "teamName", - transformer: team => (team[0] ? team[0].teamName : ""), - }, - ], - }, - { - name: "cohorts", - groups: [ - { - listenerField: "cohort", - synonymField: "region", - transformer: cohort2region, - }, - { - listenerField: "cohort", - synonymField: "algoliaKey", - transformer: (cohort: string) => - client.generateSecuredApiKey( - env.algolia.search, // Make sure to use a search key - { - filters: `cohort:${cohort} OR cohort:Global`, - restrictIndices: [ - "teams", - "founders", - "students", - "hubResources", - "advisors", - "partnerships", - ], - } - ), - }, - { - listenerField: "cohort", - synonymField: "demoDayAlgoliaKey", - transformer: (cohort: string) => - client.generateSecuredApiKey( - env.algolia.search, // Make sure to use a search key - { - filters: `cohort:${cohort} AND showOnDemoDayWebsite:true`, - restrictIndices: ["portfolio"], - } - ), - }, - { - listenerField: "location", - synonymField: "demoDayAntlerBiosAlgoliaKey", - transformer: (location: string) => - client.generateSecuredApiKey( - env.algolia.search, // Make sure to use a search key - { - filters: `location:"${location}"`, - restrictIndices: ["antlerBios"], - } - ), - }, - ], - }, - { - name: "icManagement", - groups: [ - { - listenerField: "cohort", - synonymField: "region", - transformer: cohort2region, - }, - { - listenerField: "cohort", - synonymField: "icPage", - transformer: (cohort, doc) => { - if (doc.icType === "IC" || doc.icType === "Delayed IC") { - return `https://firepage.antler.co/IC/${cohort}`; - } else return ""; - }, - }, - - { - listenerField: "cohort", - synonymField: "algoliaKey", - transformer: cohort2algoliaKey, - }, - ], - }, - { - name: "portfolio", - groups: [ - { - listenerField: "cohort", - synonymField: "region", - transformer: cohort2region, - }, - { - listenerField: "cohort", - synonymField: "location", - transformer: cohort2location, - }, - { - listenerField: "id", - synonymField: "onePager", - transformer: id => `https://firepage.antler.co/portfolio/${id}`, - }, - { - listenerField: "coach", - synonymField: "coachID", - transformer: tableConnect2ids, - }, - ], - }, - { - name: "teams", - groups: [ - { - listenerField: "coach", - synonymField: "coachID", - transformer: tableConnect2ids, - }, - { - listenerField: "cohort", - synonymField: "region", - transformer: cohort2region, - }, - { - listenerField: "id", - synonymField: "ddPage", - transformer: id => `https://firepage.antler.co/DD/${id}`, - }, - { - isForced: true, - listenerField: "icResult", - synonymField: "isDecided", - transformer: icResult => { - if (icResult && ["Yes", "No"].includes(icResult)) return true; - else return false; - }, - }, - ], - }, - { - name: "profiles", - groups: [ - { - listenerField: "email", - synonymField: "isInternal", - transformer: email => email.includes("@antler.co"), - }, - { - listenerField: "cohort", - synonymField: "region", - transformer: cohort2region, - }, - ], - }, - { - name: "demodays", - groups: [ - { - listenerField: "cohortDoc", - synonymField: "cohort", - transformer: cohortDoc => cohortDoc?.[0]?.snapshot?.cohort, - }, - ], - }, - ...cohort2regionCollections([ - "hubResources", - "sprintSubmissions", - "trackoutApplications", - "portfolioEnquiries", - ]), -]; -export default config; diff --git a/cloud_functions/functions/src/synonyms/index.ts b/cloud_functions/functions/src/synonyms/index.ts deleted file mode 100644 index 233c4e12..00000000 --- a/cloud_functions/functions/src/synonyms/index.ts +++ /dev/null @@ -1,95 +0,0 @@ -import * as functions from "firebase-functions"; - -import { db } from "../config"; - -type synonymGroup = { - isForced: boolean | undefined; - listenerField: string; - synonymField: string; - transformer: Function; -}; - -const synonyms = (docData, groups: synonymGroup[]) => - groups.reduce(async (acc: any, currGroup) => { - const newValue = await currGroup.transformer( - docData[currGroup.listenerField], - docData - ); - if ( - docData[currGroup.listenerField] && - docData[currGroup.synonymField] !== newValue - ) { - return { - ...(await acc), - [currGroup.synonymField]: newValue, - }; - } else return await acc; - }, {}); - -/** - * - */ -const addSynonymOnUpdate = (groups: synonymGroup[]) => async ( - change: functions.Change -) => { - const beforeData = change.before.data(); - const afterData = change.after.data(); - - if (!beforeData || !afterData) { - return false; - } - const changedGroups = groups.reduce((acc: synonymGroup[], currGroup) => { - if ( - currGroup.isForced || - beforeData[currGroup.listenerField] !== afterData[currGroup.listenerField] - ) { - return [...acc, currGroup]; - } else { - return acc; - } - }, []); - if (changedGroups.length === 0) { - return false; // no changes detected - } - const updates = await synonyms( - { ...afterData, id: change.after.id }, - changedGroups - ); - if (Object.values(updates).length === 0) { - return false; - } else { - const docPath = change.after.ref.path; - return db.doc(docPath).update(updates); - } -}; - -const addSynonymOnCreate = (groups: synonymGroup[]) => async ( - snapshot: FirebaseFirestore.DocumentSnapshot -) => { - const docData = snapshot.data(); - if (!docData) { - return false; - } - const updates = await synonyms({ ...docData, id: snapshot.id }, groups); - if (Object.keys(updates).length === 0) { - return false; - } else { - const docPath = snapshot.ref.path; - return db.doc(docPath).update(updates); - } -}; - -/** - * - * @param collection configuration object - */ -const synonymsFnsGenerator = collection => ({ - onCreate: functions.firestore - .document(`${collection.name}/{docId}`) - .onCreate(addSynonymOnCreate(collection.groups)), - onUpdate: functions.firestore - .document(`${collection.name}/{docId}`) - .onUpdate(addSynonymOnUpdate(collection.groups)), -}); - -export default synonymsFnsGenerator; diff --git a/cloudbuildfunctions.yaml b/cloudbuildfunctions.yaml index fcdf9c1e..33f3c71a 100644 --- a/cloudbuildfunctions.yaml +++ b/cloudbuildfunctions.yaml @@ -12,10 +12,14 @@ steps: entrypoint: yarn args: ["install"] dir: "cloud_functions/functions" + - name: node:10.15.1 + entrypoint: yarn + args: ["fetchConfig"] + dir: "cloud_functions/functions" - name: node:10.15.1 entrypoint: yarn args: - - "deploy" + - "deployFT" - "--project" - "${_PROJECT_ID}" - "--token" diff --git a/www/src/components/Table/formatters/withCustomCell.tsx b/www/src/components/Table/formatters/withCustomCell.tsx index dd07f30d..3dcf1051 100644 --- a/www/src/components/Table/formatters/withCustomCell.tsx +++ b/www/src/components/Table/formatters/withCustomCell.tsx @@ -1,5 +1,4 @@ import React, { Suspense } from "react"; -import clsx from "clsx"; import { FormatterProps } from "react-data-grid"; import { makeStyles, createStyles } from "@material-ui/core";