diff --git a/functions/src/environmentDataViewer/index.js b/functions/src/environmentDataViewer/index.js
new file mode 100644
index 00000000..51b85a76
--- /dev/null
+++ b/functions/src/environmentDataViewer/index.js
@@ -0,0 +1,35 @@
+import * as functions from 'firebase-functions'
+import { getAppFromServiceAccount } from '../utils/serviceAccounts'
+import { dataArrayFromSnap } from '../utils/firestore'
+
+/**
+ * @param {Object} data - Data passed into httpsCallable by client
+ * @param {Object} context - Cloud function context
+ * @param {Object} context.auth - Cloud function context
+ * @param {Object} context.auth.uid - UID of user that made the request
+ * @param {Object} context.auth.name - Name of user that made the request
+ */
+export async function environmentDataViewerRequest(data, context) {
+ console.log('Environment data viewer request:', data)
+ const { projectId } = data
+ // TODO: Confirm user has rights to this environment/serviceAccount
+ // Get app from service account (loaded from project)
+ const app = await getAppFromServiceAccount(data, { projectId })
+ // TODO: Make this dynamice to a number of resources
+ const topLevelResource = app[data.resource]() // database/firestore/etc
+ // TODO: Support multiple levels of query
+ const query =
+ data.resource === 'firestore'
+ ? topLevelResource.collection('projects').get()
+ : topLevelResource.ref('projects').once('value')
+ const dataSnap = await query
+ const results = dataArrayFromSnap(dataSnap)
+ return results
+}
+
+/**
+ * @name environmentDataViewer
+ * Cloud Function triggered by HTTP request
+ * @type {functions.CloudFunction}
+ */
+export default functions.https.onCall(environmentDataViewerRequest)
diff --git a/functions/test/unit/environmentDataViewer.spec.js b/functions/test/unit/environmentDataViewer.spec.js
new file mode 100644
index 00000000..b078a472
--- /dev/null
+++ b/functions/test/unit/environmentDataViewer.spec.js
@@ -0,0 +1,46 @@
+import { to } from 'utils/async'
+
+describe('environmentDataViewer HTTPS Callable Cloud Function', () => {
+ let environmentDataViewer
+ let configStub
+ let adminInitStub
+ let functions
+ let admin
+
+ before(() => {
+ /* eslint-disable global-require */
+ admin = require('firebase-admin')
+ // Stub Firebase's admin.initializeApp
+ adminInitStub = sinon.stub(admin, 'initializeApp')
+ // Stub Firebase's functions.config()
+ functions = require('firebase-functions')
+ configStub = sinon.stub(functions, 'config').returns({
+ firebase: {
+ databaseURL: 'https://not-a-project.firebaseio.com',
+ storageBucket: 'not-a-project.appspot.com',
+ projectId: 'not-a-project.appspot',
+ messagingSenderId: '823357791673'
+ }
+ // Stub any other config values needed by your functions here
+ })
+ environmentDataViewer = require(`./index`).environmentDataViewerRequest
+ /* eslint-enable global-require */
+ })
+
+ after(() => {
+ // Restoring our stubs to the original methods.
+ configStub.restore()
+ adminInitStub.restore()
+ })
+
+ it('responds with hello message when sent an empty request', async () => {
+ const data = {}
+ const context = {}
+ // Invoke request handler with fake data + context objects
+ const [err, response] = await to(environmentDataViewer(data, context))
+ // Confirm no error is thrown
+ expect(err).to.not.exist
+ // Confirm response contains message
+ expect(response).to.have.property('message', 'Hello World')
+ })
+})
diff --git a/src/constants.js b/src/constants.js
index 2c056fb1..8ee9e43c 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -7,6 +7,8 @@ export const NEW_ACTION_TEMPLATE_PATH = '/actions'
export const PROJECT_ACTION_PATH = 'actions'
export const PROJECT_ENVIRONMENTS_PATH = 'environments'
export const PROJECT_EVENTS_PATH = 'events'
+export const PROJECT_DATA_VIEWER_PATH = 'data-viewer'
+export const DATA_VIEWER_SETUP_FORM = 'dataViewerSetup'
export const ACCOUNT_FORM_NAME = 'account'
export const LOGIN_FORM_NAME = 'login'
export const SIGNUP_FORM_NAME = 'signup'
@@ -42,6 +44,12 @@ export const ANALYTICS_EVENT_NAMES = {
deleteRole: 'Delete Role'
}
+export const RESOURCE_OPTIONS = [
+ { value: 'rtdb', label: 'Real Time Database' },
+ { value: 'firestore' },
+ { value: 'storage', label: 'Cloud Storage' }
+]
+
export const formNames = {
account: ACCOUNT_FORM_NAME,
signup: SIGNUP_FORM_NAME,
diff --git a/src/routes/ActionTemplate/components/ActionTemplateBackups/ActionTemplateBackups.js b/src/routes/ActionTemplate/components/ActionTemplateBackups/ActionTemplateBackups.js
index 31a18dd3..c64df3a8 100644
--- a/src/routes/ActionTemplate/components/ActionTemplateBackups/ActionTemplateBackups.js
+++ b/src/routes/ActionTemplate/components/ActionTemplateBackups/ActionTemplateBackups.js
@@ -17,95 +17,95 @@ import Grid from '@material-ui/core/Grid'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import DeleteIcon from '@material-ui/icons/Delete'
import classes from './ActionTemplateBackups.scss'
+import { RESOURCE_OPTIONS } from 'constants'
// const pathTypeOptions = [{ value: 'only' }, { value: 'all but' }]
-const resourcesOptions = [
- { value: 'rtdb', label: 'Real Time Database' },
- { value: 'firestore' },
- { value: 'storage', label: 'Cloud Storage' }
-]
-export const ActionTemplateBackups = ({ fields, steps }) => (
-
-
- {fields.map((member, index, field) => (
-
- }>
-
- {fields.get(index).name || fields.get(index).type || 'No Name'}
-
-
-
-
-
-
-
-
-
- fields.remove(index)}
- color="secondary"
- className={classes.submit}>
-
-
-
-
-
-
-
Source
-
- Select Resource
+function ActionTemplateBackups({ fields, steps }) {
+ return (
+
+
+ {fields.map((member, index, field) => (
+
+ }>
+
+ {fields.get(index).name || fields.get(index).type || 'No Name'}
+
+
+
+
+
+
+
+
+
+ fields.remove(index)}
+ color="secondary"
+ className={classes.submit}>
+
+
+
+
+
+
+
Source
+
+
+ Select Resource
+
+
+ {RESOURCE_OPTIONS.map((option, idx) => (
+
+ ))}
+
+
- {resourcesOptions.map((option, idx) => (
-
- ))}
-
-
-
+ name={`${member}.inputs.0.path`}
+ component={TextField}
+ label="Path"
+ className={classes.field}
+ />
+
-
+
-
-
-
- ))}
-
-)
+
+
+ ))}
+
+ )
+}
ActionTemplateBackups.propTypes = {
fields: PropTypes.object.isRequired,
diff --git a/src/routes/Project/index.js b/src/routes/Project/index.js
index 14a833cd..71f0427a 100644
--- a/src/routes/Project/index.js
+++ b/src/routes/Project/index.js
@@ -29,6 +29,7 @@ export default store => ({
const Actions = require('./routes/Actions').default
const BucketConfig = require('./routes/BucketConfig').default
const Permissions = require('./routes/Permissions').default
+ const DataViewer = require('./routes/DataViewer').default
/* Return getComponent */
cb(null, [
@@ -36,6 +37,7 @@ export default store => ({
Environments(store),
BucketConfig(store),
ProjectEvents(store),
+ DataViewer(store),
Permissions(store)
])
})
diff --git a/src/routes/Project/routes/DataViewer/components/DataViewerPage/DataViewerPage.enhancer.js b/src/routes/Project/routes/DataViewer/components/DataViewerPage/DataViewerPage.enhancer.js
new file mode 100644
index 00000000..2de868d9
--- /dev/null
+++ b/src/routes/Project/routes/DataViewer/components/DataViewerPage/DataViewerPage.enhancer.js
@@ -0,0 +1,56 @@
+import PropTypes from 'prop-types'
+import { get } from 'lodash'
+import { compose } from 'redux'
+import { connect } from 'react-redux'
+import firestoreConnect from 'react-redux-firebase/lib/firestoreConnect'
+import { withStyles } from '@material-ui/core/styles'
+import styles from './DataViewerPage.styles'
+// import { formValueSelector } from 'redux-form'
+// import { formNames } from 'constants'
+import { withHandlers, setPropTypes } from 'recompose'
+import firebase from 'firebase/app'
+import withNotifications from 'modules/notification/components/withNotifications'
+
+export default compose(
+ withNotifications,
+ // Proptypes for props used in HOCs
+ setPropTypes({
+ params: PropTypes.shape({
+ projectId: PropTypes.string.isRequired
+ })
+ }),
+ // create listener for dataViewer, results go into redux
+ firestoreConnect([{ collection: 'dataViewer' }]),
+ // map redux state to props
+ // Map redux state to props
+ connect((state, { params }) => {
+ const {
+ firebase,
+ firestore: { data, ordered }
+ } = state
+ // const formSelector = formValueSelector(formNames.actionRunner)
+ const environmentsById = get(data, `environments-${params.projectId}`)
+ return {
+ uid: firebase.auth.uid,
+ projectId: params.projectId,
+ project: get(data, `projects.${params.projectId}`),
+ environments: get(ordered, `environments-${params.projectId}`),
+ environmentsById
+ }
+ }),
+ withHandlers({
+ getData: ({ showSuccess, showError, projectId }) => formData => {
+ return firebase
+ .functions()
+ .httpsCallable('environmentDataViewer')({ projectId, ...formData })
+ .then(() => {
+ showSuccess('Data loaded')
+ })
+ .catch(err => {
+ showError('Error loading data')
+ return Promise.reject(err)
+ })
+ }
+ }),
+ withStyles(styles)
+)
diff --git a/src/routes/Project/routes/DataViewer/components/DataViewerPage/DataViewerPage.js b/src/routes/Project/routes/DataViewer/components/DataViewerPage/DataViewerPage.js
new file mode 100644
index 00000000..32b9eb9d
--- /dev/null
+++ b/src/routes/Project/routes/DataViewer/components/DataViewerPage/DataViewerPage.js
@@ -0,0 +1,19 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import DataViewerSetupForm from '../DataViewerSetupForm'
+
+function DataViewerPage({ classes, projectId, getData }) {
+ return (
+
+
+
+ )
+}
+
+DataViewerPage.propTypes = {
+ classes: PropTypes.object.isRequired, // from enhancer (withStyles)
+ getData: PropTypes.func.isRequired, // from enhancer (withHandlers)
+ projectId: PropTypes.string.isRequired // from react-router
+}
+
+export default DataViewerPage
diff --git a/src/routes/Project/routes/DataViewer/components/DataViewerPage/DataViewerPage.styles.js b/src/routes/Project/routes/DataViewer/components/DataViewerPage/DataViewerPage.styles.js
new file mode 100644
index 00000000..ff2aa9eb
--- /dev/null
+++ b/src/routes/Project/routes/DataViewer/components/DataViewerPage/DataViewerPage.styles.js
@@ -0,0 +1,5 @@
+export default theme => ({
+ root: {
+ // style code
+ }
+})
diff --git a/src/routes/Project/routes/DataViewer/components/DataViewerPage/index.js b/src/routes/Project/routes/DataViewer/components/DataViewerPage/index.js
new file mode 100644
index 00000000..9b12ce6f
--- /dev/null
+++ b/src/routes/Project/routes/DataViewer/components/DataViewerPage/index.js
@@ -0,0 +1,4 @@
+import DataViewerPage from './DataViewerPage'
+import enhance from './DataViewerPage.enhancer'
+
+export default enhance(DataViewerPage)
diff --git a/src/routes/Project/routes/DataViewer/components/DataViewerSetupForm/DataViewerSetupForm.enhancer.js b/src/routes/Project/routes/DataViewer/components/DataViewerSetupForm/DataViewerSetupForm.enhancer.js
new file mode 100644
index 00000000..5d2c1947
--- /dev/null
+++ b/src/routes/Project/routes/DataViewer/components/DataViewerSetupForm/DataViewerSetupForm.enhancer.js
@@ -0,0 +1,16 @@
+import { get } from 'lodash'
+import { compose } from 'redux'
+import { reduxForm } from 'redux-form'
+import { connect } from 'react-redux'
+import { withStyles } from '@material-ui/core/styles'
+import { DATA_VIEWER_SETUP_FORM } from 'constants'
+import styles from './DataViewerSetupForm.styles'
+
+export default compose(
+ connect(({ firestore: { data, ordered } }, { projectId }) => ({
+ project: get(data, `projects.${projectId}`),
+ environments: get(ordered, `environments-${projectId}`)
+ })),
+ reduxForm({ form: DATA_VIEWER_SETUP_FORM }),
+ withStyles(styles)
+)
diff --git a/src/routes/Project/routes/DataViewer/components/DataViewerSetupForm/DataViewerSetupForm.js b/src/routes/Project/routes/DataViewer/components/DataViewerSetupForm/DataViewerSetupForm.js
new file mode 100644
index 00000000..49f29e88
--- /dev/null
+++ b/src/routes/Project/routes/DataViewer/components/DataViewerSetupForm/DataViewerSetupForm.js
@@ -0,0 +1,94 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Field } from 'redux-form'
+import { map, capitalize } from 'lodash'
+import { Select } from 'redux-form-material-ui'
+import FormControl from '@material-ui/core/FormControl'
+import InputLabel from '@material-ui/core/InputLabel'
+import Button from '@material-ui/core/Button'
+import MenuItem from '@material-ui/core/MenuItem'
+import ListItemText from '@material-ui/core/ListItemText'
+import OutlinedSelect from 'components/OutlinedSelect'
+import { databaseURLToProjectName } from 'utils'
+import { RESOURCE_OPTIONS } from 'constants'
+import Grid from '@material-ui/core/Grid'
+
+function DataViewerSetupForm({ classes, environments, handleSubmit }) {
+ return (
+
+
+
+
+ {map(environments, (environment, envIndex) => (
+
+ ))}
+
+
+
+
+ Select Resource
+
+ {RESOURCE_OPTIONS.map((option, idx) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+DataViewerSetupForm.propTypes = {
+ classes: PropTypes.object.isRequired, // from enhancer (withStyles)
+ handleSubmit: PropTypes.func.isRequired, // from enhancer (reduxForm)
+ environments: PropTypes.array.isRequired // from enhancer (reduxForm)
+}
+
+export default DataViewerSetupForm
diff --git a/src/routes/Project/routes/DataViewer/components/DataViewerSetupForm/DataViewerSetupForm.styles.js b/src/routes/Project/routes/DataViewer/components/DataViewerSetupForm/DataViewerSetupForm.styles.js
new file mode 100644
index 00000000..ff2aa9eb
--- /dev/null
+++ b/src/routes/Project/routes/DataViewer/components/DataViewerSetupForm/DataViewerSetupForm.styles.js
@@ -0,0 +1,5 @@
+export default theme => ({
+ root: {
+ // style code
+ }
+})
diff --git a/src/routes/Project/routes/DataViewer/components/DataViewerSetupForm/index.js b/src/routes/Project/routes/DataViewer/components/DataViewerSetupForm/index.js
new file mode 100644
index 00000000..1d93de72
--- /dev/null
+++ b/src/routes/Project/routes/DataViewer/components/DataViewerSetupForm/index.js
@@ -0,0 +1,4 @@
+import DataViewerSetupForm from './DataViewerSetupForm'
+import enhance from './DataViewerSetupForm.enhancer'
+
+export default enhance(DataViewerSetupForm)
diff --git a/src/routes/Project/routes/DataViewer/index.js b/src/routes/Project/routes/DataViewer/index.js
new file mode 100644
index 00000000..9644f441
--- /dev/null
+++ b/src/routes/Project/routes/DataViewer/index.js
@@ -0,0 +1,24 @@
+import { PROJECT_DATA_VIEWER_PATH as path } from 'constants'
+
+export default store => ({
+ path,
+ /* Async getComponent is only invoked when route matches */
+ getComponent(nextState, cb) {
+ /* Webpack - use 'require.ensure' to create a split point
+ and embed an async module loader (jsonp) when bundling */
+ require.ensure(
+ [],
+ require => {
+ /* Webpack - use require callback to define
+ dependencies for bundling */
+ const DataViewerPage = require('./components/DataViewerPage').default
+
+ /* Return getComponent */
+ cb(null, DataViewerPage)
+
+ /* Webpack named bundle */
+ },
+ 'DataViewerPage'
+ )
+ }
+})