mirror of
https://github.com/rowyio/rowy.git
synced 2026-07-13 05:48:53 +02:00
1
cli/.npmignore
Normal file
1
cli/.npmignore
Normal file
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
170
cli/index.js
Executable file
170
cli/index.js
Executable file
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const chalk = require("chalk");
|
||||
const clear = require("clear");
|
||||
const { printLogo } = require("./logo");
|
||||
const { Command } = require("commander");
|
||||
const terminal = require("./lib/terminal");
|
||||
const inquirer = require("./lib/inquirer");
|
||||
const Configstore = require("configstore");
|
||||
const config = new Configstore("firetable");
|
||||
const { directoryExists } = require("./lib/files");
|
||||
const program = new Command();
|
||||
program.version("0.2.0");
|
||||
|
||||
//TODO: validate if all the required packages exist
|
||||
const systemHealthCheck = async () => {
|
||||
const versions = await terminal.getRequiredVersions();
|
||||
console.log(versions);
|
||||
//throw new Error(chalk.red("missing something"));
|
||||
};
|
||||
// checks the current directory of the cli app
|
||||
const directoryCheck = async () => {
|
||||
let directory = "firetable/www";
|
||||
const isInsideFiretableFolder = directoryExists("www");
|
||||
const firetableAppExists = directoryExists("firetable/www");
|
||||
if (isInsideFiretableFolder) {
|
||||
directory = "www";
|
||||
}
|
||||
if (!isInsideFiretableFolder && !firetableAppExists) {
|
||||
console.log(chalk.red("Firetable app not detected."));
|
||||
console.log(
|
||||
`please ensure you are in the correct directory or run:${chalk.bold(
|
||||
chalk.yellow("firetable init")
|
||||
)} to get started`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const nodeModulesAvailable = directoryExists(`${directory}/node_modules`);
|
||||
if (!nodeModulesAvailable) {
|
||||
await terminal.installFiretableAppPackages(directory);
|
||||
}
|
||||
return directory;
|
||||
};
|
||||
|
||||
const deploy2firebase = async (directory = "firetable/www") => {
|
||||
const projectId = config.get("firebaseProjectId");
|
||||
let hostTarget = config.get("firebaseHostTarget");
|
||||
if (hostTarget) {
|
||||
const { changeTarget } = await inquirer.askChangeFirebaseHostTarget(
|
||||
hostTarget
|
||||
);
|
||||
if (changeTarget) {
|
||||
const response = await inquirer.askFirebaseHostTarget(projectId);
|
||||
hostTarget = response.hostTarget;
|
||||
}
|
||||
} else {
|
||||
const response = await inquirer.askFirebaseHostTarget(projectId);
|
||||
hostTarget = response.hostTarget;
|
||||
}
|
||||
await terminal.buildFiretable(directory);
|
||||
await terminal.setFirebaseHostingTarget(projectId, hostTarget, directory);
|
||||
await terminal.deployToFirebaseHosting(projectId, directory);
|
||||
config.set("firebaseHostTarget", hostTarget);
|
||||
console.log(
|
||||
chalk.green(
|
||||
`\u{1F973}\u{1F973}\u{1F973} \n Firetable has been successfully deployed to 'https://${hostTarget}.web.app' \n \u{1F973}\u{1F973}\u{1F973}`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
program
|
||||
.command("init")
|
||||
.description(
|
||||
"clones firetable repo, installs the npm packages and set the environment variables"
|
||||
)
|
||||
.action(async () => {
|
||||
try {
|
||||
clear();
|
||||
printLogo();
|
||||
// check if all the required packages are available on the machine
|
||||
await systemHealthCheck();
|
||||
const firebaseProjects = await terminal.getFirebaseProjects();
|
||||
|
||||
const { projectId } = await inquirer.selectFirebaseProject(
|
||||
firebaseProjects
|
||||
);
|
||||
config.set("firebaseProjectId", projectId);
|
||||
let envVariables = {
|
||||
projectId,
|
||||
firebaseWebApiKey: "-",
|
||||
algoliaAppId: "_",
|
||||
algoliaSearchKey: "_",
|
||||
};
|
||||
const includeAlgolia = await inquirer.installAlgolia();
|
||||
if (includeAlgolia.installAlgolia) {
|
||||
const algoliaKey = await inquirer.askAlgoliaVariables();
|
||||
envVariables = { ...envVariables, ...algoliaKey };
|
||||
}
|
||||
// clone firetable repo and install app dependencies
|
||||
await terminal.cloneFiretable();
|
||||
|
||||
// set environment variables
|
||||
await terminal.setFiretableENV(envVariables);
|
||||
let firetableAppId;
|
||||
|
||||
const existingFiretableAppId = await terminal.getExistingFiretableApp(
|
||||
projectId
|
||||
);
|
||||
if (existingFiretableAppId) {
|
||||
firetableAppId = existingFiretableAppId;
|
||||
} else {
|
||||
firetableAppId = await terminal.createFiretableWebApp(projectId);
|
||||
}
|
||||
|
||||
const webAppConfig = await terminal.getFiretableWebAppConfig(
|
||||
firetableAppId
|
||||
);
|
||||
|
||||
await terminal.createFirebaseAppConfigFile(webAppConfig);
|
||||
|
||||
console.log(chalk.green("environment variables were set successfully"));
|
||||
console.log(
|
||||
chalk.green("Success: Firetable has been successfully set up!")
|
||||
);
|
||||
console.log(
|
||||
`you can run:${chalk.bold(
|
||||
"firetable local"
|
||||
)} command to run your firetable instance locally`
|
||||
);
|
||||
console.log(
|
||||
`you can run:${chalk.bold(
|
||||
"firetable deploy"
|
||||
)} to deploy you your firetable app to firebase hosting`
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(chalk.red("FAILED:"));
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command("local")
|
||||
.description("run your firetable instance locally")
|
||||
.action(async () => {
|
||||
try {
|
||||
// check directory for firetable
|
||||
let directory = await directoryCheck();
|
||||
if (!directory) return;
|
||||
terminal.startFiretableLocally(directory);
|
||||
} catch (error) {
|
||||
console.log(chalk.red("FAILED:"));
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command("deploy")
|
||||
.description("deploys firetable to a firebase hosting site")
|
||||
.action(async () => {
|
||||
try {
|
||||
// check directory for firetable
|
||||
let directory = await directoryCheck();
|
||||
if (!directory) return;
|
||||
await deploy2firebase(directory);
|
||||
} catch (error) {
|
||||
console.log(chalk.red("FAILED:"));
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
program.parse(process.argv);
|
||||
12
cli/lib/files.js
Normal file
12
cli/lib/files.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
getCurrentDirectoryBase: () => {
|
||||
return path.basename(process.cwd());
|
||||
},
|
||||
|
||||
directoryExists: filePath => {
|
||||
return fs.existsSync(filePath);
|
||||
},
|
||||
};
|
||||
78
cli/lib/inquirer.js
Normal file
78
cli/lib/inquirer.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const inquirer = require("inquirer");
|
||||
|
||||
module.exports = {
|
||||
selectFirebaseProject: projects =>
|
||||
inquirer.prompt({
|
||||
name: "projectId",
|
||||
type: "list",
|
||||
message: "Select the firebase project you want deploy to:",
|
||||
choices: projects,
|
||||
validate: function(value) {
|
||||
if (value.length) {
|
||||
return true;
|
||||
} else {
|
||||
return "Please enter your the firebase project id you would like to deploy to.\n or you can create new project here:https://console.firebase.google.com/project";
|
||||
}
|
||||
},
|
||||
}),
|
||||
installAlgolia: () => {
|
||||
const questions = [
|
||||
{
|
||||
type: "confirm",
|
||||
name: "installAlgolia",
|
||||
message:
|
||||
"do you want integrate firetable with algolia? \n You can add it later on, if you don't have it setup yet",
|
||||
},
|
||||
];
|
||||
return inquirer.prompt(questions);
|
||||
},
|
||||
|
||||
askAlgoliaVariables: () => {
|
||||
const questions = [
|
||||
{
|
||||
name: "algoliaAppId",
|
||||
type: "input",
|
||||
message: "Enter your Algolia App ID:",
|
||||
validate: function(value) {
|
||||
if (value.length) {
|
||||
return true;
|
||||
} else {
|
||||
return "Please enter your Algolia App ID.\n or you can create new project here:https://algolia.com ";
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "algoliaSearchKey",
|
||||
type: "input",
|
||||
message: "Enter your Algolia search key(not the admin key)",
|
||||
validate: function(value) {
|
||||
if (value.length > 5) {
|
||||
return true;
|
||||
} else {
|
||||
return "Please enter a valid api key ";
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
return inquirer.prompt(questions);
|
||||
},
|
||||
deployToFirebaseHosting: () =>
|
||||
inquirer.prompt({
|
||||
type: "confirm",
|
||||
name: "deployToFirebase",
|
||||
message: "Do you want deploy firetable on firebase hosting?",
|
||||
}),
|
||||
askChangeFirebaseHostTarget: hostTarget =>
|
||||
inquirer.prompt({
|
||||
type: "confirm",
|
||||
name: "changeTarget",
|
||||
default: false,
|
||||
message: `do you want to change your current host target (${hostTarget}) ?`,
|
||||
}),
|
||||
askFirebaseHostTarget: projectId =>
|
||||
inquirer.prompt({
|
||||
type: "input",
|
||||
name: "hostTarget",
|
||||
message: `where do you want to deploy?\n you can find your available sites or create new one here: 'https://console.firebase.google.com/u/0/project/${projectId}/hosting' \n enter your site name: `,
|
||||
}),
|
||||
};
|
||||
67
cli/lib/repo.js
Normal file
67
cli/lib/repo.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const CLI = require("clui");
|
||||
const fs = require("fs");
|
||||
const git = require("simple-git/promise")();
|
||||
const Spinner = CLI.Spinner;
|
||||
const touch = require("touch");
|
||||
const _ = require("lodash");
|
||||
|
||||
const inquirer = require("./inquirer");
|
||||
const gh = require("./github");
|
||||
|
||||
module.exports = {
|
||||
createRemoteRepo: async () => {
|
||||
const github = gh.getInstance();
|
||||
const answers = await inquirer.askRepoDetails();
|
||||
|
||||
const data = {
|
||||
name: answers.name,
|
||||
description: answers.description,
|
||||
private: answers.visibility === "private",
|
||||
};
|
||||
|
||||
const status = new Spinner("Creating remote repository...");
|
||||
status.start();
|
||||
|
||||
try {
|
||||
const response = await github.repos.createForAuthenticatedUser(data);
|
||||
return response.data.ssh_url;
|
||||
} finally {
|
||||
status.stop();
|
||||
}
|
||||
},
|
||||
|
||||
createGitignore: async () => {
|
||||
const filelist = _.without(fs.readdirSync("."), ".git", ".gitignore");
|
||||
|
||||
if (filelist.length) {
|
||||
const answers = await inquirer.askIgnoreFiles(filelist);
|
||||
|
||||
if (answers.ignore.length) {
|
||||
fs.writeFileSync(".gitignore", answers.ignore.join("\n"));
|
||||
} else {
|
||||
touch(".gitignore");
|
||||
}
|
||||
} else {
|
||||
touch(".gitignore");
|
||||
}
|
||||
},
|
||||
|
||||
setupRepo: async url => {
|
||||
const status = new Spinner(
|
||||
"Initializing local repository and pushing to remote..."
|
||||
);
|
||||
status.start();
|
||||
|
||||
try {
|
||||
git
|
||||
.init()
|
||||
.then(git.add(".gitignore"))
|
||||
.then(git.add("./*"))
|
||||
.then(git.commit("Initial commit"))
|
||||
.then(git.addRemote("origin", url))
|
||||
.then(git.push("origin", "master"));
|
||||
} finally {
|
||||
status.stop();
|
||||
}
|
||||
},
|
||||
};
|
||||
206
cli/lib/terminal.js
Normal file
206
cli/lib/terminal.js
Normal file
@@ -0,0 +1,206 @@
|
||||
var exec = require("child_process").exec;
|
||||
const CLI = require("clui");
|
||||
const Spinner = CLI.Spinner;
|
||||
// appId regex \d:[0-9]*:web:[0-z]*
|
||||
|
||||
// firetable app Regex │ firetable-app.*
|
||||
function execute(command, callback) {
|
||||
exec(command, function(error, stdout, stderr) {
|
||||
//console.log({ error, stdout, stderr });
|
||||
callback(stdout);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.getRequiredVersions = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
const checkingVersionsStatus = new Spinner(
|
||||
"Checking the versions of required system packages, please wait..."
|
||||
);
|
||||
checkingVersionsStatus.start();
|
||||
execute("git --version", function(git) {
|
||||
execute("node --version", function(node) {
|
||||
execute("yarn --version", function(yarn) {
|
||||
execute("firebase --version", function(firebase) {
|
||||
checkingVersionsStatus.stop();
|
||||
resolve({
|
||||
node: node.replace("\n", ""),
|
||||
git: git.replace("\n", ""),
|
||||
yarn: yarn.replace("\n", ""),
|
||||
firebase: firebase.replace("\n", ""),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.getGitUser = function(callback) {
|
||||
execute("git config --global user.name", function(name) {
|
||||
execute("git config --global user.email", function(email) {
|
||||
callback({
|
||||
name: name.replace("\n", ""),
|
||||
email: email.replace("\n", ""),
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.cloneFiretable = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
const cloningStatus = new Spinner(
|
||||
"cloning the firetable repository, please wait..."
|
||||
);
|
||||
cloningStatus.start();
|
||||
execute("git clone https://github.com/AntlerVC/firetable.git", function() {
|
||||
cloningStatus.stop();
|
||||
const installingPackagesStatus = new Spinner("installing packages");
|
||||
installingPackagesStatus.start();
|
||||
execute("cd firetable/www;yarn;", function(results) {
|
||||
installingPackagesStatus.stop();
|
||||
resolve(results);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.setFiretableENV = envVariables =>
|
||||
new Promise((resolve, reject) => {
|
||||
const status = new Spinner("setting environment variables, please wait...");
|
||||
status.start();
|
||||
const command = `cd firetable/www;node createDotEnv ${envVariables.projectId} ${envVariables.firebaseWebApiKey} ${envVariables.algoliaAppId} ${envVariables.algoliaSearchKey}`;
|
||||
execute(command, function() {
|
||||
status.stop();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.setFirebaseHostingTarget = (
|
||||
projectId,
|
||||
hostingTarget,
|
||||
directory = "firetable/www"
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const status = new Spinner("setting environment variables, please wait...");
|
||||
status.start();
|
||||
const command = `cd ${directory};echo '{}' > .firebaserc;yarn target ${hostingTarget} --project ${projectId}`;
|
||||
execute(command, function() {
|
||||
status.stop();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.deployToFirebaseHosting = (
|
||||
projectId,
|
||||
directory = "firetable/www"
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const status = new Spinner("deploying to firebase hosting, please wait...");
|
||||
status.start();
|
||||
const command = `cd ${directory};firebase deploy --project ${projectId} --only hosting`;
|
||||
execute(command, function(results) {
|
||||
if (results.includes("Error:")) {
|
||||
throw new Error(results);
|
||||
}
|
||||
status.stop();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.startFiretableLocally = directory =>
|
||||
new Promise((resolve, reject) => {
|
||||
const status = new Spinner("Starting firetable locally, please wait...");
|
||||
status.start();
|
||||
execute(`cd ${directory};yarn local`, function() {
|
||||
status.stop();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
module.exports.installFiretableAppPackages = directory =>
|
||||
new Promise((resolve, reject) => {
|
||||
const status = new Spinner("Installing firetable app packages...");
|
||||
status.start();
|
||||
execute(`cd ${directory};yarn`, function() {
|
||||
status.stop();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
module.exports.buildFiretable = (directory = "firetable/www") =>
|
||||
new Promise((resolve, reject) => {
|
||||
const status = new Spinner(
|
||||
"Building firetable, this one might take a while \u{1F602}..."
|
||||
);
|
||||
status.start();
|
||||
execute(`cd ${directory};yarn build`, function() {
|
||||
status.stop();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.getFirebaseProjects = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
const status = new Spinner("Getting your firebase projects...");
|
||||
status.start();
|
||||
execute(`firebase projects:list`, function(results) {
|
||||
status.stop();
|
||||
//console.log(results);
|
||||
if (results.includes("Failed to authenticate")) {
|
||||
throw new Error(results);
|
||||
}
|
||||
const projects = results.match(/(?<=│.*│ )[0-z,-]*(?= *│ \d)/g);
|
||||
resolve(projects);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.getExistingFiretableApp = projectId =>
|
||||
new Promise((resolve, reject) => {
|
||||
const status = new Spinner("Checking for existing firetable web app...");
|
||||
status.start();
|
||||
execute(`firebase apps:list WEB --project ${projectId}`, function(results) {
|
||||
status.stop();
|
||||
const firetableApp = results.match(/│ firetable-app.*/);
|
||||
if (firetableApp) {
|
||||
resolve(firetableApp[0].match(/\d:[0-9]*:web:[0-z]*/)[0]);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.createFiretableWebApp = projectId =>
|
||||
new Promise((resolve, reject) => {
|
||||
const status = new Spinner(`Creating a firetable web app in ${projectId}`);
|
||||
status.start();
|
||||
execute(
|
||||
`firebase apps:create --project ${projectId} web firetable-app`,
|
||||
function(results) {
|
||||
status.stop();
|
||||
resolve(results.match(/(?<=ID: ).*/)[0]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
module.exports.getFiretableWebAppConfig = webAppId =>
|
||||
new Promise((resolve, reject) => {
|
||||
const status = new Spinner(`getting your web app config`);
|
||||
status.start();
|
||||
execute(`firebase apps:sdkconfig WEB ${webAppId}`, function(results) {
|
||||
status.stop();
|
||||
const config = results.match(/{(.*)([\s\S]*)}/)[0];
|
||||
resolve(config);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.createFirebaseAppConfigFile = config =>
|
||||
new Promise((resolve, reject) => {
|
||||
const status = new Spinner(`creatING firebase config file.`);
|
||||
status.start();
|
||||
execute(
|
||||
`cd firetable/www/src/firebase; echo 'export default ${config.replace(
|
||||
/\n/g,
|
||||
""
|
||||
)}' > config.ts`,
|
||||
function(results) {
|
||||
status.stop();
|
||||
resolve(results);
|
||||
}
|
||||
);
|
||||
});
|
||||
52
cli/logo.js
Normal file
52
cli/logo.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const chalk = require("chalk");
|
||||
const figlet = require("figlet");
|
||||
|
||||
const logo = `tttttttttttttttttttttttttttttttttttttttt
|
||||
tttttttttttttttttttttttttttttttttttttttt
|
||||
tttttttttttttttttttttttt tttt
|
||||
tttttttttttttttttttttttt A tttt
|
||||
tttttttttttttttttttttttt AA AA tttt
|
||||
tttttttttttttttttttttttt AA AA tttt
|
||||
tttttttttttttttttttttttt AAA AAA tttt
|
||||
tttttttttttttt tttt
|
||||
tttttttttttttt tttttttt ttttttt tttt
|
||||
tttttttttttttt tttttttt ttttttt tttt
|
||||
tttttttttttttt tttttttt ttttttt tttt
|
||||
tttt tttt
|
||||
tttt ttttttt tttttttt ttttttt tttt
|
||||
tttt ttttttt tttttttt ttttttt tttt
|
||||
tttt ttttttt tttttttt ttttttt tttt
|
||||
tttt tttt
|
||||
tttttttttttttttttttttttttttttttttttttttt
|
||||
tttttttttttttttttttttttttttttttttttttttt`;
|
||||
|
||||
const logo1 = `++++++++++++++++++++++++++++++++++++++++
|
||||
++++++++++++++++++++++++++++++++++++++++
|
||||
++++++++++++++++++++++++ ++++
|
||||
++++++++++++++++++++++++ A ++++
|
||||
++++++++++++++++++++++++ AA AA ++++
|
||||
++++++++++++++++++++++++ AA AA ++++
|
||||
++++++++++++++++++++++++ AAA AAA ++++
|
||||
++++++++++++++ ++++
|
||||
++++++++++++++ ++++++++ +++++++ ++++
|
||||
++++++++++++++ ++++++++ +++++++ ++++
|
||||
++++++++++++++ ++++++++ +++++++ ++++
|
||||
++++ ++++
|
||||
++++ +++++++ ++++++++ +++++++ ++++
|
||||
++++ +++++++ ++++++++ +++++++ ++++
|
||||
++++ +++++++ ++++++++ +++++++ ++++
|
||||
++++ ++++
|
||||
++++++++++++++++++++++++++++++++++++++++
|
||||
++++++++++++++++++++++++++++++++++++++++`;
|
||||
|
||||
module.exports.printLogo = () => {
|
||||
console.log(chalk.red(logo1));
|
||||
console.log(
|
||||
chalk.white(
|
||||
figlet.textSync("FIRETABLE", {
|
||||
font: "rounded",
|
||||
horizontalLayout: "full",
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
13
www/app.yaml
Normal file
13
www/app.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
# [START runtime]
|
||||
runtime: nodejs
|
||||
env: flex
|
||||
# [END runtime]
|
||||
|
||||
# [START handlers]
|
||||
handlers:
|
||||
- url: /
|
||||
static_files: build/index.html
|
||||
upload: build/index.html
|
||||
- url: /(.*)$
|
||||
static_files: build/\1
|
||||
upload: build/(.*)
|
||||
5
www/dispatch.yaml
Normal file
5
www/dispatch.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
dispatch:
|
||||
- url: "firetable-magic.uc.r.appspot.com/"
|
||||
service: default
|
||||
- url: "*.firetable.io/"
|
||||
service: default
|
||||
@@ -64,9 +64,11 @@
|
||||
"yup": "^0.27.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"start": "serve -s build",
|
||||
"prestart": "npm install -g serve",
|
||||
"local": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
"env": "node createDotEnv",
|
||||
"target": "firebase target:apply hosting firetable",
|
||||
|
||||
@@ -72,7 +72,12 @@ const App: React.FC = () => {
|
||||
|
||||
<PrivateRoute
|
||||
exact
|
||||
path={[routes.home, routes.tableWithId, routes.gridWithId]}
|
||||
path={[
|
||||
routes.home,
|
||||
routes.tableWithId,
|
||||
routes.tableGroupWithId,
|
||||
routes.gridWithId,
|
||||
]}
|
||||
render={() => (
|
||||
<FiretableContextProvider>
|
||||
<Switch>
|
||||
@@ -86,8 +91,8 @@ const App: React.FC = () => {
|
||||
render={() => <TableView />}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path={routes.gridWithId}
|
||||
render={() => <GridView />}
|
||||
path={routes.tableGroupWithId}
|
||||
render={() => <TableView />}
|
||||
/>
|
||||
</Switch>
|
||||
</FiretableContextProvider>
|
||||
|
||||
@@ -28,7 +28,6 @@ const searchClient = algoliasearch(
|
||||
process.env.REACT_APP_ALGOLIA_APP_ID ?? "",
|
||||
process.env.REACT_APP_ALGOLIA_SEARCH_API_KEY ?? ""
|
||||
);
|
||||
console.log("SEARCH CLIENT", searchClient);
|
||||
|
||||
export interface IPopupContentsProps
|
||||
extends Omit<IConnectTableSelectProps, "className" | "TextFieldProps"> {}
|
||||
@@ -60,8 +59,6 @@ export default function PopupContents({
|
||||
const [search] = useDebouncedCallback(
|
||||
async (query: string) => {
|
||||
if (!algoliaIndex) return;
|
||||
console.log("SEARCH", query, algoliaIndex, row);
|
||||
|
||||
const data = { ...userClaims, ...row };
|
||||
const filters = config?.filters
|
||||
? config?.filters.replace(/\{\{(.*?)\}\}/g, (m, k) => data[k])
|
||||
|
||||
@@ -75,8 +75,9 @@ export default function Navigation({
|
||||
// Get the table path, including filtering for user permissions
|
||||
const getTablePath = (table: Table): LinkProps["to"] => {
|
||||
if (!table || !userClaims) return "";
|
||||
|
||||
return table.collection;
|
||||
return table.isCollectionGroup
|
||||
? `/tableGroup/${table.collection}`
|
||||
: `/table/${table.collection}`;
|
||||
};
|
||||
|
||||
const currentCollection = tableCollection.split("/")[0];
|
||||
@@ -160,6 +161,7 @@ export default function Navigation({
|
||||
disabled={sectionName === section}
|
||||
// onClick={handleSectionClick(sectionName)}
|
||||
component={Link}
|
||||
replace={true}
|
||||
to={getTablePath(tables[0])}
|
||||
color="inherit"
|
||||
className={classes.sectionButton}
|
||||
|
||||
@@ -54,8 +54,6 @@ function Action({
|
||||
const snack = useContext(SnackContext);
|
||||
const handleRun = () => {
|
||||
setIsRunning(true);
|
||||
console.log("RUN");
|
||||
|
||||
cloudFunction(
|
||||
config.callableName,
|
||||
{
|
||||
|
||||
@@ -29,11 +29,11 @@ const useStyles = makeStyles(theme =>
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function CodeEditor(props: any) {
|
||||
const { handleChange, script } = props;
|
||||
|
||||
const { tableState } = useFiretableContext();
|
||||
console.log({ script });
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const editorRef = useRef<any>();
|
||||
|
||||
@@ -28,7 +28,6 @@ const ColumnSelector = ({
|
||||
};
|
||||
useEffect(() => {
|
||||
if (table) {
|
||||
console.log({ table });
|
||||
getColumns(table);
|
||||
}
|
||||
}, [table]);
|
||||
|
||||
@@ -219,7 +219,6 @@ export default function FormDialog({
|
||||
<ConfigForm
|
||||
type={type}
|
||||
handleChange={key => update => {
|
||||
console.log(key, update);
|
||||
setNewConfig({ ...newConfig, [key]: update });
|
||||
}}
|
||||
config={newConfig}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { SnackContext } from "contexts/snackContext";
|
||||
import { useFiretableContext } from "contexts/firetableContext";
|
||||
import { db } from "../../firebase";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { isCollectionGroup } from "util/fns";
|
||||
|
||||
const ITEM_HEIGHT = 48;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
@@ -159,7 +160,9 @@ export default function ExportCSV() {
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
let query: any = db.collection(tableState?.tablePath!);
|
||||
let query: any = isCollectionGroup()
|
||||
? db.collectionGroup(tableState?.tablePath!)
|
||||
: db.collection(tableState?.tablePath!);
|
||||
// add filters
|
||||
tableState?.filters.forEach(filter => {
|
||||
query = query.where(filter.key, filter.operator, filter.value);
|
||||
|
||||
@@ -13,7 +13,6 @@ const AlgoliaSelect = (props: any) => {
|
||||
} = props;
|
||||
const [searchState] = useAlgolia(algoliaIndex, algoliaKey, filters);
|
||||
|
||||
console.log(filters);
|
||||
const [options, setOptions] = useState<any[]>([]);
|
||||
useEffect(() => {
|
||||
if (Array.isArray(searchState.results?.hits)) {
|
||||
|
||||
@@ -9,8 +9,6 @@ const MigrateButton = ({ columns, needsMigration }) => {
|
||||
|
||||
const configDocPath = tableState?.config.tableConfig.path;
|
||||
const handleColumnMigration = async () => {
|
||||
console.log({ columns });
|
||||
|
||||
const newColumns = columns.reduce((acc, currCol, currIndex) => {
|
||||
const baseCol = {
|
||||
...currCol,
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
Typography,
|
||||
Button,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import { isCollectionGroup } from "../../util/fns";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
|
||||
import Filters from "./Filters";
|
||||
@@ -84,25 +86,27 @@ export default function TableHeader({
|
||||
className={classes.root}
|
||||
>
|
||||
<MigrateButton needsMigration={needsMigration} columns={tempColumns} />
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const initialVal = tempColumns.reduce((acc, currCol) => {
|
||||
if (currCol.type === FieldType.checkbox) {
|
||||
return { ...acc, [currCol.key]: false };
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, {});
|
||||
tableActions?.row.add(initialVal);
|
||||
}}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
>
|
||||
Add Row
|
||||
</Button>
|
||||
</Grid>
|
||||
{!isCollectionGroup() && (
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const initialVal = tempColumns.reduce((acc, currCol) => {
|
||||
if (currCol.type === FieldType.checkbox) {
|
||||
return { ...acc, [currCol.key]: false };
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, {});
|
||||
tableActions?.row.add(initialVal);
|
||||
}}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
>
|
||||
Add Row
|
||||
</Button>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item>{/* <HiddenFields /> */}</Grid>
|
||||
<Grid item>
|
||||
@@ -150,9 +154,11 @@ export default function TableHeader({
|
||||
|
||||
<Grid item />
|
||||
|
||||
<Grid item>
|
||||
<ImportCSV />
|
||||
</Grid>
|
||||
{!isCollectionGroup() && (
|
||||
<Grid item>
|
||||
<ImportCSV />
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item>
|
||||
<ExportCSV />
|
||||
|
||||
@@ -44,12 +44,7 @@ export type FiretableColumn = Column<any> & {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
interface ITableProps {
|
||||
collection: string;
|
||||
filters: FireTableFilter[];
|
||||
}
|
||||
|
||||
export default function Table({ collection, filters }: ITableProps) {
|
||||
export default function Table() {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
const finalColumnClasses = useFinalColumnStyles();
|
||||
@@ -64,12 +59,6 @@ export default function Table({ collection, filters }: ITableProps) {
|
||||
const { userDoc } = useAppContext();
|
||||
const userDocHiddenFields =
|
||||
userDoc.state.doc?.tables[`${tableState?.tablePath}`]?.hiddenFields ?? [];
|
||||
useEffect(() => {
|
||||
if (tableActions && tableState && tableState.tablePath !== collection) {
|
||||
tableActions.table.set(collection, filters);
|
||||
if (sideDrawerRef?.current) sideDrawerRef.current.setCell!(null);
|
||||
}
|
||||
}, [collection]);
|
||||
|
||||
const rowsContainerRef = useRef<HTMLDivElement>(null);
|
||||
// Gets more rows when scrolled down.
|
||||
@@ -138,7 +127,7 @@ export default function Table({ collection, filters }: ITableProps) {
|
||||
const rows = tableState.rows;
|
||||
const rowGetter = (rowIdx: number) => rows[rowIdx];
|
||||
|
||||
const inSubTable = collection.split("/").length > 1;
|
||||
const inSubTable = tableState.tablePath.split("/").length > 1;
|
||||
|
||||
let tableWidth: any = `calc(100% - ${
|
||||
DRAWER_COLLAPSED_WIDTH
|
||||
@@ -152,7 +141,7 @@ export default function Table({ collection, filters }: ITableProps) {
|
||||
<Hotkeys selectedCell={selectedCell} />
|
||||
</Suspense> */}
|
||||
|
||||
{inSubTable && <SubTableBreadcrumbs collection={collection} />}
|
||||
{inSubTable && <SubTableBreadcrumbs collection={tableState.tablePath} />}
|
||||
|
||||
<TableHeader
|
||||
rowHeight={rowHeight}
|
||||
|
||||
@@ -11,11 +11,12 @@ import {
|
||||
DialogTitle,
|
||||
TextField,
|
||||
Button,
|
||||
Select,
|
||||
Typography,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
} from "@material-ui/core";
|
||||
import { useFiretableContext } from "../contexts/firetableContext";
|
||||
import OptionsInput from "./Table/ColumnMenu/ConfigFields/OptionsInput";
|
||||
|
||||
export enum TableSettingsDialogModes {
|
||||
create,
|
||||
update,
|
||||
@@ -31,6 +32,7 @@ export interface ICreateTableDialogProps {
|
||||
section: string;
|
||||
description: string;
|
||||
name: string;
|
||||
isCollectionGroup: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -40,6 +42,7 @@ const FORM_EMPTY_STATE = {
|
||||
collection: "",
|
||||
description: "",
|
||||
roles: ["ADMIN"],
|
||||
isCollectionGroup: false,
|
||||
};
|
||||
export default function TableSettingsDialog({
|
||||
mode,
|
||||
@@ -66,7 +69,11 @@ export default function TableSettingsDialog({
|
||||
settingsActions?.createTable(formState);
|
||||
|
||||
if (router.location.pathname === "/") {
|
||||
router.history.push(`table/${formState.collection}`);
|
||||
router.history.push(
|
||||
`${formState.isCollectionGroup ? "tableGroup" : "table"}/${
|
||||
formState.collection
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
router.history.push(formState.collection);
|
||||
}
|
||||
@@ -123,6 +130,16 @@ export default function TableSettingsDialog({
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormControlLabel
|
||||
control={<Switch />}
|
||||
label={"isCollectionGroup"}
|
||||
labelPlacement="start"
|
||||
value={formState.isCollectionGroup}
|
||||
onChange={e =>
|
||||
handleChange("isCollectionGroup", !formState.isCollectionGroup)
|
||||
}
|
||||
// classes={{ root: classes.formControlLabel, label: classes.label }}
|
||||
/>
|
||||
<TextField
|
||||
value={formState.section}
|
||||
onChange={e => handleChange("section", e.target.value)}
|
||||
|
||||
@@ -5,7 +5,10 @@ export enum routes {
|
||||
signOut = "/signOut",
|
||||
|
||||
table = "/table",
|
||||
tableGroup = "/tableGroup",
|
||||
|
||||
tableWithId = "/table/:id",
|
||||
tableGroupWithId = "/tableGroup/:id",
|
||||
grid = "/grid",
|
||||
gridWithId = "/grid/:id",
|
||||
editor = "/editor",
|
||||
|
||||
@@ -19,6 +19,7 @@ export type Table = {
|
||||
roles: string[];
|
||||
description: string;
|
||||
section: string;
|
||||
isCollectionGroup: boolean;
|
||||
};
|
||||
|
||||
interface FiretableContextProps {
|
||||
|
||||
7
www/src/firebase/config.ts
Normal file
7
www/src/firebase/config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
apiKey: process.env.REACT_APP_FIREBASE_PROJECT_WEB_API_KEY,
|
||||
authDomain: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseapp.com`,
|
||||
databaseURL: `https://${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseio.com`,
|
||||
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
|
||||
storageBucket: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.appspot.com`,
|
||||
};
|
||||
43
www/src/firebase/experiment.ts
Normal file
43
www/src/firebase/experiment.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import firebase from "firebase/app";
|
||||
import "firebase/auth";
|
||||
import "firebase/firestore";
|
||||
import "firebase/functions";
|
||||
import "firebase/storage";
|
||||
|
||||
export let auth: any = false;
|
||||
|
||||
export let db: any = false;
|
||||
|
||||
export let bucket: any = false;
|
||||
export let functions: any = false;
|
||||
|
||||
export let googleProvider: any = false;
|
||||
|
||||
console.log(`fetching config for ${window.location.hostname.split(".")[0]}`);
|
||||
fetch(
|
||||
`https://us-central1-firetable-magic.cloudfunctions.net/getWebAppConfig?projectId=${window.location.hostname.split(
|
||||
"."
|
||||
)[0] ?? "antler-vc"}`
|
||||
)
|
||||
.then(async response => {
|
||||
const config = await response.json();
|
||||
console.log({ config });
|
||||
firebase.initializeApp(config);
|
||||
auth = firebase.auth();
|
||||
|
||||
db = firebase.firestore();
|
||||
// db.settings({ cacheSizeBytes: firebase.firestore.CACHE_SIZE_UNLIMITED });
|
||||
// db.enablePersistence({ synchronizeTabs: true });
|
||||
|
||||
bucket = firebase.storage();
|
||||
functions = firebase.functions();
|
||||
|
||||
googleProvider = new firebase.auth.GoogleAuthProvider().setCustomParameters(
|
||||
{
|
||||
prompt: "select_account",
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
|
||||
export const deleteField = firebase.firestore.FieldValue.delete;
|
||||
@@ -4,15 +4,9 @@ import "firebase/firestore";
|
||||
import "firebase/functions";
|
||||
import "firebase/storage";
|
||||
|
||||
const config = {
|
||||
apiKey: process.env.REACT_APP_FIREBASE_PROJECT_WEB_API_KEY,
|
||||
authDomain: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseapp.com`,
|
||||
databaseURL: `https://${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseio.com`,
|
||||
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
|
||||
storageBucket: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.appspot.com`,
|
||||
};
|
||||
import appConfig from "./config";
|
||||
|
||||
firebase.initializeApp(config);
|
||||
firebase.initializeApp(appConfig);
|
||||
|
||||
export const auth = firebase.auth();
|
||||
|
||||
@@ -29,5 +23,4 @@ export const googleProvider = new firebase.auth.GoogleAuthProvider().setCustomPa
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
export const deleteField = firebase.firestore.FieldValue.delete;
|
||||
|
||||
@@ -49,11 +49,6 @@ const useFiretable = (
|
||||
orderBy,
|
||||
});
|
||||
|
||||
/**Subscribes to _FIRETABLE_/settings/schema to have table configuration pre-cached */
|
||||
const [settingsState, setttingsDispatch] = useTable({
|
||||
path: "_FIRETABLE_/settings/schema",
|
||||
});
|
||||
|
||||
/** set collection path of table */
|
||||
const setTable = (collectionName: string, filters: FireTableFilter[]) => {
|
||||
if (collectionName !== tableState.path || filters !== tableState.filters) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import firebase from "firebase/app";
|
||||
import { FireTableFilter, FiretableOrderBy } from ".";
|
||||
import { SnackContext } from "../../contexts/snackContext";
|
||||
import { cloudFunction } from "../../firebase/callables";
|
||||
import { isCollectionGroup } from "util/fns";
|
||||
const CAP = 1000; // safety paramter sets the upper limit of number of docs fetched by this hook
|
||||
const serverTimestamp = firebase.firestore.FieldValue.serverTimestamp;
|
||||
var characters =
|
||||
@@ -87,7 +88,9 @@ const useTable = (initialOverrides: any) => {
|
||||
});
|
||||
let query:
|
||||
| firebase.firestore.CollectionReference
|
||||
| firebase.firestore.Query = db.collection(tableState.path);
|
||||
| firebase.firestore.Query = isCollectionGroup()
|
||||
? db.collectionGroup(tableState.path)
|
||||
: db.collection(tableState.path);
|
||||
|
||||
filters.forEach(filter => {
|
||||
query = query.where(filter.key, filter.operator, filter.value);
|
||||
|
||||
@@ -6,7 +6,7 @@ import _camelCase from "lodash/camelCase";
|
||||
import _findIndex from "lodash/findIndex";
|
||||
import _find from "lodash/find";
|
||||
import _sortBy from "lodash/sortBy";
|
||||
import { arrayMover } from "../../util/fns";
|
||||
import { arrayMover, isCollectionGroup } from "../../util/fns";
|
||||
import { db, deleteField } from "../../firebase";
|
||||
|
||||
//import
|
||||
@@ -14,10 +14,9 @@ import { db, deleteField } from "../../firebase";
|
||||
const formatPathRegex = /\/[^\/]+\/([^\/]+)/g;
|
||||
|
||||
const formatPath = (tablePath: string) => {
|
||||
return `_FIRETABLE_/settings/schema/${tablePath.replace(
|
||||
formatPathRegex,
|
||||
"/subTables/$1"
|
||||
)}`;
|
||||
return `_FIRETABLE_/settings/${
|
||||
isCollectionGroup() ? "groupSchema" : "schema"
|
||||
}/${tablePath.replace(formatPathRegex, "/subTables/$1")}`;
|
||||
};
|
||||
const useTableConfig = (tablePath?: string) => {
|
||||
const [tableConfigState, documentDispatch] = useDoc({
|
||||
@@ -50,7 +49,6 @@ const useTableConfig = (tablePath?: string) => {
|
||||
const add = (name: string, type: FieldType, data?: any) => {
|
||||
//TODO: validation
|
||||
const { columns } = tableConfigState;
|
||||
console.log({ columns });
|
||||
const newIndex = Object.keys(columns).length;
|
||||
let updatedColumns = columns;
|
||||
const key = _camelCase(name);
|
||||
|
||||
@@ -53,3 +53,8 @@ export const sanitiseRowData = (rowData: any) => {
|
||||
});
|
||||
return rowData;
|
||||
};
|
||||
|
||||
export const isCollectionGroup = () => {
|
||||
const pathName = window.location.pathname.split("/")[1];
|
||||
return pathName === "tableGroup";
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import queryString from "query-string";
|
||||
import { useFiretableContext } from "contexts/firetableContext";
|
||||
|
||||
import { Hidden } from "@material-ui/core";
|
||||
|
||||
@@ -13,7 +14,7 @@ import useRouter from "hooks/useRouter";
|
||||
export default function TableView() {
|
||||
const router = useRouter();
|
||||
const tableCollection = decodeURIComponent(router.match.params.id);
|
||||
|
||||
const { tableState, tableActions, sideDrawerRef } = useFiretableContext();
|
||||
let filters: FireTableFilter[] = [];
|
||||
const parsed = queryString.parse(router.location.search);
|
||||
if (typeof parsed.filters === "string") {
|
||||
@@ -23,14 +24,20 @@ export default function TableView() {
|
||||
//TODO: json schema validator
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
tableActions &&
|
||||
tableState &&
|
||||
tableState.tablePath !== tableCollection
|
||||
) {
|
||||
tableActions.table.set(tableCollection, filters);
|
||||
if (sideDrawerRef?.current) sideDrawerRef.current.setCell!(null);
|
||||
}
|
||||
}, [tableCollection]);
|
||||
if (!tableState?.tablePath) return <></>;
|
||||
return (
|
||||
<Navigation tableCollection={tableCollection}>
|
||||
<Table
|
||||
key={tableCollection}
|
||||
collection={tableCollection}
|
||||
filters={filters}
|
||||
/>
|
||||
|
||||
<Table key={tableCollection} />
|
||||
<Hidden smDown>
|
||||
<SideDrawer />
|
||||
</Hidden>
|
||||
|
||||
@@ -89,6 +89,7 @@ const TablesView = () => {
|
||||
roles: string[];
|
||||
name: string;
|
||||
section: string;
|
||||
isCollectionGroup: boolean;
|
||||
};
|
||||
}>({
|
||||
mode: null,
|
||||
@@ -137,7 +138,9 @@ const TablesView = () => {
|
||||
}
|
||||
bodyContent={table.description}
|
||||
primaryLink={{
|
||||
to: `${routes.table}/${table.collection}`,
|
||||
to: `${
|
||||
table.isCollectionGroup ? routes.tableGroup : routes.table
|
||||
}/${table.collection}`,
|
||||
label: "Open",
|
||||
}}
|
||||
secondaryAction={
|
||||
|
||||
Reference in New Issue
Block a user