├── .codecov.yml
├── .env
├── .env.test
├── .eslintignore
├── .eslintrc.js
├── .firebaserc
├── .github
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
└── workflows
│ ├── app-deploy.yml
│ ├── app-verify.yml
│ ├── docs-deploy.yml
│ └── docs-verify.yml
├── .gitignore
├── .size-limit.js
├── .yo-rc.json
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── bin
└── generate-firebase-sdk-config.js
├── cypress.json
├── cypress
├── .eslintrc.js
├── Dockerfile
├── fixtures
│ ├── example.json
│ ├── fakeEnvironment.json
│ ├── fakeEvent.json
│ ├── fakeProject.json
│ └── fakeServiceAccount.json
├── integration
│ ├── ActionTemplates
│ │ ├── ActionTemplatePage.spec.js
│ │ └── ActionTemplatesPage.spec.js
│ ├── Docs.spec.js
│ ├── HomePage.spec.js
│ ├── LoginPage.spec.js
│ ├── Project
│ │ ├── ActionRunner.spec.js
│ │ ├── Environments.spec.js
│ │ ├── Events.spec.js
│ │ └── Permissions.spec.js
│ └── ProjectsPage.spec.js
├── plugins
│ └── index.js
├── support
│ ├── commands.js
│ └── index.js
└── utils
│ ├── commands.js
│ └── index.js
├── database.rules.json
├── docs
├── .env.development
├── .eslintignore
├── .eslintrc.js
├── README.md
├── content
│ ├── docs
│ │ ├── README.md
│ │ └── adding-to-these-docs.md
│ ├── guides
│ │ ├── README.md
│ │ ├── custom-action-template.md
│ │ └── initial-setup.md
│ ├── source
│ │ ├── README.md
│ │ ├── adding-a-feature.md
│ │ └── file-structure.md
│ └── testing
│ │ ├── README.md
│ │ ├── cloud-functions-tests.md
│ │ └── ui-tests.md
├── data
│ └── siteConfig.js
├── gatsby-config.js
├── gatsby-node.js
├── package.json
├── src
│ ├── assets
│ │ ├── FireadminLogo.png
│ │ └── gatsby-icon.png
│ ├── components
│ │ ├── Article
│ │ │ ├── Article.css
│ │ │ └── Article.js
│ │ ├── Bio
│ │ │ ├── Bio.css
│ │ │ └── Bio.js
│ │ ├── Content
│ │ │ ├── Content.css
│ │ │ ├── Content.js
│ │ │ └── GatsbyHighlight.css
│ │ ├── ContentHeader
│ │ │ ├── ContentHeader.css
│ │ │ └── ContentHeader.js
│ │ ├── Hero
│ │ │ ├── Hero.css
│ │ │ └── Hero.js
│ │ ├── HomePage
│ │ │ ├── HomePage.js
│ │ │ ├── HomePage.styles.js
│ │ │ └── index.js
│ │ ├── Loading
│ │ │ ├── Loading.js
│ │ │ ├── Loading.styles.js
│ │ │ └── index.js
│ │ ├── PostsList
│ │ │ └── PostsList.js
│ │ ├── PostsListItem
│ │ │ ├── PostsListItem.css
│ │ │ └── PostsListItem.js
│ │ ├── SEO
│ │ │ └── SEO.js
│ │ ├── SidebarItem
│ │ │ ├── SidebarItem.js
│ │ │ └── index.js
│ │ ├── TagList
│ │ │ ├── TagList.css
│ │ │ └── TagList.js
│ │ ├── Wrapper
│ │ │ ├── Wrapper.css
│ │ │ └── Wrapper.js
│ │ ├── index.css
│ │ └── layout.js
│ ├── constants
│ │ └── paths.js
│ ├── html.js
│ ├── pages
│ │ ├── 404.js
│ │ └── index.js
│ ├── templates
│ │ └── page.js
│ └── utils
│ │ └── index.js
├── static
│ ├── .nojekyll
│ ├── favicon.ico
│ ├── images
│ │ ├── FireadminLogo.png
│ │ └── cover.jpg
│ └── robots.txt
└── yarn.lock
├── firebase.json
├── firestore.indexes.json
├── firestore.rules
├── functions
├── .eslintrc.js
├── .gitignore
├── .mocharc.js
├── .nycrc
├── README.md
├── index.js
├── index.spec.ts
├── jsconfig.json
├── package.json
├── scripts
│ └── testSetup.js
├── src
│ ├── actionRunner
│ │ ├── actionRunner.spec.ts
│ │ ├── actions.ts
│ │ ├── constants.ts
│ │ ├── index.ts
│ │ ├── runAction.ts
│ │ ├── runSteps.ts
│ │ ├── types.ts
│ │ └── utils.ts
│ ├── callGoogleApi
│ │ ├── callGoogleApi.spec.ts
│ │ ├── callGoogleApi.ts
│ │ ├── constants.ts
│ │ └── index.ts
│ ├── cleanupProject
│ │ ├── cleanupProject.spec.ts
│ │ └── index.ts
│ ├── constants
│ │ └── firebasePaths.ts
│ ├── copyServiceAccountToFirestore
│ │ ├── copyServiceAccountToFirestore.spec.ts
│ │ └── index.ts
│ ├── indexActionTemplates
│ │ ├── index.ts
│ │ └── indexActionTemplates.spec.ts
│ ├── indexUser
│ │ ├── index.ts
│ │ └── indexUser.spec.ts
│ └── utils
│ │ ├── async.ts
│ │ ├── cloudStorage.ts
│ │ ├── encryption.ts
│ │ ├── firestore.ts
│ │ ├── index.ts
│ │ ├── search.ts
│ │ └── serviceAccounts.ts
├── tsconfig.json
└── yarn.lock
├── husky
└── pre-commit
├── jsconfig.json
├── package.json
├── public
├── favicon.ico
├── firebase-messaging-sw.js
├── humans.txt
├── index.html
├── manifest.json
└── robots.txt
├── scripts
└── snapshotResolver.js
├── src
├── components
│ ├── AnalyticsPageViewLogger
│ │ ├── AnalyticsPageViewLogger.js
│ │ └── index.js
│ ├── CollectionSearch
│ │ ├── CollectionSearch.js
│ │ ├── CollectionSearch.styles.js
│ │ ├── ResultsList.js
│ │ ├── SearchResults.js
│ │ ├── SuggestedItem.js
│ │ └── index.js
│ ├── LoadingSpinner
│ │ ├── LoadingSpinner.js
│ │ ├── LoadingSpinner.styles.js
│ │ ├── LoadingSpinner.test.js
│ │ └── index.js
│ ├── SetupFirestore
│ │ ├── SetupFirestore.js
│ │ └── index.js
│ ├── SetupMessaging
│ │ ├── SetupMessaging.js
│ │ ├── index.js
│ │ └── useSetupMessaging.js
│ ├── SplitButton
│ │ ├── SplitButton.js
│ │ └── index.js
│ ├── TabContainer
│ │ ├── TabContainer.js
│ │ └── index.js
│ ├── UsersList
│ │ ├── UsersList.js
│ │ └── index.js
│ ├── UsersSearch
│ │ ├── ResultsList.js
│ │ ├── SearchResults.js
│ │ ├── SuggestedUser.js
│ │ ├── UsersSearch.js
│ │ ├── UsersSearch.styles.js
│ │ └── index.js
│ └── VersionChangeReloader
│ │ └── index.js
├── constants
│ ├── analytics.js
│ ├── defaultRoles.js
│ ├── docs.js
│ ├── firebasePaths.js
│ ├── formNames.js
│ └── paths.js
├── containers
│ ├── App
│ │ ├── App.js
│ │ └── index.js
│ └── Navbar
│ │ ├── AccountMenu.js
│ │ ├── Navbar.js
│ │ ├── Navbar.styles.js
│ │ ├── NavbarWithoutAuth.js
│ │ └── index.js
├── index.css
├── index.js
├── layouts
│ ├── CoreLayout
│ │ ├── CoreLayout.js
│ │ ├── CoreLayout.styles.js
│ │ └── index.js
│ └── SidebarLayout
│ │ ├── SidebarLayout.js
│ │ ├── SidebarLayout.styles.js
│ │ ├── SidebarList.js
│ │ ├── index.js
│ │ └── sidebarOptions.js
├── modules
│ └── notification
│ │ ├── Notifications.js
│ │ ├── NotificationsProvider.js
│ │ ├── actionTypes.js
│ │ ├── actions.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── useNotifications.js
├── routes
│ ├── Account
│ │ ├── components
│ │ │ ├── AccountEditor
│ │ │ │ ├── AccountEditor.js
│ │ │ │ ├── AccountEditor.styles.js
│ │ │ │ └── index.js
│ │ │ ├── AccountForm
│ │ │ │ ├── AccountForm.js
│ │ │ │ ├── AccountForm.styles.js
│ │ │ │ └── index.js
│ │ │ ├── AccountPage
│ │ │ │ ├── AccountPage.js
│ │ │ │ ├── AccountPage.styles.js
│ │ │ │ └── index.js
│ │ │ └── ProviderDataForm
│ │ │ │ ├── ProviderDataForm.js
│ │ │ │ └── index.js
│ │ └── index.js
│ ├── ActionTemplate
│ │ ├── components
│ │ │ ├── ActionStepLocation
│ │ │ │ ├── ActionStepLocation.js
│ │ │ │ ├── ActionStepLocation.styles.js
│ │ │ │ └── index.js
│ │ │ ├── ActionTemplateBackups
│ │ │ │ ├── ActionTemplateBackups.js
│ │ │ │ ├── ActionTemplateBackups.styles.js
│ │ │ │ └── index.js
│ │ │ ├── ActionTemplateEnvs
│ │ │ │ ├── ActionTemplateEnvs.js
│ │ │ │ ├── ActionTemplateEnvs.styles.js
│ │ │ │ └── index.js
│ │ │ ├── ActionTemplateForm
│ │ │ │ ├── ActionTemplateForm.js
│ │ │ │ ├── ActionTemplateForm.styles.js
│ │ │ │ └── index.js
│ │ │ ├── ActionTemplateInputs
│ │ │ │ ├── ActionTemplateInputs.js
│ │ │ │ ├── ActionTemplateInputs.styles.js
│ │ │ │ └── index.js
│ │ │ ├── ActionTemplatePage
│ │ │ │ ├── ActionTemplatePage.js
│ │ │ │ ├── ActionTemplatePage.styles.js
│ │ │ │ ├── TemplateLoadingError.js
│ │ │ │ ├── TemplateNotFound.js
│ │ │ │ └── index.js
│ │ │ ├── ActionTemplateSteps
│ │ │ │ ├── ActionTemplateSteps.js
│ │ │ │ ├── ActionTemplateSteps.styles.js
│ │ │ │ └── index.js
│ │ │ └── DeleteTemplateDialog
│ │ │ │ ├── DeleteTemplateDialog.js
│ │ │ │ └── index.js
│ │ └── index.js
│ ├── ActionTemplates
│ │ ├── components
│ │ │ ├── ActionTemplateListCard
│ │ │ │ ├── ActionTemplateListCard.js
│ │ │ │ ├── ActionTemplateListCard.styles.js
│ │ │ │ └── index.js
│ │ │ ├── ActionTemplatesList
│ │ │ │ ├── ActionTemplatesList.js
│ │ │ │ ├── ActionTemplatesList.styles.js
│ │ │ │ └── index.js
│ │ │ ├── ActionTemplatesPage
│ │ │ │ ├── ActionTemplatesPage.js
│ │ │ │ ├── ActionTemplatesPage.styles.js
│ │ │ │ └── index.js
│ │ │ ├── NewActionTemplateDialog
│ │ │ │ ├── NewActionTemplateDialog.js
│ │ │ │ ├── NewActionTemplateDialog.styles.js
│ │ │ │ └── index.js
│ │ │ └── PrivateActionTemplates
│ │ │ │ ├── PrivateActionTemplates.js
│ │ │ │ ├── PrivateActionTemplates.styles.js
│ │ │ │ └── index.js
│ │ └── index.js
│ ├── Home
│ │ ├── components
│ │ │ └── HomePage
│ │ │ │ ├── HomePage.js
│ │ │ │ ├── HomePage.styles.js
│ │ │ │ └── index.js
│ │ └── index.js
│ ├── Login
│ │ ├── components
│ │ │ └── LoginPage
│ │ │ │ ├── LoginPage.js
│ │ │ │ ├── LoginPage.styles.js
│ │ │ │ └── index.js
│ │ └── index.js
│ ├── NotFound
│ │ ├── components
│ │ │ └── NotFoundPage
│ │ │ │ ├── NotFoundPage.js
│ │ │ │ ├── NotFoundPage.styles.js
│ │ │ │ └── index.js
│ │ └── index.js
│ ├── Projects
│ │ ├── components
│ │ │ ├── NewProjectDialog
│ │ │ │ ├── NewProjectDialog.js
│ │ │ │ ├── NewProjectDialog.styles.js
│ │ │ │ └── index.js
│ │ │ ├── NewProjectTile
│ │ │ │ ├── NewProjectTile.js
│ │ │ │ ├── NewProjectTile.styles.js
│ │ │ │ └── index.js
│ │ │ ├── ProjectTile
│ │ │ │ ├── ProjectTile.js
│ │ │ │ ├── ProjectTile.styles.js
│ │ │ │ └── index.js
│ │ │ ├── ProjectsList
│ │ │ │ ├── ProjectsList.js
│ │ │ │ ├── ProjectsList.styles.js
│ │ │ │ └── index.js
│ │ │ ├── ProjectsPage
│ │ │ │ ├── ProjectsPage.js
│ │ │ │ ├── ProjectsPage.styles.js
│ │ │ │ └── index.js
│ │ │ └── SharingDialog
│ │ │ │ ├── SharingDialog.js
│ │ │ │ ├── SharingDialog.styles.js
│ │ │ │ └── index.js
│ │ ├── index.js
│ │ └── routes
│ │ │ └── Project
│ │ │ ├── components
│ │ │ ├── OverviewPanel
│ │ │ │ ├── OverviewPanel.js
│ │ │ │ ├── OverviewPanel.styles.js
│ │ │ │ └── index.js
│ │ │ └── ProjectPage
│ │ │ │ ├── ProjectErrorPage.js
│ │ │ │ ├── ProjectNotFoundPage.js
│ │ │ │ ├── ProjectPage.js
│ │ │ │ ├── ProjectPage.styles.js
│ │ │ │ └── index.js
│ │ │ ├── index.js
│ │ │ └── routes
│ │ │ ├── Actions
│ │ │ ├── components
│ │ │ │ ├── ActionsPage
│ │ │ │ │ ├── ActionPage.styles.js
│ │ │ │ │ ├── ActionsPage.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── useActionsPage.js
│ │ │ │ ├── ActionsRunnerForm
│ │ │ │ │ ├── ActionsRunnerForm.js
│ │ │ │ │ ├── ActionsRunnerForm.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── PrivateActionTemplates
│ │ │ │ │ ├── NoTemplatesFound.js
│ │ │ │ │ ├── PrivateActionTemplates.js
│ │ │ │ │ ├── PrivateActionTemplates.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── RecentActions
│ │ │ │ │ ├── NoRecentActions.js
│ │ │ │ │ ├── RecentActions.js
│ │ │ │ │ ├── RecentActions.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ └── StepsViewer
│ │ │ │ │ ├── StepsViewer.js
│ │ │ │ │ └── index.js
│ │ │ └── index.js
│ │ │ ├── BucketConfig
│ │ │ ├── components
│ │ │ │ ├── BucketConfigForm
│ │ │ │ │ ├── BucketConfigForm.js
│ │ │ │ │ ├── BucketConfigForm.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── BucketConfigPage
│ │ │ │ │ ├── BucketConfigPage.js
│ │ │ │ │ ├── BucketConfigPage.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── CorsList
│ │ │ │ │ ├── CorsList.js
│ │ │ │ │ ├── CorsList.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ └── CorsOriginList
│ │ │ │ │ ├── CorsOriginList.js
│ │ │ │ │ ├── CorsOriginList.styles.js
│ │ │ │ │ └── index.js
│ │ │ └── index.js
│ │ │ ├── Environments
│ │ │ ├── components
│ │ │ │ ├── AddEnvironmentDialog
│ │ │ │ │ ├── AddEnvironmentDialog.js
│ │ │ │ │ ├── AddEnvironmentDialog.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── DeleteEnvironmentDialog
│ │ │ │ │ ├── DeleteEnvironmentDialog.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── EditEnvironmentDialog
│ │ │ │ │ ├── EditEnvironmentDialog.js
│ │ │ │ │ ├── EditEnvironmentDialog.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── EnvironmentsPage
│ │ │ │ │ ├── EnvironmentsPage.js
│ │ │ │ │ ├── EnvironmentsPage.styles.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── useEnvironmentsPage.js
│ │ │ │ ├── FilesUploader
│ │ │ │ │ ├── FilesUploader.js
│ │ │ │ │ ├── FilesUploader.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ └── Instance
│ │ │ │ │ ├── Instance.js
│ │ │ │ │ ├── Instance.styles.js
│ │ │ │ │ └── index.js
│ │ │ └── index.js
│ │ │ ├── Permissions
│ │ │ ├── components
│ │ │ │ ├── DeleteMemberModal
│ │ │ │ │ ├── DeleteMemberModal.js
│ │ │ │ │ ├── DeleteMemberModal.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── NewMemberModal
│ │ │ │ │ ├── NewMemberModal.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── useNewMemberModal.js
│ │ │ │ ├── NewRoleCard
│ │ │ │ │ ├── NewRoleCard.js
│ │ │ │ │ ├── NewRoleCard.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── Permissions
│ │ │ │ │ ├── Permissions.js
│ │ │ │ │ ├── Permissions.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── PermissionsTable
│ │ │ │ │ ├── NoPermissionsFound.js
│ │ │ │ │ ├── PermissionsTable.js
│ │ │ │ │ ├── PermissionsTable.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── PermissionsTableRow
│ │ │ │ │ ├── PermissionsTableRow.js
│ │ │ │ │ ├── PermissionsTableRow.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── RolesTable
│ │ │ │ │ ├── NoRolesFound.js
│ │ │ │ │ ├── RolesTable.js
│ │ │ │ │ ├── RolesTable.styles.js
│ │ │ │ │ └── index.js
│ │ │ │ └── RolesTableRow
│ │ │ │ │ ├── RolesTableRow.js
│ │ │ │ │ ├── RolesTableRow.styles.js
│ │ │ │ │ └── index.js
│ │ │ └── index.js
│ │ │ └── ProjectEvents
│ │ │ ├── components
│ │ │ └── ProjectEventsPage
│ │ │ │ ├── NoProjectEvents.js
│ │ │ │ ├── ProjectEventsPage.js
│ │ │ │ ├── ProjectEventsPage.styles.js
│ │ │ │ └── index.js
│ │ │ └── index.js
│ └── index.js
├── static
│ ├── User.png
│ └── logo.svg
├── theme.js
└── utils
│ ├── analytics.js
│ ├── async.js
│ ├── components.js
│ ├── data.js
│ ├── device.js
│ ├── errorHandler.js
│ ├── firebaseFunctions.js
│ ├── form.js
│ ├── formatters.js
│ ├── index.js
│ └── router.js
├── storage.rules
└── yarn.lock
/.codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | target: auto
6 | patch: off
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # Needed to skip warnings from jest@beta in package.json
2 | SKIP_PREFLIGHT_CHECK=true
--------------------------------------------------------------------------------
/.env.test:
--------------------------------------------------------------------------------
1 | # Needed to skip warnings from jest@beta in package.json
2 | SKIP_PREFLIGHT_CHECK=true
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/coverage/**
2 | **/node_modules/**
3 | build/**
4 | src/index.html
5 | blueprints/**
6 | src/config.js
7 | functions/dist/**
8 | functions/test/mocha.opts
9 | functions/coverage/**
10 | functions/mochawesome-report/**
11 | docs/.cache/**
12 | docs/public/**
13 | docs/content/**
14 | docs/static/**
15 | cypress/screenshots/**
16 | cypress/videos/**
17 | .nyc_output/**
18 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "master": "fireadmin-stage",
4 | "prod": "fireadmin-33d82",
5 | "stage": "fireadmin-stage",
6 | "default": "fireadmin-stage"
7 | },
8 | "ci": {
9 | "setEnv": {
10 | "master": {
11 | "REACT_APP_FIREADMIN_ENV": "stage",
12 | "REACT_APP_SEGMENT_ID": "HWEhRQnBTbnYCgv9b4qeA793wVcR0W5b",
13 | "REACT_APP_SENTRY_DSN": "https://65bfbcc3e0f94d1391daae55500165fa@sentry.io/1238306",
14 | "REACT_APP_PUBLIC_VAPID_KEY": "BMQbbkyTknePwCeoqbyFVC6z4WJ6A3mdFKgIeNXDmcwUwl_DLIY6LYHKOAedvZW4rge-cD0iO4ZP7nxZ3LbFJVk",
15 | "REACT_APP_ALGOLIA_APP_ID": "WULRUE0M3R",
16 | "REACT_APP_ALGOLIA_API_KEY": "96c289817959585293abf860ec7aca8a"
17 | },
18 | "prod": {
19 | "REACT_APP_FIREADMIN_ENV": "production",
20 | "REACT_APP_SEGMENT_ID": "F9YnTGtlTuDSYnOmzIqgPVvY2SRJ4efI",
21 | "REACT_APP_SENTRY_DSN": "https://65bfbcc3e0f94d1391daae55500165fa@sentry.io/1238306",
22 | "REACT_APP_PUBLIC_VAPID_KEY": "BKcXWc4wy3PcmxBAbYZJ4qtqQQdqnEfmVJVEzNSZ95SUK89ZuqT6XjCk7VaSJWOJgXYiSN-d07m3gcvDSQnxRnA",
23 | "REACT_APP_ALGOLIA_APP_ID": "C0D1I0GB86",
24 | "REACT_APP_ALGOLIA_API_KEY": "7327fc566154893e3834a49de6fa73c3"
25 | }
26 | }
27 | },
28 | "targets": {
29 | "fireadmin-stage": {
30 | "hosting": {
31 | "docs": [
32 | "fireadmin-stage-docs"
33 | ],
34 | "app": [
35 | "fireadmin-stage"
36 | ]
37 | }
38 | },
39 | "fireadmin-33d82": {
40 | "hosting": {
41 | "docs": [
42 | "fireadmin-docs"
43 | ],
44 | "app": [
45 | "fireadmin-33d82"
46 | ]
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | ---
5 |
6 | ## Bug Description
7 |
8 |
9 | ## Steps To Reproduce
10 |
11 |
12 | ## Expected Behavior
13 |
14 |
15 | ## Possible Solution
16 |
17 |
18 | ## Additional Context/Screenshots
19 |
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | ---
5 | ## Description Of Potential Feature
6 |
7 |
8 | ## Additional Issues This May/Does Solve
9 |
10 |
11 | ## Additional Context
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 | ## Important Code
4 |
5 | ## Additional Information
6 |
7 | ## Relevant Issues
8 |
9 |
12 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # For for information on config options, visit docs:
2 | # https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates
3 | version: 2
4 | updates:
5 | - package-ecosystem: "npm"
6 | directory: "/"
7 | schedule:
8 | interval: "daily"
9 | ignore:
10 | # TODO: remove this as part of https://github.com/prescottprue/fireadmin/issues/132
11 | - dependency-name: "react-dnd-html5-backend"
12 | - dependency-name: "react-dropzone"
13 | - dependency-name: "react-dnd"
14 | - package-ecosystem: "npm"
15 | directory: "/docs"
16 | schedule:
17 | interval: "weekly"
18 | - package-ecosystem: "npm"
19 | directory: "/functions"
20 | schedule:
21 | interval: "daily"
22 | - package-ecosystem: "github-actions"
23 | directory: "/"
24 | schedule:
25 | interval: "weekly"
26 |
--------------------------------------------------------------------------------
/.github/workflows/docs-verify.yml:
--------------------------------------------------------------------------------
1 | name: Verify Docs
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - 'docs/**'
7 | - '.github/workflows/docs-deploy.yml'
8 |
9 | jobs:
10 | verify-build:
11 | name: Verify + Build
12 | runs-on: ubuntu-latest
13 | timeout-minutes: 15
14 | steps:
15 | - name: Cancel Previous Runs
16 | uses: styfle/cancel-workflow-action@0.9.0
17 | with:
18 | access_token: ${{ github.token }}
19 |
20 | - name: Checkout Repo
21 | uses: actions/checkout@v2
22 |
23 | - name: Setup Node
24 | uses: actions/setup-node@v2.2.0
25 | with:
26 | node-version: 14
27 |
28 | - name: Get yarn cache
29 | id: yarn-cache
30 | run: echo "::set-output name=dir::$(yarn cache dir)"
31 |
32 | - name: Cache Dependencies
33 | uses: actions/cache@v2.1.6
34 | with:
35 | path: ${{ steps.yarn-cache.outputs.dir }}
36 | key: ${{ runner.os }}-docs-${{ hashFiles('**/yarn.lock') }}
37 |
38 | # Removed due to causing failed builds
39 | # - name: Cache Gatsby Build
40 | # uses: actions/cache@v2.1.6
41 | # with:
42 | # path: docs/.cache
43 | # key: ${{ runner.os }}-docs-build
44 |
45 | - name: Install Dependencies
46 | env:
47 | CYPRESS_INSTALL_BINARY: 0 # Skip installing of cypress
48 | run: |
49 | yarn --cwd docs install --frozen-lockfile
50 |
51 | - name: Verify
52 | run: |
53 | yarn --cwd docs lint
54 |
55 | - name: Build
56 | run: |
57 | yarn --cwd docs build
58 |
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS
2 | *.log*
3 | **/*.log
4 | .DS_Store
5 | **/.DS_Store
6 |
7 | # Dependencies
8 | **/node_modules
9 |
10 | # React App
11 | build
12 | .env.local
13 |
14 | # Functions
15 | functions/etc
16 | functions/dist
17 | functions/.runtimeconfig.json
18 | functions/coverage
19 |
20 | # Tests
21 | .nyc_output
22 | cypress/videos
23 | cypress/screenshots
24 | cypress/config.json
25 | cypress.env.json
26 | serviceAccount.json
27 |
28 | # Docs
29 | docs/public
30 | docs/.gatsby-context.js
31 | docs/.cache/
32 | .intermediate-representation/
33 |
34 |
--------------------------------------------------------------------------------
/.size-limit.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | {
3 | path: "build/static/js/main.*.chunk.js",
4 | limit: "30kb",
5 | name: "Main bundle",
6 | webpack: true,
7 | gzip: false
8 | },
9 | {
10 | path: "build/static/js/runtime~main.*.js",
11 | limit: "3 kb",
12 | name: "Vendor bundle",
13 | gzip: false,
14 | webpack: true
15 | }
16 | ]
--------------------------------------------------------------------------------
/.yo-rc.json:
--------------------------------------------------------------------------------
1 | {
2 | "generator-react-firebase": {
3 | "promptValues": {
4 | "githubUser": "prescottprue",
5 | "firebaseName": "fireadmin-stage",
6 | "firebaseKey": "",
7 | "includeRedux": true,
8 | "includeFirestore": true,
9 | "otherFeatures": [
10 | "Continuous Integration config",
11 | "Firebase Cloud Messaging",
12 | "Firebase Functions (with ESNext support)",
13 | "Tests for UI (Cypress)",
14 | "Error Tracking (Sentry.io)",
15 | "Client Side Error Reporting (Stackdriver)"
16 | ],
17 | "messagingSenderId": "",
18 | "ciProvider": "gitlab",
19 | "deployTo": "firebase",
20 | "appId": "1:823357791673:web:e53b561c7e36427fe06a68"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:10
2 | ARG FIREBASE_PROJECT
3 | ENV FIREBASE_PROJECT=$FIREBASE_PROJECT
4 | ARG NODE_ENV=production
5 | ENV NODE_ENV=$NODE_ENV
6 | ARG PORT=8080
7 | ENV PORT=$PORT
8 |
9 | # Expose Port 8080 to be used later for firebase-serve
10 | EXPOSE $PORT
11 |
12 | # Install yarn
13 | RUN curl -o- -L https://yarnpkg.com/install.sh | bash
14 |
15 | ## Copy package/lock files
16 | COPY package.json yarn.lock ./
17 |
18 | # Copy app source
19 | COPY . .
20 |
21 | # Install dependencies (skipping install of Cypress binary)
22 | RUN CYPRESS_INSTALL_BINARY=0 yarn --pure-lockfile --no-cache
23 |
24 | ## Build app bundle and index.html
25 | RUN yarn build
26 |
27 | # Run http-server so exit signals such as SIGTERM and SIGINT are recieved by
28 | # node process instead of being swallowed by npm
29 | ENTRYPOINT [ "firebase", "serve", "--only", "hosting:app", "-p", $PORT ]
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Prescott Prue
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/bin/generate-firebase-sdk-config.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const fsp = require('fs').promises
4 | const firebase = require('firebase-tools')
5 | const argv = require('minimist')(process.argv.slice(2))
6 |
7 | const project = process.env.GCLOUD_PROJECT
8 |
9 | if (!project) {
10 | console.error('Missing GCLOUD_PROJECT environment variable.')
11 | process.exit(1)
12 | }
13 |
14 | async function getFirebaseConfig() {
15 | const apps = await firebase.apps.list('WEB', {
16 | project
17 | })
18 | // NOTE: find is used because displayName is not a supported config option in firebase-tools
19 | const { appId, platform } = argv.appName
20 | ? apps.find((appConfig) => appConfig.displayName === argv.appName) || {}
21 | : apps[0]
22 |
23 | if (!appId) {
24 | throw new Error('No app found matching the provided app name')
25 | }
26 |
27 | const { sdkConfig } = await firebase.apps.sdkconfig(platform, appId)
28 | return sdkConfig
29 | }
30 |
31 | async function run() {
32 | const sdkConfig = await getFirebaseConfig()
33 | const content = JSON.stringify({ firebase: sdkConfig }, null, 2)
34 |
35 | if (argv.file) {
36 | await fsp.writeFile(argv.file, content)
37 | console.log(`Successfully written Firebase SDK config to ${argv.file}`)
38 | }
39 | if (argv.outputEnv) {
40 | const envString = Object.entries(sdkConfig)
41 | .map(([key, val]) => `REACT_APP_FIREBASE_${key}=${val}`)
42 | .join('\n')
43 | console.log(`\n${envString}`)
44 | } else {
45 | console.log(content)
46 | }
47 | }
48 |
49 | run().catch((err) => {
50 | console.error(err)
51 | process.exit(1)
52 | })
53 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "projectId": "6my6ku",
3 | "chromeWebSecurity": false,
4 | "defaultCommandTimeout": 9000,
5 | "retries": 1
6 | }
7 |
--------------------------------------------------------------------------------
/cypress/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: '../.eslintrc.js',
3 | env: {
4 | mocha: true,
5 | 'cypress/globals': true
6 | },
7 | plugins: ['cypress', 'chai-friendly'],
8 | rules: {
9 | 'no-console': 0,
10 | 'no-unused-expressions': 0,
11 | 'chai-friendly/no-unused-expressions': 2
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/cypress/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM cypress/base:10
2 |
3 | ## Build Arguments
4 | # URL which Cypress will run tests against (with default)
5 | ARG test_url=https://fireadmin-stage.firebaseapp.com
6 | # Arguments to add to the cypress run command
7 | ARG test_command_args=""
8 | # Image Build Id (from Cloud Build) used in recording of test files
9 | ARG build_id
10 |
11 | ## Environment Variables
12 | # URL which Cypress will run tests against (defaults to test_url arg)
13 | ENV CYPRESS_baseUrl=$test_url
14 | # Arguments to add to the cypress run command
15 | ENV TEST_ARGS "${test_command_args}"
16 |
17 | # Token used to auth firebase-tools for use in seeding/checking Firebase (RTDB + Firestore)
18 | ENV FIREBASE_TOKEN $FIREBASE_TOKEN
19 | # Token used to auth firebase-tools for use in seeding/checking Firebase (RTDB + Firestore)
20 | ENV CYPRESS_KEY $CYPRESS_KEY
21 | # Image Build Id (from Cloud Build) used in recording of test files
22 | ENV BUILD_ID=$build_id
23 |
24 | # Prevent a large number messages during NPM install
25 | ENV npm_config_loglevel warn
26 | # Color logs where possible
27 | ENV TERM xterm
28 |
29 | ## Copy code into container
30 | ### Files
31 | # A wildcard is used to ensure both package.json AND package-lock.json are copied
32 | # where available (npm@5+)
33 | COPY package*.json cypress.json serviceAccount.json yarn.lock ./
34 |
35 | ### Directories
36 | COPY src/ /src/
37 | COPY cypress/ /cypress/
38 |
39 | # Install Dependencies (only those used to build config files)
40 | RUN CYPRESS_INSTALL_BINARY=0 npm install cypress-firebase
41 |
42 | # Install Cypress
43 | # Set CI=true to prevent a large number of messages during install of
44 | # dependencies such as Cypress
45 | RUN CI=true npm install cypress
46 |
47 | # Verify Cypress Installed correctly
48 | RUN $(npm bin)/cypress verify
49 |
50 | # Run Cypress tests (URL set by ENV above) reporting results to Cypress
51 | ENTRYPOINT npm run test:ui:stage -- --record --key $CYPRESS_KEY$TEST_ARGS
52 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
--------------------------------------------------------------------------------
/cypress/fixtures/fakeEnvironment.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fake env",
3 | "databaseURL": "https://testing.firebaseio.com",
4 | "projectId": "test-project",
5 | "serviceAccount": {
6 | "credential": "asdf",
7 | "fullPath": "adsf"
8 | }
9 | }
--------------------------------------------------------------------------------
/cypress/fixtures/fakeEvent.json:
--------------------------------------------------------------------------------
1 | {
2 | "createdByType": "user",
3 | "eventData": {
4 | "id": "asdf",
5 | "projectId": "asdf",
6 | "databaseURL": "",
7 | "description": "",
8 | "serviceAccount": {
9 | "credential": "asdf",
10 | "fullPath": "asdf/asdf/asdf.json"
11 | }
12 | },
13 | "eventType": "updateEnvironment"
14 | }
--------------------------------------------------------------------------------
/cypress/fixtures/fakeProject.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-project",
3 | "roles": {
4 | "owner": {
5 | "name": "Owner",
6 | "permissions": {
7 | "read": {
8 | "environments": true,
9 | "members": true,
10 | "permissions": true,
11 | "roles": true
12 | },
13 | "update": {
14 | "environments": true,
15 | "members": true,
16 | "permissions": true,
17 | "roles": true
18 | },
19 | "delete": {
20 | "environments": true,
21 | "members": true,
22 | "permissions": true,
23 | "roles": true
24 | },
25 | "create": {
26 | "environments": true,
27 | "members": true,
28 | "permissions": true,
29 | "roles": true
30 | }
31 | }
32 | },
33 | "editor": {
34 | "name": "Editor",
35 | "permissions": {
36 | "read": { "environments": true },
37 | "update": { "environments": true },
38 | "create": { "environments": true }
39 | }
40 | },
41 | "viewer": {
42 | "permissions": { "read": { "environments": true } }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/cypress/fixtures/fakeServiceAccount.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "service_account",
3 | "project_id": "asdf",
4 | "private_key_id": "asdf",
5 | "private_key": "asdf",
6 | "client_email": "asdf",
7 | "client_id": "sadf",
8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth",
9 | "token_uri": "https://accounts.google.com/o/oauth2/token",
10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11 | "client_x509_cert_url": "asdf"
12 | }
13 |
--------------------------------------------------------------------------------
/cypress/integration/Docs.spec.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from '../utils'
2 |
3 | describe('Docs', () => {
4 | beforeEach(() => {
5 | cy.visit('/')
6 | })
7 |
8 | it('Link to docs exists in Navbar of homepage', () => {
9 | cy.get(createSelector('docs-button')).should('exist')
10 | })
11 | })
12 |
--------------------------------------------------------------------------------
/cypress/integration/HomePage.spec.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from '../utils'
2 |
3 | describe('Home', () => {
4 | beforeEach(() => {
5 | cy.logout()
6 | cy.visit('/')
7 | })
8 |
9 | it('Shows features', () => {
10 | cy.get(createSelector('features')).should('exist')
11 | })
12 |
13 | it('Has link to login page', () => {
14 | cy.get(createSelector('sign-in')).click()
15 | cy.url().should('include', '/login')
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/cypress/integration/LoginPage.spec.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from '../utils'
2 |
3 | describe('Login Page', () => {
4 | beforeEach(() => {
5 | cy.logout()
6 | cy.visit('/login')
7 | })
8 |
9 | it('Shows Login Through Google Button', () => {
10 | cy.url().should('include', '/login')
11 | cy.get(createSelector('google-auth-button')).should('exist')
12 | })
13 | })
14 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 | const cypressFirebasePlugin = require('cypress-firebase').plugin
11 | const admin = require('firebase-admin')
12 |
13 | module.exports = (on, config) => {
14 | // `on` is used to hook into various events Cypress emits
15 | // `config` is the resolved Cypress config
16 | require('cypress-log-to-output').install(on, (type, event) => {
17 | // return true or false from this plugin to control if the event is logged
18 | // `type` is either `console` or `browser`
19 | // if `type` is `browser`, `event` is an object of the type `LogEntry`:
20 | // https://chromedevtools.github.io/devtools-protocol/tot/Log#type-LogEntry
21 | // if `type` is `console`, `event` is an object of the type passed to `Runtime.consoleAPICalled`:
22 | // https://chromedevtools.github.io/devtools-protocol/tot/Runtime#event-consoleAPICalled
23 |
24 | // if (event.level === 'error' || event.type === 'error') {
25 | // return true
26 | // }
27 |
28 | return true
29 | })
30 | // Extends with config from .firebaserc
31 | return cypressFirebasePlugin(on, config, admin)
32 | }
33 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 | import 'cypress-wait-until'
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
22 | Cypress.on('uncaught:exception', (err, runnable) => {
23 | console.log('Error in uncaught exception:', err.message)
24 | // returning false here prevents Cypress from
25 | // failing the test
26 | return false
27 | })
28 |
--------------------------------------------------------------------------------
/cypress/utils/commands.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Converts Cypress fixtures, including JSON, to a Blob. All file types are
3 | * converted to base64 then converted to a Blob using Cypress
4 | * expect application/json. Json files are just stringified then converted to
5 | * a blob (prevents issues with invalid string decoding).
6 | * @param {String} fileUrl - The file url to upload
7 | * @param {String} type - content type of the uploaded file
8 | * @returns {Promise} Resolves with blob containing fixture contents
9 | * @example
10 | * createSelector('some-btn')
11 | * // => [data-test=some-btn]
12 | */
13 | export function getFixtureBlob(fileUrl, type) {
14 | console.log('fixture blob', fileUrl)
15 | return type === 'application/json'
16 | ? cy
17 | .fixture(fileUrl)
18 | .then(JSON.stringify)
19 | .then((jsonStr) => new Blob([jsonStr], { type: 'application/json' }))
20 | : cy
21 | .fixture(fileUrl, 'base64')
22 | .then((item) => Cypress.Blob.base64StringToBlob(item, type))
23 | }
24 |
--------------------------------------------------------------------------------
/cypress/utils/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Create selector for data-test attribute value
3 | * @param {String} selectorValue - Value of selector
4 | * @return {String} String containing selector and value
5 | */
6 | export function createSelector(selectorValue) {
7 | return `[data-test=${selectorValue}]`
8 | }
9 |
10 | /**
11 | * Create selector for data-test-id attribute value
12 | * @param {String} selectorValue - Value of selector
13 | * @return {String} String containing selector and value
14 | */
15 | export function createIdSelector(selectorValue) {
16 | return `[data-test-id=${selectorValue}]`
17 | }
18 |
19 | /**
20 | * Create selector for data-test-value attribute value
21 | * @param {String} selectorValue - Value of selector
22 | * @return {String} String containing selector and value
23 | */
24 | export function createValueSelector(selectorValue) {
25 | return `[data-test-value=${selectorValue}]`
26 | }
27 |
--------------------------------------------------------------------------------
/docs/.env.development:
--------------------------------------------------------------------------------
1 | FIREADMIN_URL=http://localhost:8080
--------------------------------------------------------------------------------
/docs/.eslintignore:
--------------------------------------------------------------------------------
1 | coverage/**
2 | **/node_modules/**
3 | dist/**
4 | src/index.html
5 | blueprints/**
6 | src/config.js
7 | .cache/**
8 | public/**
--------------------------------------------------------------------------------
/docs/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // TODO: Switch back to "extends: '../.eslintrc.js'" once react-app is removed from top level
2 | module.exports = {
3 | extends: ['prettier'],
4 | root: true,
5 | parser: 'babel-eslint',
6 | plugins: ['import', 'babel', 'react', 'react-hooks', 'prettier'],
7 | settings: {
8 | react: {
9 | version: '17.0'
10 | },
11 | 'import/resolver': {
12 | node: {
13 | moduleDirectory: ['node_modules', '/']
14 | }
15 | }
16 | },
17 | rules: {
18 | semi: [2, 'never'],
19 | 'no-console': 'error',
20 | 'react/forbid-prop-types': 0,
21 | 'react/require-default-props': 0,
22 | 'react/jsx-filename-extension': 0,
23 | 'import/no-named-as-default': 0,
24 | 'no-return-await': 2,
25 | 'react-hooks/rules-of-hooks': 'error',
26 | 'react-hooks/exhaustive-deps': 'warn',
27 | 'prettier/prettier': [
28 | 'error',
29 | {
30 | singleQuote: true,
31 | trailingComma: 'none',
32 | semi: false,
33 | bracketSpacing: true,
34 | jsxBracketSameLine: true,
35 | printWidth: 80,
36 | tabWidth: 2,
37 | useTabs: false
38 | }
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Fireadmin Docs
2 |
3 | > Fireadmin documentation content and application (markdown converted into static HTML using Gatsby)
4 |
5 | ## Getting Started
6 |
7 | 1. Make sure you set node version to at least `8.12.0` (`nvm use 8.12.0`)
8 | 1. Install deps: `yarn install`
9 | 1. Start dev server: `yarn start`
10 |
11 | ## Dev Commands
12 |
13 | ```sh
14 | # lint code
15 | yarn lint
16 |
17 | # auto-fix lint and format
18 | yarn lint:fix
19 |
20 | # generate static build
21 | yarn build
22 | ```
23 |
24 | **:warning: Add `--prefix-paths` if you are using path prefix!**
25 |
26 | ## Changing Docs
27 |
28 | All docs content lives in the `content` folder.
29 |
30 | ### Updating Docs
31 |
32 | 1. Go into the `content` folder and look for the markdown file associated with the doc (structure is a mirror of what is in UI)
33 | 1. Change markdown file and save
34 | 1. Run locally using `yarn start` and visiting `localhost:8000` to confirm the looks of your changes
35 | 1. Make a PR to this Repo and assign it to another engineer
36 |
37 | ### Adding A Doc
38 |
39 | 1. Place a new `.md` file within the `content` folder (make sure to place it in the relevant location)
40 | 1. Place config similar at the top of the document (where `slug` is the intended path to the doc):
41 |
42 | ```md
43 | ---
44 | title: Adding To These Docs
45 | slug: docs/adding-to-these-docs
46 | type: page
47 | language: en
48 | tags:
49 | - docs
50 | ---
51 | ```
52 | 1. Reboot your local dev server if it was already running (`yarn start` again)
53 | 1. Confirm that your doc appears in the sidebar (under any parent if provided in slug)
54 |
55 | **Note:** If you are making a new folder, make sure to have a `README.md` in the base of the folder with `slug: <- folder ->` like (` slug: docs`) or it will not appear in the sidebar.
56 |
--------------------------------------------------------------------------------
/docs/content/docs/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Documentation
3 | slug: docs
4 | type: page
5 | language: en
6 | order: 4
7 | tags:
8 | - docs
9 | - react
10 | ---
11 |
12 | Pretty funny to have docs for the docs huh?
--------------------------------------------------------------------------------
/docs/content/guides/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Guides
3 | slug: guides
4 | type: page
5 | language: en
6 | order: 1
7 | tags:
8 | - guides
9 | - getting-started
10 | - environments
11 | - projects
12 | - actions
13 | ---
14 |
15 | The Fireadmin guides are step-by-step walkthroughs that help you get started using Fireadmin.
16 |
17 | * [Initial Setup](/guides/initial-setup)
18 |
--------------------------------------------------------------------------------
/docs/content/guides/custom-action-template.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Custom Action Templates
3 | slug: guides/custom-action-templates
4 | type: page
5 | language: en
6 | order: 2
7 | tags:
8 | - guides
9 | - getting-started
10 | - environments
11 | - projects
12 | - actions
13 | ---
14 |
15 | ## Creating An Action Template
16 |
17 | 1. Click **Create Action Template** from project actions page
18 | 1. Now fill in information about the template — if you would like others to be able to use the template you are creating you can mark it as public.
19 | 1. Add an environment — Name it Source
20 | 1. Add another environment — Name it Dest
21 | 1. Add an input — mark it as type
22 | 1. Give the input a name — collectionName
23 | 1. Add a step — set its type to Copy, the path type to User Input (uses user provided input as value) and select collectionName
24 | 1. Finish up by clicking Save
25 |
26 | 
--------------------------------------------------------------------------------
/docs/content/source/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Source Code
3 | slug: source
4 | type: page
5 | language: en
6 | order: 2
7 | tags:
8 | - source
9 | - pattern
10 | ---
11 |
12 |
--------------------------------------------------------------------------------
/docs/content/source/adding-a-feature.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Adding Features
3 | slug: source/adding-a-feature
4 | type: page
5 | language: en
6 | tags:
7 | - source
8 | ---
9 |
10 | Adding features is commonly done using some sort of scaffolding in tool, in our case we are using [the subgenerators from generator-react-firebase](https://github.com/prescottprue/generator-react-firebase#sub-generators) to add new features.
11 |
12 | ## React Components
13 |
14 | ### Top Level
15 |
16 | Create a new component name "SomeComponent" by running:
17 |
18 | ```bash
19 | yo react-firebase:component SomeComponent
20 | ```
21 |
22 | ### Nested
23 |
24 | Create a new component name "SomeComponent" in `routes/Transaction/routes/Other/components` by running:
25 |
26 | ```bash
27 | yo react-firebase:component SomeComponent routes/Transaction/routes/Other
28 | ```
29 |
30 | ## Cloud Functions
31 |
32 | Create a new cloud function "someFunc" by running:
33 |
34 | ```bash
35 | yo react-firebase:function someFunc
36 | ```
37 |
38 | It creates
39 |
40 | ```txt
41 | functions/someFunc/index.js
42 | functions/test/someFunc/index.spec.js
43 | ```
44 |
--------------------------------------------------------------------------------
/docs/content/testing/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Testing
3 | slug: testing
4 | type: page
5 | language: en
6 | order: 3
7 | tags:
8 | - testing
9 | ---
10 |
11 | # Testing
12 |
13 | The types of testing is often thought of as a spectrum. We will be covering the two ends of that spectrum, End To End Testing and Unit Testing
14 |
15 | Horchata tests confirm that providing a request (db update or external request) - database and external services are correctly updated
16 |
17 | ## UI Tests (E2E)
18 | UI tests confirm that they correctly write to DB (Cypress + `callFirestore` and `callRTDB`)
19 |
20 | * [Writing E2E UI Tests](/testing/writing/ui)
21 |
22 | ## Code Tests (Unit)
23 |
24 | Checking that specific code functions as expected. For example - providing a request (db update or external request) to a cloud function then confirming database and external services are correctly updated
25 |
26 | * [Writing Unit Tests](/testing/writing/unit)
27 | * [Cloud Functions Testing](/testing/cloud-functions)
28 |
--------------------------------------------------------------------------------
/docs/content/testing/cloud-functions-tests.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Cloud Functions
3 | slug: testing/cloud-functions
4 | type: page
5 | order: 1
6 | language: en
7 | tags:
8 | - testing
9 | ---
10 |
11 | Tests for Cloud functions are written using Mocha/Chai. They [live within the `functions/test` folder](https://github.com/prescottprue/fireadmin/tree/master/functions/test).
12 |
13 | ## Running Locally
14 |
15 | Unit tests are run through mocha when calling:
16 |
17 | ```bash
18 | yarn functions:test
19 | ```
20 |
21 | **Note:** Coverage is genearted when running:
22 |
23 | ```bash
24 | yarn functions:test:cov
25 | ```
26 |
27 | ## Writing
28 |
29 | How you write your tests for cloud functions depends on the scope you would like to cover in your test.
30 | [the vanilla js testing section](/images/FiradminLogo.png)
31 |
32 | ### Logic
33 |
34 | As mentioned in [the vanilla js testing section](/testing/vanilla-js), we can tests simple logic directly with mocha/chai.
35 |
36 | ### Full Cloud Function
37 |
38 | Testing is slightly different between [HTTP functions](https://firebase.google.com/docs/functions/unit-testing#testing_http_functions) and [other function types](https://firebase.google.com/docs/functions/unit-testing#testing_background_non-http_functions)
39 |
40 | Everything in tests can be mocked including Database updates and external API calls
41 |
42 | - [PubSub Function Test Example](https://github.com/prescottprue/fireadmin/blob/master/functions/test/unit/sendFcm.spec.js)
43 | - [RTDB Function Test Example](https://github.com/prescottprue/fireadmin/blob/master/functions/test/unit/callGoogleApi.spec.js) (has external API calls which use HTTP mocked with [`fauxJax`][faux-jax-url])
44 | - [HTTP Function Test Example](https://github.com/prescottprue/fireadmin/blob/master/functions/test/unit/version.spec.js)
45 |
46 | ## Other Resources
47 |
48 | - [Unit testing of Cloud Functions | Firebase](https://firebase.google.com/docs/functions/unit-testing)
49 | - [faux-jax][faux-jax-url] for mocking http
50 |
51 | [faux-jax-url]: https://github.com/algolia/faux-jax
52 |
--------------------------------------------------------------------------------
/docs/data/siteConfig.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | siteTitle: 'fireadmin-docs',
3 | siteDescription: 'Fireadmin documentation.',
4 | authorName: 'Prescott Prue',
5 | authorAvatar: '/images/avatar.png',
6 | authorDescription:
7 | 'Mechanical/Aerospace engineer turned fullstack javascript developer. Author of react-redux-firebase, redux-firestore and other tools.',
8 | siteUrl: 'https://docs.fireadmin.io',
9 | // Prefixes all links
10 | pathPrefix: '/docs', // Note: it must *not* have a trailing slash.
11 | siteCover: '/images/cover.jpg',
12 | googleAnalyticsId: 'G-Y3XG94BTJX',
13 | background_color: '#ffffff',
14 | theme_color: '#ef6c00',
15 | icon: 'src/assets/FireadminLogo.png'
16 | }
17 |
--------------------------------------------------------------------------------
/docs/src/assets/FireadminLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prescottprue/fireadmin/7d49cb768a77078608c4f21d219cea2b447dfd1f/docs/src/assets/FireadminLogo.png
--------------------------------------------------------------------------------
/docs/src/assets/gatsby-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prescottprue/fireadmin/7d49cb768a77078608c4f21d219cea2b447dfd1f/docs/src/assets/gatsby-icon.png
--------------------------------------------------------------------------------
/docs/src/components/Article/Article.css:
--------------------------------------------------------------------------------
1 | .article {
2 | padding: 0 30px 30px;
3 | }
4 |
5 | @media only screen and (max-width: 500px) {
6 | .article {
7 | padding: 0;
8 | }
9 | }
10 |
11 | .article-footer {
12 | position: relative;
13 | margin: 6rem 0 0;
14 | padding: 3rem 0 0;
15 | border-top: 1px solid #ebf2f6;
16 | }
17 |
--------------------------------------------------------------------------------
/docs/src/components/Article/Article.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Bio from '../Bio/Bio'
4 | import Content from '../Content/Content'
5 | import './Article.css'
6 |
7 | function Article({ post }) {
8 | return (
9 |
10 |
15 |
18 |
19 | )
20 | }
21 |
22 | Article.propTypes = {
23 | post: PropTypes.object
24 | }
25 |
26 | export default Article
27 |
--------------------------------------------------------------------------------
/docs/src/components/Bio/Bio.css:
--------------------------------------------------------------------------------
1 | .author-image {
2 | -webkit-box-sizing: border-box;
3 | -moz-box-sizing: border-box;
4 | box-sizing: border-box;
5 | display: block;
6 | position: absolute;
7 | top: -40px;
8 | left: 50%;
9 | margin-left: -40px;
10 | width: 80px;
11 | height: 80px;
12 | border-radius: 100%;
13 | overflow: hidden;
14 | padding: 6px;
15 | background: #fff;
16 | z-index: 2;
17 | box-shadow: #e7eef2 0 0 0 1px;
18 | }
19 |
20 | .author-image .img {
21 | position: relative;
22 | display: block;
23 | width: 100%;
24 | height: 100%;
25 | background-size: cover;
26 | background-position: center center;
27 | border-radius: 100%;
28 | }
29 |
30 | .author-profile .author-image {
31 | position: relative;
32 | left: auto;
33 | top: auto;
34 | width: 120px;
35 | height: 120px;
36 | padding: 3px;
37 | margin: -100px auto 0 auto;
38 | box-shadow: none;
39 | }
40 |
41 | .post-footer {
42 | position: relative;
43 | margin: 6rem 0 0;
44 | padding: 3rem 0 0;
45 | border-top: #ebf2f6 1px solid;
46 | }
47 |
48 | .bio-text {
49 | padding-top: 1em;
50 | line-height: 1.4em;
51 | }
52 |
53 | .bio-text a {
54 | border-bottom: 1px dotted rgba(162,162,162,0.8);
55 | }
56 | .bio-text a:hover {
57 | border-bottom-style: solid;
58 | }
59 |
60 | @media only screen and (max-width: 500px) {
61 | .post-footer .author-image {
62 | top: -60px;
63 | }
64 |
65 | .author-profile .author-image {
66 | margin-top: -70px;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/docs/src/components/Bio/Bio.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import { withPrefix } from 'gatsby'
3 | import siteConfig from '../../../data/siteConfig'
4 | import './Bio.css'
5 |
6 | function Bio() {
7 | const prefixedImg = withPrefix(siteConfig.authorAvatar)
8 | return (
9 |
10 |
11 |
15 |
19 |
20 |
21 |
22 | About the author
23 |
27 |
28 |
29 | )
30 | }
31 |
32 | export default Bio
33 |
--------------------------------------------------------------------------------
/docs/src/components/Content/Content.css:
--------------------------------------------------------------------------------
1 | .content {
2 | line-height: 1.6;
3 | }
4 |
5 | .content > h2 {
6 | padding-top: 3rem;
7 | margin-top: 3rem;
8 | border-top: 1px solid #ececec;
9 | }
10 |
11 | .content > h3 {
12 | padding-top: 3rem;
13 | }
14 |
15 | .content > p {
16 | margin: 1em 0 0 0;
17 | }
18 |
19 | .content-alert {
20 | padding: 15px;
21 | margin-bottom: 20px;
22 | border-radius: 3px;
23 | color: #31708f;
24 | background-color: #d9edf7;
25 | }
26 |
27 | .content p > a,
28 | .content li > a {
29 | border-bottom: 1px dotted rgba(162,162,162,0.8);
30 | }
31 |
32 | .content p > a:hover,
33 | .content li > a:hover {
34 | border-bottom-style: solid;
35 | }
36 |
37 | .gatsby-resp-image-link {
38 | border-bottom: none !important;
39 | }
40 |
41 | .content > blockquote {
42 | box-sizing: border-box;
43 | margin: 1.75em 0 1.75em -2.2em;
44 | padding: 0 0 0 1.75em;
45 | border-left: .4em solid rgba(32,35,42,0.85);
46 | }
47 |
48 | .content > blockquote p {
49 | margin: .8em 0;
50 | font-style: italic;
51 | }
--------------------------------------------------------------------------------
/docs/src/components/Content/Content.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import ContentHeader from '../ContentHeader/ContentHeader'
4 | import './Content.css'
5 | import './GatsbyHighlight.css'
6 |
7 | function Content({ content, date, tags }) {
8 | return (
9 |
10 | {(tags || date) && }
11 |
12 |
13 | )
14 | }
15 |
16 | Content.propTypes = {
17 | content: PropTypes.string,
18 | date: PropTypes.object,
19 | tags: PropTypes.array
20 | }
21 |
22 | export default Content
23 |
--------------------------------------------------------------------------------
/docs/src/components/Content/GatsbyHighlight.css:
--------------------------------------------------------------------------------
1 | .gatsby-highlight {
2 | border-radius: 8px;
3 | font-size: 15px;
4 | line-height: 1.7;
5 | background: #2d2d2d;
6 | color: #ffffff;
7 | border-radius: 10px;
8 | overflow: auto;
9 | tab-size: 1.5em;
10 | margin: 1.5em 0em 1.5em 0;
11 | padding: 1em;
12 | }
13 |
14 | .gatsby-highlight > pre {
15 | border: 0;
16 | margin: 0;
17 | padding: 0;
18 | }
--------------------------------------------------------------------------------
/docs/src/components/ContentHeader/ContentHeader.css:
--------------------------------------------------------------------------------
1 | .ContentHeader {
2 | margin-bottom: 2rem;
3 | }
4 |
5 | .ContentHeader-time {
6 | color: #7f7e7e;
7 | }
8 |
9 | .ContentHeader-in {
10 | color: #7f7e7e;
11 | }
--------------------------------------------------------------------------------
/docs/src/components/ContentHeader/ContentHeader.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import PropTypes from 'prop-types'
3 | import TagList from '../TagList/TagList'
4 | import './ContentHeader.css'
5 |
6 | function ContentHeader({ date, tags }) {
7 | return (
8 |
9 | {date && }
10 | {Array.isArray(tags) && tags.length > 0 && (
11 |
12 | in
13 |
14 |
15 | )}
16 |
17 | )
18 | }
19 |
20 | ContentHeader.propTypes = {
21 | date: PropTypes.object,
22 | tags: PropTypes.array
23 | }
24 |
25 | export default ContentHeader
26 |
--------------------------------------------------------------------------------
/docs/src/components/Hero/Hero.css:
--------------------------------------------------------------------------------
1 | .hero {
2 | position: relative;
3 | display: table;
4 | /* margin-top: 60px; */
5 | width: 100%;
6 | height: 400px;
7 | overflow: hidden;
8 | background-repeat: no-repeat;
9 | background-position: center;
10 | background-size: cover;
11 | }
12 |
13 | .hero-title {
14 | display: table-cell;
15 | vertical-align: middle;
16 | text-align: center;
17 | width: 100%;
18 | }
19 |
20 | .hero-title > h1 {
21 | font-weight: 700;
22 | font-size: 3rem;
23 | margin: 10px 60px;
24 | color: #fff;
25 | text-shadow: 1px 1px 4px rgba(34,34,34,0.6);
26 | }
--------------------------------------------------------------------------------
/docs/src/components/Hero/Hero.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { withPrefix } from 'gatsby'
4 | import siteConfig from '../../../data/siteConfig'
5 | import './Hero.css'
6 |
7 | function Hero({ heroImg, title }) {
8 | const imgUrl = heroImg || withPrefix(siteConfig.siteCover)
9 |
10 | return (
11 |
12 |
13 |
{title}
14 |
15 |
16 | )
17 | }
18 |
19 | Hero.propTypes = {
20 | heroImg: PropTypes.string,
21 | title: PropTypes.string
22 | }
23 |
24 | export default Hero
25 |
--------------------------------------------------------------------------------
/docs/src/components/HomePage/HomePage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Typography from '@material-ui/core/Typography'
3 | import { makeStyles } from '@material-ui/core/styles'
4 | import styles from './HomePage.styles'
5 |
6 | const useStyles = makeStyles(styles)
7 |
8 | function HomePage() {
9 | const classes = useStyles()
10 |
11 | return (
12 |
13 |
14 |
15 | Fireadmin Documentation
16 |
17 |
18 | Covering everything from general usage and feature explanation to
19 | understanding the source code
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | export default HomePage
27 |
--------------------------------------------------------------------------------
/docs/src/components/HomePage/HomePage.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | display: 'flex',
4 | justifyContent: 'center'
5 | },
6 | words: {
7 | display: 'flex',
8 | flexDirection: 'column',
9 | alignItems: 'center'
10 | },
11 | body: {
12 | marginTop: '2rem'
13 | }
14 | })
15 |
--------------------------------------------------------------------------------
/docs/src/components/HomePage/index.js:
--------------------------------------------------------------------------------
1 | import HomePage from './HomePage'
2 |
3 | export default HomePage
4 |
--------------------------------------------------------------------------------
/docs/src/components/Loading/Loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import CircularProgress from '@material-ui/core/CircularProgress'
3 | import { makeStyles } from '@material-ui/core/styles'
4 | import styles from './Loading.styles'
5 |
6 | const useStyles = makeStyles(styles)
7 |
8 | function Loading() {
9 | const classes = useStyles()
10 |
11 | return (
12 |
13 |
14 | Loading...
15 |
16 | )
17 | }
18 |
19 | export default Loading
20 |
--------------------------------------------------------------------------------
/docs/src/components/Loading/Loading.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | display: 'flex',
4 | flexDirection: 'column',
5 | alignItems: 'center',
6 | marginTop: '4rem',
7 | width: '100%'
8 | },
9 | words: {
10 | marginTop: '2rem'
11 | }
12 | })
13 |
--------------------------------------------------------------------------------
/docs/src/components/Loading/index.js:
--------------------------------------------------------------------------------
1 | import Loading from './Loading'
2 |
3 | export default Loading
4 |
--------------------------------------------------------------------------------
/docs/src/components/PostsList/PostsList.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import PropTypes from 'prop-types'
3 | import PostsListItem from '../PostsListItem/PostsListItem'
4 |
5 | function PostsList({ posts }) {
6 | return (
7 |
8 | {posts &&
9 | posts.map((post) => {
10 | const itemProps = {
11 | title: post.node.frontmatter.title,
12 | excerpt: post.node.excerpt,
13 | slug: post.node.frontmatter.slug,
14 | date: post.node.frontmatter.date,
15 | language: post.node.frontmatter.language || 'fr',
16 | tags: post.node.frontmatter.tags || []
17 | }
18 | return
19 | })}
20 |
21 | )
22 | }
23 |
24 | PostsList.propTypes = {
25 | posts: PropTypes.array
26 | }
27 |
28 | export default PostsList
29 |
--------------------------------------------------------------------------------
/docs/src/components/PostsListItem/PostsListItem.css:
--------------------------------------------------------------------------------
1 | .post {
2 | border-bottom: 1px solid rgba(214,209,230,.5);
3 | padding-bottom: 1.25rem;
4 | }
5 |
6 | .post-title > a:hover {
7 | border-bottom: 1px dotted rgba(34,34,34,0.8);;
8 | }
9 |
10 | .post-header {
11 | padding: 1em 0;
12 | }
13 |
14 | .post-read {
15 | display: block;
16 | font-size: .75rem;
17 | margin-top: 1rem;
18 | text-align: center;
19 | text-decoration: none;
20 | text-transform: uppercase;
21 | letter-spacing: .05em;
22 | line-height: 2;
23 | }
24 |
25 | .post-date {
26 | color: #7f7e7e;
27 | }
28 |
29 | .post-excerpt {
30 | line-height: 1.45;
31 | padding-bottom: 0.5em;
32 | }
33 |
34 | .post-read:hover {
35 | background-color: rgba(32,35,42,0.85);
36 | border-radius: .25rem;
37 | color: #fff;
38 | }
--------------------------------------------------------------------------------
/docs/src/components/PostsListItem/PostsListItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'gatsby'
4 | import TagList from '../TagList/TagList'
5 | import './PostsListItem.css'
6 |
7 | function PostsListItem({ title, excerpt, slug, date, language, tags }) {
8 | return (
9 |
10 |
11 |
12 | {title}
13 |
14 |
15 |
18 |
28 |
29 | )
30 | }
31 |
32 | PostsListItem.propTypes = {
33 | title: PropTypes.string,
34 | excerpt: PropTypes.string,
35 | slug: PropTypes.string,
36 | date: PropTypes.string,
37 | language: PropTypes.string,
38 | tags: PropTypes.array
39 | }
40 |
41 | export default PostsListItem
42 |
--------------------------------------------------------------------------------
/docs/src/components/SEO/SEO.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Helmet from 'react-helmet'
3 | import PropTypes from 'prop-types'
4 | import { withPrefix } from 'gatsby'
5 | import siteConfig from '../../../data/siteConfig'
6 |
7 | function SEO(props) {
8 | const { isBlogPost, path = '', lang = 'en' } = props
9 | const title = props.title
10 | ? `${props.title} | ${siteConfig.siteTitle}`
11 | : siteConfig.siteTitle
12 | const imagePath = props.cover || withPrefix(siteConfig.siteCover)
13 | const formatedSiteUrl = siteConfig.siteUrl.substring(
14 | 0,
15 | siteConfig.siteUrl.length - 1
16 | )
17 | const image = `${formatedSiteUrl}${imagePath}`
18 | const description = props.description || siteConfig.siteDescription
19 |
20 | return (
21 |
22 | {/* General tags */}
23 |
24 |
25 |
26 | {/* OpenGraph tags */}
27 |
28 |
29 |
30 |
31 |
32 |
33 | {/* Twitter Card tags */}
34 |
35 |
36 |
37 |
38 |
39 |
40 | )
41 | }
42 |
43 | SEO.propTypes = {
44 | isBlogPost: PropTypes.bool,
45 | path: PropTypes.string,
46 | description: PropTypes.string,
47 | title: PropTypes.string,
48 | cover: PropTypes.string,
49 | lang: PropTypes.string
50 | }
51 |
52 | export default SEO
53 |
--------------------------------------------------------------------------------
/docs/src/components/SidebarItem/index.js:
--------------------------------------------------------------------------------
1 | import SidebarItem from './SidebarItem'
2 |
3 | export default SidebarItem
4 |
--------------------------------------------------------------------------------
/docs/src/components/TagList/TagList.css:
--------------------------------------------------------------------------------
1 | .tag-list {
2 | display: inline;
3 | margin: 0 .5rem 0 0;
4 | color: #7f7e7e;
5 | }
6 |
7 | .tag-list-item {
8 | margin-left: 0.3rem;
9 | color: #7f7e7e;
10 | }
11 |
12 | .tag-list-item:hover {
13 | border-bottom: 1px dotted #7f7e7e;
14 | }
15 |
16 | .tag-list-item::before {
17 | content: "#";
18 | }
--------------------------------------------------------------------------------
/docs/src/components/TagList/TagList.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'gatsby'
4 | import './TagList.css'
5 |
6 | function TagList({ tags, icon }) {
7 | return (
8 |
9 | {icon === true && 🏷 }
10 | {tags.map((tag, i) => {
11 | return (
12 |
13 |
14 | {tag}
15 |
16 | {i < tags.length - 1 ? ', ' : ''}
17 |
18 | )
19 | })}
20 |
21 | )
22 | }
23 |
24 | TagList.propTypes = {
25 | tags: PropTypes.array,
26 | icon: PropTypes.bool
27 | }
28 |
29 | export default TagList
30 |
--------------------------------------------------------------------------------
/docs/src/components/Wrapper/Wrapper.css:
--------------------------------------------------------------------------------
1 | .main {
2 | position: relative;
3 | width: 80%;
4 | max-width: 1170px;
5 | border-bottom: 1px solid #ebf2f6;
6 | word-wrap: break-word;
7 | background-color: #fff;
8 | margin: 0px auto 30px auto;
9 | top: 30px;
10 | padding: 50px;
11 | box-shadow: 0 0 0 0,0 6px 12px rgba(0,0,0,0.1);
12 | }
13 |
14 | @media (max-width: 780px) {
15 | .main {
16 | width: 90%;
17 | padding: 25px;
18 | }
19 | }
--------------------------------------------------------------------------------
/docs/src/components/Wrapper/Wrapper.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import './Wrapper.css'
4 |
5 | function Template({ children }) {
6 | return (
7 |
8 | {children}
9 |
10 | )
11 | }
12 |
13 | Template.propTypes = {
14 | children: PropTypes.object
15 | }
16 |
17 | export default Template
18 |
--------------------------------------------------------------------------------
/docs/src/components/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | body {
8 | font-family: "Lato", sans-serif;
9 | color: #222222cc;
10 | background-color: #e8e8e8;
11 | }
12 |
13 | img {
14 | max-width: 100%;
15 | height: auto;
16 | vertical-align: middle;
17 | border: 0;
18 | }
19 |
20 | a {
21 | text-decoration: none;
22 | color: rgba(34,34,34,0.8);
23 | }
24 |
25 | ul {
26 | padding-left: 2em;
27 | margin: 1em 0 0 0;
28 | }
--------------------------------------------------------------------------------
/docs/src/constants/paths.js:
--------------------------------------------------------------------------------
1 | // Set by .env.development in local
2 | export const FIREADMIN_URL = process.env.FIREADMIN_URL || 'https://fireadmin.io'
3 |
--------------------------------------------------------------------------------
/docs/src/html.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | function HTML(props) {
5 | return (
6 |
7 |
8 |
9 |
10 |
14 |
18 | {props.headComponents}
19 |
20 |
21 | {props.preBodyComponents}
22 |
27 | {props.postBodyComponents}
28 |
29 |
30 | )
31 | }
32 |
33 | HTML.propTypes = {
34 | htmlAttributes: PropTypes.object,
35 | headComponents: PropTypes.array,
36 | bodyAttributes: PropTypes.object,
37 | preBodyComponents: PropTypes.array,
38 | body: PropTypes.string,
39 | postBodyComponents: PropTypes.array
40 | }
41 |
42 | export default HTML
43 |
--------------------------------------------------------------------------------
/docs/src/pages/404.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Typography from '@material-ui/core/Typography'
4 | import { makeStyles } from '@material-ui/core/styles'
5 | import Layout from '../components/layout'
6 | import Wrapper from '../components/Wrapper/Wrapper'
7 | import SEO from '../components/SEO/SEO'
8 |
9 | const useStyles = makeStyles((theme) => ({
10 | main: {
11 | textAlign: 'center'
12 | },
13 | ghost: {
14 | lintHeight: 1.5,
15 | textAlign: 'center',
16 | fontSize: '7rem'
17 | }
18 | }))
19 |
20 | function NotFoundPage({ data, location }) {
21 | const classes = useStyles()
22 | return (
23 |
24 |
25 |
26 |
27 | 404 Page Not Found
28 |
29 |
30 | 👻
31 |
32 |
33 | Looks like you've followed a broken link or entered a URL that doesn't
34 | exist on this site.
35 |
36 |
37 |
38 | )
39 | }
40 |
41 | NotFoundPage.propTypes = {
42 | data: PropTypes.object,
43 | location: PropTypes.object
44 | }
45 |
46 | export default NotFoundPage
47 |
--------------------------------------------------------------------------------
/docs/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { graphql } from 'gatsby'
4 | import get from 'lodash/get'
5 | import Layout from '../components/layout'
6 | import Wrapper from '../components/Wrapper/Wrapper'
7 | import SEO from '../components/SEO/SEO'
8 | import HomePage from '../components/HomePage'
9 |
10 | function App({ data, location }) {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 |
21 | App.propTypes = {
22 | data: PropTypes.object,
23 | location: PropTypes.object.isRequired
24 | }
25 |
26 | export default App
27 |
28 | export const pageQuery = graphql`
29 | query {
30 | site {
31 | siteMetadata {
32 | title
33 | description
34 | }
35 | }
36 | allMarkdownRemark(
37 | sort: { fields: [frontmatter___order], order: DESC }
38 | filter: { frontmatter: { type: { ne: "post" } } } # only those without type: "post"
39 | ) {
40 | edges {
41 | node {
42 | excerpt
43 | frontmatter {
44 | title
45 | order
46 | tags
47 | language
48 | slug
49 | }
50 | }
51 | }
52 | }
53 | }
54 | `
55 |
--------------------------------------------------------------------------------
/docs/src/templates/page.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { graphql } from 'gatsby'
4 | import { get } from 'lodash'
5 | import Typography from '@material-ui/core/Typography'
6 | import Layout from '../components/layout'
7 | import Content from '../components/Content/Content'
8 | import Wrapper from '../components/Wrapper/Wrapper'
9 | import SEO from '../components/SEO/SEO'
10 |
11 | function Page({ location, data, ...rest }) {
12 | const page = get(data, 'markdownRemark')
13 | return (
14 |
15 |
21 |
26 | {page.frontmatter.title}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | )
35 | }
36 |
37 | Page.propTypes = {
38 | location: PropTypes.object.isRequired,
39 | data: PropTypes.object
40 | }
41 |
42 | export default Page
43 |
44 | export const pageQuery = graphql`
45 | query PageBySlug($slug: String!) {
46 | markdownRemark(frontmatter: { slug: { eq: $slug } }) {
47 | id
48 | excerpt
49 | html
50 | frontmatter {
51 | title
52 | slug
53 | }
54 | }
55 | allMarkdownRemark(
56 | sort: { fields: [frontmatter___order], order: ASC } # sort by order
57 | filter: { frontmatter: { type: { ne: "post" } } } # only those without type: "post"
58 | ) {
59 | edges {
60 | node {
61 | excerpt
62 | frontmatter {
63 | title
64 | order
65 | tags
66 | language
67 | slug
68 | }
69 | }
70 | }
71 | }
72 | }
73 | `
74 |
--------------------------------------------------------------------------------
/docs/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import { filter, get } from 'lodash'
2 | import { withPrefix } from 'gatsby'
3 |
4 | /**
5 | * Get a list of pages which represent child chapters
6 | * i.e. they are not at the top level.
7 | * @param {Array} pages - List of pages
8 | */
9 | export function getChildChapters(pages) {
10 | return filter(pages, (page) => {
11 | const slug = get(page, 'node.frontmatter.slug') || ''
12 | return slug.split('/').length > 1
13 | })
14 | }
15 |
16 | /**
17 | * Check current location for a match with a slug from
18 | * frontmatter.
19 | * @param {String} slug - Slug value to check for in path
20 | */
21 | export function slugIsInCurrentPath(slug) {
22 | /* eslint-disable no-restricted-globals */
23 | return (
24 | typeof location !== 'undefined' &&
25 | location.pathname.includes(withPrefix(slug))
26 | )
27 | /* eslint-enable no-restricted-globals */
28 | }
29 |
--------------------------------------------------------------------------------
/docs/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prescottprue/fireadmin/7d49cb768a77078608c4f21d219cea2b447dfd1f/docs/static/.nojekyll
--------------------------------------------------------------------------------
/docs/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prescottprue/fireadmin/7d49cb768a77078608c4f21d219cea2b447dfd1f/docs/static/favicon.ico
--------------------------------------------------------------------------------
/docs/static/images/FireadminLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prescottprue/fireadmin/7d49cb768a77078608c4f21d219cea2b447dfd1f/docs/static/images/FireadminLogo.png
--------------------------------------------------------------------------------
/docs/static/images/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prescottprue/fireadmin/7d49cb768a77078608c4f21d219cea2b447dfd1f/docs/static/images/cover.jpg
--------------------------------------------------------------------------------
/docs/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": {
3 | "rules": "database.rules.json"
4 | },
5 | "firestore": {
6 | "rules": "firestore.rules",
7 | "indexes": "firestore.indexes.json"
8 | },
9 | "storage": {
10 | "rules": "storage.rules"
11 | },
12 | "hosting": [
13 | {
14 | "target": "app",
15 | "public": "build",
16 | "ignore": [
17 | "**/node_modules/**",
18 | "firebase.json",
19 | "cypress.env.json",
20 | "serviceAccount.json",
21 | "*.md",
22 | "*.log",
23 | "**/.*"
24 | ],
25 | "rewrites": [
26 | {
27 | "source": "**",
28 | "destination": "/index.html"
29 | },
30 | {
31 | "source": "/docs/:page*",
32 | "destination": "https://docs.fireadmin.io/:page",
33 | "type": 301
34 | }
35 | ]
36 | },
37 | {
38 | "target": "docs",
39 | "public": "docs/public",
40 | "ignore": [
41 | "*.json",
42 | "*.md",
43 | "*.log",
44 | "**/.*",
45 | "**/node_modules/**",
46 | "firebase.json",
47 | "cypress.env.json",
48 | "cypress.json",
49 | "cypress",
50 | "serviceAccount.json",
51 | "jsconfig.json",
52 | "Dockerfile",
53 | "LICENSE",
54 | "scripts",
55 | "build",
56 | "src"
57 | ]
58 | }
59 | ],
60 | "functions": {
61 | "predeploy": ["npm --prefix \"$RESOURCE_DIR\" run build"],
62 | "ignore": [
63 | "**/node_modules/**",
64 | "jsconfig.json",
65 | ".runtimeconfig.json",
66 | "serviceAccount.json",
67 | "**/*.ts",
68 | "scripts",
69 | "tsconfig.json",
70 | "*.md",
71 | "*.log",
72 | "**/.*",
73 | "build",
74 | "coverage",
75 | "src",
76 | "test"
77 | ]
78 | },
79 | "emulators": {
80 | "database": {
81 | "port": 9000
82 | },
83 | "firestore": {
84 | "port": 8080
85 | },
86 | "hosting": {
87 | "port": 3000
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/firestore.indexes.json:
--------------------------------------------------------------------------------
1 | {
2 | "indexes": [
3 | {
4 | "collectionGroup": "events",
5 | "queryScope": "COLLECTION",
6 | "fields": [
7 | {
8 | "fieldPath": "eventType",
9 | "order": "ASCENDING"
10 | },
11 | {
12 | "fieldPath": "createdAt",
13 | "order": "ASCENDING"
14 | }
15 | ]
16 | },
17 | {
18 | "collectionGroup": "events",
19 | "queryScope": "COLLECTION",
20 | "fields": [
21 | {
22 | "fieldPath": "eventType",
23 | "order": "ASCENDING"
24 | },
25 | {
26 | "fieldPath": "createdAt",
27 | "order": "DESCENDING"
28 | }
29 | ]
30 | },
31 | {
32 | "collectionGroup": "projects",
33 | "queryScope": "COLLECTION",
34 | "fields": [
35 | {
36 | "fieldPath": "createdBy",
37 | "order": "ASCENDING"
38 | },
39 | {
40 | "fieldPath": "createdAt",
41 | "order": "DESCENDING"
42 | }
43 | ]
44 | },
45 | {
46 | "collectionGroup": "projects",
47 | "queryScope": "COLLECTION",
48 | "fields": [
49 | {
50 | "fieldPath": "createdBy",
51 | "order": "ASCENDING"
52 | },
53 | {
54 | "fieldPath": "desc",
55 | "order": "ASCENDING"
56 | }
57 | ]
58 | }
59 | ],
60 | "fieldOverrides": []
61 | }
62 |
--------------------------------------------------------------------------------
/firestore.rules:
--------------------------------------------------------------------------------
1 | rules_version = '2';
2 | service cloud.firestore {
3 | match /databases/{database}/documents {
4 | function isOwner(res) {
5 | return res.data.createdBy == request.auth.uid
6 | }
7 | function isOwnerOrPublic(res) {
8 | return isOwner(res) || res.data.public == true
9 | }
10 | function isCollaborator(res) {
11 | return res.data.collaborators[request.auth.uid] == true
12 | }
13 | match /users/{userId} {
14 | allow read, write: if request.auth.uid == userId;
15 | }
16 | match /users_public/{userId} {
17 | allow read;
18 | allow write: if request.auth.uid == userId;
19 | }
20 | match /projects/{projectId} {
21 | allow create: if isOwner(request.resource);
22 | allow read: if isOwner(resource) || isCollaborator(resource);
23 | allow write: if isOwner(resource) || isCollaborator(resource);
24 | match /{allChildren=**} {
25 | allow read: if isOwner(get(/databases/$(database)/documents/projects/$(projectId)))
26 | || isCollaborator(get(/databases/$(database)/documents/projects/$(projectId)));
27 | allow write: if request.auth != null;
28 | }
29 | }
30 | match /actionTemplates/{templateId} {
31 | allow create: if isOwner(request.resource);
32 | allow read: if isOwnerOrPublic(resource);
33 | allow write: if isOwner(resource);
34 | match /{allChildren=**} {
35 | allow read: if isOwnerOrPublic(get(/databases/$(database)/documents/actionTemplates/$(templateId)));
36 | allow write: if isOwnerOrPublic(get(/databases/$(database)/documents/actionTemplates/$(templateId)));
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/functions/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['../.eslintrc.js', 'plugin:jsdoc/recommended'],
3 | parser: '@typescript-eslint/parser',
4 | plugins: [
5 | 'node',
6 | 'jsdoc',
7 | '@typescript-eslint',
8 | 'prettier/@typescript-eslint'
9 | ],
10 | settings: {
11 | 'import/resolver': {
12 | node: {
13 | moduleDirectory: ['node_modules', '/']
14 | }
15 | }
16 | },
17 | rules: {
18 | '@typescript-eslint/no-explicit-any': 0,
19 | 'no-console': 0,
20 | 'no-return-await': 2,
21 | 'no-unused-vars': 0,
22 | 'jsdoc/require-param-type': 0,
23 | 'jsdoc/newline-after-description': 0,
24 | 'jsdoc/no-undefined-types': 0
25 | },
26 | overrides: [
27 | {
28 | files: ['./src/**/*.spec.ts', 'index.spec.ts'],
29 | env: {
30 | mocha: true
31 | },
32 | globals: {
33 | sinon: true,
34 | expect: true,
35 | should: true,
36 | functionsTest: true,
37 | mockFunctionsConfig: true
38 | },
39 | rules: {
40 | 'jsdoc/require-returns': 0,
41 | 'jsdoc/require-param-description': 0,
42 | 'jsdoc/require-param-type': 0,
43 | 'no-console': 0,
44 | 'import/no-dynamic-require': 0,
45 | 'no-unused-expressions': 0,
46 | 'import/prefer-default-export': 0,
47 | 'no-return-await': 2
48 | }
49 | }
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/functions/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/functions/.mocharc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | require: ['ts-node/register', './scripts/testSetup'],
3 | recursive: true
4 | }
--------------------------------------------------------------------------------
/functions/.nycrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@istanbuljs/nyc-config-typescript"
3 | }
--------------------------------------------------------------------------------
/functions/README.md:
--------------------------------------------------------------------------------
1 | # Fireadmin Functions
2 |
3 | ## Function Docs
4 |
5 | ### actionRunner
6 | Runs actions. Supports `backups` and `steps` parameters which can write to multiple resources including Cloud Storage, Firestore, and Real Time Database
7 |
8 | ### callGoogleApi
9 | Call a Google API authenticated with a service account. Currently used to set Bucket CORS from the project bucket config page.
10 |
11 | ### copyServiceAccountToFirestore
12 | Copy Service Account JSON files from Cloud Storage to Firestore
13 |
14 | ### indexActionTemplates
15 | Index public action templates within Algolia for autocomplete searching
16 |
17 | ### indexUsers
18 | Index public user data within Algolia for autocomplete searching
19 |
20 | ### sendInvite
21 | Send an invite email to users
22 |
23 | ### storageFileToRTDB
24 | Convert a file from cloud storage into JSON that is then stored within Firebase Real Time Database
25 |
26 |
27 | ## FAQ
28 | * Why are the babel transforms within `dependencies` instead of `devDependencies`?
29 | They are used to transform custom code provided as input to action steps. This allows for the template builder to write custom code that uses features like `import` and `async/await`.
30 |
--------------------------------------------------------------------------------
/functions/index.js:
--------------------------------------------------------------------------------
1 | const glob = require('glob')
2 | const path = require('path')
3 | const admin = require('firebase-admin')
4 | const functions = require('firebase-functions')
5 |
6 | // Initialize Firebase so it is available within functions
7 | try {
8 | admin.initializeApp()
9 | } catch (e) {
10 | /* istanbul ignore next: not called in tests */
11 | console.error(
12 | 'Caught error initializing app:',
13 | e.message || e
14 | )
15 | }
16 |
17 | // Set Firestore timestamp settings
18 | // NOTE: Skipped when running tests tests so it does not have to be mocked
19 | if (process.env.NODE_ENV !== 'test') {
20 | admin.firestore().settings({ timestampsInSnapshots: true })
21 | }
22 |
23 | const codeFolder = process.env.NODE_ENV === 'test' ? './src' : './dist'
24 |
25 | // Load all folders within dist directory (mirrors layout of src)
26 | const files = glob.sync(codeFolder + '/**/index.js', {
27 | cwd: __dirname,
28 | ignore: [
29 | './node_modules/**',
30 | codeFolder + '/utils/**',
31 | codeFolder + '/constants'
32 | ]
33 | })
34 |
35 | // Loop over all folders found within dist loading only the relevant function
36 | files.forEach((functionFile) => {
37 | // Get folder name from file name (removing any dashes)
38 | const folderName = path
39 | .basename(path.dirname(functionFile))
40 | .replace(/[-]/g, '')
41 |
42 | // Load single function from default
43 | !process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === folderName // eslint-disable-line no-unused-expressions
44 | ? (exports[folderName] = require(functionFile).default) // eslint-disable-line global-require
45 | : () => {}
46 | })
47 |
--------------------------------------------------------------------------------
/functions/index.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import sinon from 'sinon';
3 | import * as admin from 'firebase-admin'
4 |
5 | describe('Cloud Functions', () => {
6 | let myFunctions
7 | let adminInitStub
8 |
9 | before(() => {
10 | /* eslint-disable global-require */
11 | adminInitStub = sinon.stub(admin, 'initializeApp')
12 | myFunctions = require(`${__dirname}/../../index`)
13 | /* eslint-enable global-require */
14 | })
15 |
16 | after(() => {
17 | // Restoring our stubs to the original methods
18 | adminInitStub.restore()
19 | })
20 |
21 | it('exports an object', () => {
22 | expect(myFunctions).to.be.an('object')
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/functions/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src",
4 | "paths": {
5 | "utils/*": [
6 | "utils/*",
7 | ]
8 | }
9 | },
10 | "exclude": [
11 | "node_modules",
12 | "dist"
13 | ]
14 | }
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fireadmin-functions",
3 | "description": "Cloud Functions for Fireadmin application.",
4 | "scripts": {
5 | "test": "NODE_ENV=test mocha ./src/**/*.spec.ts",
6 | "test:watch": "yarn test --watch",
7 | "test:cov": "nyc --reporter=lcov --reporter=html yarn test",
8 | "test:cov:watch": "npm run test:cov -- --watch",
9 | "lint": "eslint -c .eslintrc.js src test",
10 | "lint:fix": "npm run lint -- --fix",
11 | "clean": "rimraf dist etc",
12 | "build": "npm run clean && tsc",
13 | "watch": "npm run build -- --watch",
14 | "shell": "npm run build && firebase functions:shell",
15 | "start": "npm run shell"
16 | },
17 | "dependencies": {
18 | "algoliasearch": "^4.9.1",
19 | "firebase-admin": "^9.8.0",
20 | "firebase-functions": "^3.14.0",
21 | "glob": "^7.1.7",
22 | "google-auth-library": "^6.1.1",
23 | "lodash": "^4.17.21",
24 | "mkdirp": "^1.0.4",
25 | "node-fetch": "^2.6.1"
26 | },
27 | "devDependencies": {
28 | "@istanbuljs/nyc-config-typescript": "^1.0.1",
29 | "@types/chai": "^4.2.18",
30 | "@types/mkdirp": "^1.0.1",
31 | "@types/mocha": "^8.2.2",
32 | "@types/node": "^15.3.0",
33 | "@types/node-fetch": "^2.5.10",
34 | "@types/sinon": "^10.0.0",
35 | "@types/sinon-chai": "^3.2.5",
36 | "chai": "^4.3.4",
37 | "faux-jax": "^5.0.6",
38 | "firebase-functions-test": "^0.2.3",
39 | "mocha": "^8.4.0",
40 | "nyc": "^15.1.0",
41 | "rimraf": "^3.0.2",
42 | "sinon": "^10.0.0",
43 | "sinon-chai": "^3.6.0",
44 | "ts-node": "^9.1.1",
45 | "typescript": "^4.2.4"
46 | },
47 | "engines": {
48 | "node": "14"
49 | },
50 | "author": "prescottprue (https://github.com/prescottprue)",
51 | "license": "MIT",
52 | "repository": {
53 | "type": "git",
54 | "url": "git+https://github.com/prescottprue/fireadmin.git"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/functions/scripts/testSetup.js:
--------------------------------------------------------------------------------
1 | const chai = require('chai')
2 | const sinon = require('sinon')
3 | const sinonChai = require('sinon-chai')
4 |
5 | chai.use(sinonChai)
6 |
7 | // Stub Firebase's config environment var
8 | process.env.FIREBASE_CONFIG = JSON.stringify({
9 | databaseURL: 'https://some-project.firebaseio.com',
10 | storageBucket: 'some-bucket.appspot.com'
11 | })
12 |
--------------------------------------------------------------------------------
/functions/src/actionRunner/constants.ts:
--------------------------------------------------------------------------------
1 | export const ACTION_RUNNER_EVENT_PATH = 'actionRunner'
2 | export const ACTION_RUNNER_REQUESTS_PATH = `requests/${ACTION_RUNNER_EVENT_PATH}`
3 |
--------------------------------------------------------------------------------
/functions/src/actionRunner/index.ts:
--------------------------------------------------------------------------------
1 | import * as functions from 'firebase-functions'
2 | import { ACTION_RUNNER_REQUESTS_PATH } from './constants'
3 | import runAction from './runAction'
4 |
5 | const runtimeOpts: functions.RuntimeOptions = {
6 | timeoutSeconds: 540,
7 | memory: '4GB'
8 | }
9 |
10 | /**
11 | * @name actionRunner
12 | * Run action based on action template. Multiple Service Account Types
13 | * supported (i.e. stored on Firestore or cloud storage)
14 | * @type {functions.CloudFunction}
15 | * @example
16 | * // functions shell with json file
17 | * actionRunner(require('./functions/test.json'))
18 | */
19 | export default functions
20 | .runWith(runtimeOpts)
21 | .database.ref(`${ACTION_RUNNER_REQUESTS_PATH}/{pushId}`)
22 | .onCreate(runAction)
23 |
--------------------------------------------------------------------------------
/functions/src/actionRunner/types.ts:
--------------------------------------------------------------------------------
1 | type ActionRunnerInstanceResource = 'rtdb' | 'firestore' | 'storage'
2 |
3 | export interface ActionRunnerInstanceSetting {
4 | path: string
5 | resource: ActionRunnerInstanceResource
6 | }
7 |
8 | interface EnvironmentServiceAccountObj {
9 | /**
10 | * Set by copyServiceAccountToFirestore Cloud Function
11 | */
12 | credential?: string
13 | fullPath: string
14 | }
15 |
16 | interface ActionRunnerEnvironment {
17 | serviceAccount: EnvironmentServiceAccountObj
18 | databaseURL: string
19 | locked: boolean
20 | readOnly: boolean
21 | createdBy: string
22 | createdAt: FirebaseFirestore.Timestamp
23 | }
24 |
25 | export interface ActionRunnerEventData {
26 | src: ActionRunnerInstanceSetting
27 | dest: ActionRunnerInstanceSetting
28 | environments?: ActionRunnerEnvironment[]
29 | projectId: string
30 | environmentValues?: any[]
31 | inputValues: any[]
32 | }
33 |
34 | type ActionStepType = 'copy' | 'custom'
35 |
36 | export interface ActionStep {
37 | type: ActionStepType
38 | disableBatching?: boolean
39 | src: ActionRunnerInstanceSetting
40 | dest: ActionRunnerInstanceSetting
41 | merge?: boolean
42 | subcollections?: boolean
43 | required?: boolean
44 | }
45 |
--------------------------------------------------------------------------------
/functions/src/callGoogleApi/constants.ts:
--------------------------------------------------------------------------------
1 | export const eventPathName = 'callGoogleApi'
2 |
--------------------------------------------------------------------------------
/functions/src/callGoogleApi/index.ts:
--------------------------------------------------------------------------------
1 | import * as functions from 'firebase-functions'
2 | import callGoogleApi from './callGoogleApi'
3 | import { eventPathName } from './constants'
4 |
5 | /**
6 | * @name callGoogleApi
7 | * Call a Google API with a Service Account
8 | * @type {functions.CloudFunction}
9 | */
10 | export default functions.database
11 | .ref(`/requests/${eventPathName}/{pushId}`)
12 | .onCreate(callGoogleApi)
13 |
--------------------------------------------------------------------------------
/functions/src/constants/firebasePaths.ts:
--------------------------------------------------------------------------------
1 | export const PROJECTS_COLLECTION = 'projects'
--------------------------------------------------------------------------------
/functions/src/indexActionTemplates/index.ts:
--------------------------------------------------------------------------------
1 | import * as functions from 'firebase-functions'
2 | import { createIndexFunc } from '../utils/search'
3 |
4 | // Updates the search index for action templates when template is created or updated.
5 | export default functions.firestore
6 | .document('/actionTemplates/{templateId}')
7 | .onWrite(
8 | // index action templates with parameter templateId only if template is public
9 | createIndexFunc({
10 | indexName: 'actionTemplates',
11 | idParam: 'templateId',
12 | indexCondition: (template) => template.public
13 | })
14 | )
15 |
--------------------------------------------------------------------------------
/functions/src/indexUser/index.ts:
--------------------------------------------------------------------------------
1 | import * as admin from 'firebase-admin'
2 | import * as functions from 'firebase-functions'
3 | import { createIndexFunc } from '../utils/search'
4 |
5 | // Updates the search index when users are created or displayName is updated
6 | export default functions.firestore.document('/users/{userId}').onWrite(
7 | createIndexFunc({
8 | indexName: 'users',
9 | idParam: 'userId',
10 | indexCondition: (user, change) => {
11 | const previousData = change.before.data()
12 | const nameChanged = user?.displayName !== previousData?.displayName
13 | if (nameChanged) {
14 | console.log('Display name changed re-indexing...')
15 | } else {
16 | console.log(
17 | 'Display name did not change, no reason to re-index. Exiting...'
18 | )
19 | }
20 | return nameChanged
21 | },
22 | otherPromises: [
23 | (user: any, objectID: string): Promise =>
24 | admin.database().ref(`displayNames/${objectID}`).set(user?.displayName)
25 | ]
26 | })
27 | )
28 |
--------------------------------------------------------------------------------
/functions/src/utils/async.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Async await wrapper for easy error handling
3 | * @param promise - Promise to wrap responses of
4 | * @returns Resolves and rejects with an array
5 | * @example
6 | * async function asyncFunctionWithThrow() {
7 | * const [err, snap] = await to(
8 | * admin.database().ref('some').once('value')
9 | * );
10 | * if (err) {
11 | * console.error('Error getting data:', err.message || err)
12 | * throw err
13 | * }
14 | * if (!snap.val()) throw new Error('Data not found');
15 | * console.log('Data found:', snap.val())
16 | * }
17 | */
18 | export function to(promise: Promise): Promise {
19 | return promise.then((data) => [null, data]).catch((err) => [err, undefined])
20 | }
21 |
22 | /**
23 | * Run promises in a waterfall instead of all the same time (Promise.all)
24 | * @param callbacks - List of promises to run in order
25 | * @returns Resolves when all promises have completed in order
26 | */
27 | export function promiseWaterfall(callbacks: any[]): Promise {
28 | return callbacks.reduce(
29 | (accumulator, callback) =>
30 | accumulator.then(
31 | typeof callback === 'function' ? callback : () => callback
32 | ),
33 | Promise.resolve()
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/functions/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Check object for all keys
3 | * @param obj - Object to check for keys
4 | * @param keysList - List of keys to check for in object
5 | * @returns Whether or not object has all keys
6 | */
7 | export function hasAll(obj: any, keysList: string[]): boolean {
8 | return !!obj && keysList.every((k) => !!obj[k])
9 | }
10 |
--------------------------------------------------------------------------------
/functions/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["es6"],
4 | "types": ["node"],
5 | "skipLibCheck": true,
6 | "target": "ES2019",
7 | "module": "commonjs",
8 | "moduleResolution": "node",
9 | "declaration": true,
10 | "outDir": "dist",
11 | "rootDir": "./src",
12 | "importHelpers": true,
13 | "downlevelIteration": true,
14 | "allowSyntheticDefaultImports": true,
15 | "strict": true,
16 | "noImplicitAny": false,
17 | "esModuleInterop": true
18 | },
19 | "typeRoots": ["./types", "./node_modules/@types"],
20 | "include": ["src/**/*"],
21 | "exclude": ["node_modules"]
22 | }
23 |
--------------------------------------------------------------------------------
/husky/pre-commit:
--------------------------------------------------------------------------------
1 | npx --no lint-staged
2 | yarn lint-staged
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src",
4 | "paths": {
5 | "utils/*": [
6 | "utils/*"
7 | ]
8 | }
9 | },
10 | "exclude": [
11 | "node_modules",
12 | "build"
13 | ]
14 | }
--------------------------------------------------------------------------------
/public/firebase-messaging-sw.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /* global firebase importScripts */
3 | // Import and configure the Firebase SDK
4 | // These scripts are made available when the app is served or deployed on Firebase Hosting
5 | // If you do not serve/host your project using Firebase Hosting see https://firebase.google.com/docs/web/setup
6 | importScripts('https://www.gstatic.com/firebasejs/5.4.1/firebase-app.js')
7 | importScripts('https://www.gstatic.com/firebasejs/5.4.1/firebase-messaging.js')
8 |
9 | const STAGE_MESSAGING_ID = '566109548798'
10 | const PROD_MESSAGING_ID = '286913465508'
11 |
12 | firebase.initializeApp({
13 | messagingSenderId:
14 | self.location.hostname.includes('fireadmin-33d82') ||
15 | self.location.hostname === 'fireadmin.io'
16 | ? PROD_MESSAGING_ID
17 | : STAGE_MESSAGING_ID
18 | })
19 |
20 | const messaging = firebase.messaging()
21 |
22 | // Custom background message handler
23 | messaging.setBackgroundMessageHandler(function (payload) {
24 | console.log(
25 | '[firebase-messaging-sw.js] Received background message ',
26 | payload
27 | )
28 |
29 | const notificationTitle = 'Fireadmin'
30 | const notificationOptions = {
31 | body: 'Background Message body.',
32 | icon: '/firebase-logo.png'
33 | }
34 |
35 | return self.registration.showNotification(
36 | notificationTitle,
37 | notificationOptions
38 | )
39 | })
40 |
--------------------------------------------------------------------------------
/public/humans.txt:
--------------------------------------------------------------------------------
1 | # Check it out: http://humanstxt.org/
2 |
3 | # TEAM
4 |
5 | -- --
6 |
7 | # THANKS
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "fireadmin",
3 | "name": "fireadmin",
4 | "start_url": "./",
5 | "scope": ".",
6 | "background_color": "#ef6c00",
7 | "theme_color": "#ef6c00",
8 | "display": "standalone",
9 | "gcm_sender_id": "103953800507"
10 | }
11 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/scripts/snapshotResolver.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | /** resolves from test to snapshot path */
3 | resolveSnapshotPath: (testPath, snapshotExtension) => {
4 | return testPath.replace('src/', '__snapshots__/') + snapshotExtension
5 | },
6 |
7 | /** resolves from snapshot to test path */
8 | resolveTestPath: (snapshotFilePath, snapshotExtension) => {
9 | return snapshotFilePath
10 | .replace('__snapshots__/', 'src/')
11 | .slice(0, -snapshotExtension.length)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/AnalyticsPageViewLogger/AnalyticsPageViewLogger.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useAnalytics, useUser } from 'reactfire'
3 | import { useLocation } from 'react-router-dom'
4 | import { setErrorUser } from 'utils/errorHandler'
5 |
6 | export default function AnalyticsPageViewLogger() {
7 | const location = useLocation()
8 | const analytics = useAnalytics()
9 | const user = useUser()
10 | // By passing `user.uid` to the second argument of `useEffect`,
11 | // we only set user id when it exists
12 | useEffect(() => {
13 | if (user?.uid) {
14 | analytics.setUserId(user.uid)
15 | setErrorUser(user)
16 | }
17 | }, [user?.uid]) // eslint-disable-line react-hooks/exhaustive-deps
18 |
19 | // By passing `location.pathname` to the second argument of `useEffect`,
20 | // we only log on first render and when the `pathname` changes
21 | useEffect(() => {
22 | if (!window.Cypress) {
23 | // Trigger event in Firebase analytics
24 | if (process.env.REACT_APP_FB_measurementId) {
25 | analytics.logEvent('page-view', { path_name: location.pathname })
26 | }
27 | // Track event in Segment
28 | if (process.env.REACT_APP_SEGMENT_ID && window.analytics) {
29 | window.analytics.track('page-view', { path_name: location.pathname })
30 | }
31 | }
32 | }, [location.pathname]) // eslint-disable-line react-hooks/exhaustive-deps
33 |
34 | return null
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/AnalyticsPageViewLogger/index.js:
--------------------------------------------------------------------------------
1 | import AnalyticsPageViewLogger from './AnalyticsPageViewLogger'
2 |
3 | export default AnalyticsPageViewLogger
4 |
--------------------------------------------------------------------------------
/src/components/CollectionSearch/CollectionSearch.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | spacer: {
3 | height: '2rem'
4 | },
5 | poweredBy: {
6 | ...theme.flexRowCenter,
7 | marginTop: '.4rem',
8 | marginBottom: '0rem'
9 | }
10 | })
11 |
--------------------------------------------------------------------------------
/src/components/CollectionSearch/ResultsList.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React from 'react'
3 | import PropTypes from 'prop-types'
4 | import { connectHits } from 'react-instantsearch/connectors'
5 | import MenuList from '@material-ui/core/MenuList'
6 | import SuggestedItem from './SuggestedItem'
7 |
8 | function ResultsList({ hits, onSuggestionClick }) {
9 | return (
10 |
11 |
12 | {hits.map((hit, i) => (
13 |
18 | ))}
19 |
20 |
21 | )
22 | }
23 |
24 | ResultsList.propTypes = {
25 | hits: PropTypes.array, // from connectHits
26 | onSuggestionClick: PropTypes.func // from UsersSearch
27 | }
28 |
29 | export default connectHits(ResultsList)
30 |
--------------------------------------------------------------------------------
/src/components/CollectionSearch/SearchResults.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Stats } from 'react-instantsearch/dom'
4 | import { connectStateResults } from 'react-instantsearch/connectors'
5 | import ResultsList from './ResultsList'
6 |
7 | function SearchResults({ searchState, searchResults, onSuggestionClick }) {
8 | return searchState.query && searchResults && searchResults.nbHits !== 0 ? (
9 |
10 |
11 |
12 |
13 | ) : null
14 | }
15 |
16 | SearchResults.propTypes = {
17 | searchState: PropTypes.shape({
18 | // from connectStateResults
19 | query: PropTypes.string
20 | }),
21 | onSuggestionClick: PropTypes.func.isRequired,
22 | searchResults: PropTypes.object // from connectStateResults
23 | }
24 |
25 | export default connectStateResults(SearchResults)
26 |
--------------------------------------------------------------------------------
/src/components/CollectionSearch/SuggestedItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import MenuItem from '@material-ui/core/MenuItem'
4 | import { Highlight } from 'react-instantsearch/dom'
5 |
6 | function SuggestedItem({ hit, onClick }) {
7 | function handleClick() {
8 | onClick(hit)
9 | }
10 |
11 | return (
12 |
29 | )
30 | }
31 |
32 | SuggestedItem.propTypes = {
33 | hit: PropTypes.object,
34 | onClick: PropTypes.func
35 | }
36 |
37 | export default SuggestedItem
38 |
--------------------------------------------------------------------------------
/src/components/CollectionSearch/index.js:
--------------------------------------------------------------------------------
1 | import CollectionSearch from './CollectionSearch'
2 |
3 | export default CollectionSearch
4 |
--------------------------------------------------------------------------------
/src/components/LoadingSpinner/LoadingSpinner.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import CircularProgress from '@material-ui/core/CircularProgress'
4 | import { makeStyles } from '@material-ui/core/styles'
5 | import styles from './LoadingSpinner.styles'
6 |
7 | const useStyles = makeStyles(styles)
8 |
9 | function LoadingSpinner({ size }) {
10 | const classes = useStyles()
11 |
12 | return (
13 |
18 | )
19 | }
20 |
21 | LoadingSpinner.propTypes = {
22 | size: PropTypes.number
23 | }
24 |
25 | export default LoadingSpinner
26 |
--------------------------------------------------------------------------------
/src/components/LoadingSpinner/LoadingSpinner.styles.js:
--------------------------------------------------------------------------------
1 | export default () => ({
2 | root: {
3 | display: 'flex',
4 | alignItems: 'center',
5 | flexDirection: 'column',
6 | justifyContent: 'flex-start',
7 | paddingTop: '7rem',
8 | height: '100%'
9 | },
10 | progress: {
11 | display: 'flex',
12 | justifyContent: 'center',
13 | alignItems: 'center',
14 | height: '50%'
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/src/components/LoadingSpinner/LoadingSpinner.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import renderer from 'react-test-renderer'
4 | import LoadingSpinner from './index'
5 |
6 | it('renders without crashing', () => {
7 | const div = document.createElement('div')
8 | ReactDOM.render(, div)
9 | ReactDOM.unmountComponentAtNode(div)
10 | })
11 |
12 | it('renders a spinner', () => {
13 | const tree = renderer.create().toJSON()
14 | expect(tree).toMatchSnapshot()
15 | })
16 |
--------------------------------------------------------------------------------
/src/components/LoadingSpinner/index.js:
--------------------------------------------------------------------------------
1 | import LoadingSpinner from './LoadingSpinner'
2 |
3 | export default LoadingSpinner
4 |
--------------------------------------------------------------------------------
/src/components/SetupFirestore/SetupFirestore.js:
--------------------------------------------------------------------------------
1 | import { useFirestore } from 'reactfire'
2 |
3 | export default function SetupFirestore() {
4 | const firestore = useFirestore()
5 | const firestoreSettings = {}
6 | if (window.Cypress) {
7 | // Needed for Firestore support in Cypress (see https://github.com/cypress-io/cypress/issues/6350)
8 | firestoreSettings.experimentalForceLongPolling = true
9 | }
10 | if (process.env.REACT_APP_FIRESTORE_EMULATOR_HOST) {
11 | firestoreSettings.host = process.env.REACT_APP_FIRESTORE_EMULATOR_HOST
12 | firestoreSettings.ssl = false
13 | // eslint-disable-next-line no-console
14 | console.debug(
15 | `Firestore emulator enabled: ${process.env.REACT_APP_FIRESTORE_EMULATOR_HOST}`
16 | )
17 | }
18 | firestore.settings(firestoreSettings)
19 |
20 | return null
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/SetupFirestore/index.js:
--------------------------------------------------------------------------------
1 | import SetupFirestore from './SetupFirestore'
2 |
3 | export default SetupFirestore
4 |
--------------------------------------------------------------------------------
/src/components/SetupMessaging/SetupMessaging.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useMessaging, useUser } from 'reactfire'
3 | import useSetupMessaging from './useSetupMessaging'
4 |
5 | function LoadMessaging() {
6 | const { initializeMessaging } = useSetupMessaging()
7 | initializeMessaging()
8 | return null
9 | }
10 |
11 | function LoadIfAuthed() {
12 | const user = useUser()
13 |
14 | // Render nothing if user is not logged in or if messaging is not supported
15 | if (!user || !user.uid) {
16 | return null
17 | }
18 |
19 | return
20 | }
21 |
22 | export default function SetupMessaging() {
23 | const { isSupported } = useMessaging
24 |
25 | // Render nothing if not supported or run UI tests
26 | if (
27 | !process.env.REACT_APP_FB_measurementId ||
28 | window.Cypress ||
29 | !isSupported()
30 | ) {
31 | return null
32 | }
33 |
34 | // Load messaging if user is logged in
35 | return
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/SetupMessaging/index.js:
--------------------------------------------------------------------------------
1 | import SetupMessaging from './SetupMessaging'
2 |
3 | export default SetupMessaging
4 |
--------------------------------------------------------------------------------
/src/components/SplitButton/index.js:
--------------------------------------------------------------------------------
1 | import SplitButton from './SplitButton'
2 |
3 | export default SplitButton
4 |
--------------------------------------------------------------------------------
/src/components/TabContainer/TabContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Typography from '@material-ui/core/Typography'
4 |
5 | function TabContainer({ children }) {
6 | return (
7 |
8 | {children}
9 |
10 | )
11 | }
12 |
13 | TabContainer.propTypes = {
14 | children: PropTypes.object
15 | }
16 |
17 | export default TabContainer
18 |
--------------------------------------------------------------------------------
/src/components/TabContainer/index.js:
--------------------------------------------------------------------------------
1 | import TabContainer from './TabContainer'
2 |
3 | export default TabContainer
4 |
--------------------------------------------------------------------------------
/src/components/UsersList/UsersList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import PersonIcon from '@material-ui/icons/Person'
4 | import Checkbox from '@material-ui/core/Checkbox'
5 | import Avatar from '@material-ui/core/Avatar'
6 | import List from '@material-ui/core/List'
7 | import ListItem from '@material-ui/core/ListItem'
8 | import ListItemAvatar from '@material-ui/core/ListItemAvatar'
9 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
10 | import ListItemText from '@material-ui/core/ListItemText'
11 |
12 | function UsersList({ users, onUserClick }) {
13 | return (
14 |
15 | {users.map((user, i) => (
16 |
19 |
20 | {user.avatarUrl ? (
21 |
22 | ) : (
23 |
24 |
25 |
26 | )}
27 |
28 |
29 |
30 | onUserClick(user)}
33 | checked={
34 | !!users.find(
35 | (currentUser) =>
36 | currentUser.objectID === user.id || user.objectID
37 | )
38 | }
39 | />
40 |
41 |
42 | ))}
43 |
44 | )
45 | }
46 |
47 | UsersList.propTypes = {
48 | users: PropTypes.array.isRequired,
49 | onUserClick: PropTypes.func.isRequired
50 | }
51 |
52 | export default UsersList
53 |
--------------------------------------------------------------------------------
/src/components/UsersList/index.js:
--------------------------------------------------------------------------------
1 | import UsersList from './UsersList'
2 |
3 | export default UsersList
4 |
--------------------------------------------------------------------------------
/src/components/UsersSearch/ResultsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connectHits } from 'react-instantsearch/connectors'
4 | import List from '@material-ui/core/List'
5 | import SuggestedUser from './SuggestedUser'
6 |
7 | function ResultsList({ hits, onSuggestionClick }) {
8 | return (
9 |
10 | {hits.map((hit, i) => (
11 |
17 | ))}
18 |
19 | )
20 | }
21 |
22 | ResultsList.propTypes = {
23 | hits: PropTypes.array, // from connectHits
24 | onSuggestionClick: PropTypes.func // from UsersSearch
25 | }
26 |
27 | export default connectHits(ResultsList)
28 |
--------------------------------------------------------------------------------
/src/components/UsersSearch/SearchResults.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Stats } from 'react-instantsearch/dom'
4 | import { connectStateResults } from 'react-instantsearch/connectors'
5 | import ResultsList from './ResultsList'
6 |
7 | function SearchResults({
8 | searchState,
9 | searchResults,
10 | onSuggestionClick,
11 | resultsTitle
12 | }) {
13 | return searchState.query && searchResults && searchResults.nbHits !== 0 ? (
14 |
15 | {resultsTitle ?
{resultsTitle}
: null}
16 |
17 |
18 |
19 | ) : null
20 | }
21 |
22 | SearchResults.propTypes = {
23 | searchState: PropTypes.shape({
24 | // from connectStateResults
25 | query: PropTypes.string
26 | }),
27 | resultsTitle: PropTypes.string,
28 | onSuggestionClick: PropTypes.func.isRequired,
29 | searchResults: PropTypes.object // from connectStateResults
30 | }
31 |
32 | export default connectStateResults(SearchResults)
33 |
--------------------------------------------------------------------------------
/src/components/UsersSearch/SuggestedUser.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Highlight } from 'react-instantsearch/dom'
4 | import ListItem from '@material-ui/core/ListItem'
5 | import ListItemText from '@material-ui/core/ListItemText'
6 |
7 | function SuggestedUser({ hit, onClick }) {
8 | return (
9 | onClick(hit)} style={{ cursor: 'pointer' }}>
10 | }
12 | // secondary={}
13 | />
14 |
15 | )
16 | }
17 |
18 | SuggestedUser.propTypes = {
19 | hit: PropTypes.object,
20 | onClick: PropTypes.func
21 | }
22 |
23 | export default SuggestedUser
24 |
--------------------------------------------------------------------------------
/src/components/UsersSearch/UsersSearch.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | spacer: {
3 | height: '2rem'
4 | },
5 | poweredBy: {
6 | ...theme.flexRowCenter,
7 | marginTop: '.4rem',
8 | marginBottom: '0rem'
9 | }
10 | })
11 |
--------------------------------------------------------------------------------
/src/components/UsersSearch/index.js:
--------------------------------------------------------------------------------
1 | import UsersSearch from './UsersSearch'
2 |
3 | export default UsersSearch
4 |
--------------------------------------------------------------------------------
/src/components/VersionChangeReloader/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useDatabase, useDatabaseObjectData } from 'reactfire'
3 |
4 | export default function VersionChangeReloader() {
5 | const database = useDatabase()
6 | const versionInfo = useDatabaseObjectData(database.ref('versionInfo'))
7 |
8 | const sessionStorageKey = 'fireadminVersion'
9 |
10 | useEffect(() => {
11 | const currentRemoteVersion = versionInfo.current
12 | const currentClientVersion = window.version
13 | const sessionVersion = window.sessionStorage.getItem(sessionStorageKey)
14 | // set version to session storage if it does not exist
15 | if (!sessionVersion) {
16 | window.sessionStorage.setItem(sessionStorageKey, currentRemoteVersion)
17 | // Exit since the client does not have a version in session storage
18 | return
19 | }
20 |
21 | // Exit if there is no current remote version
22 | if (!currentRemoteVersion) {
23 | return
24 | }
25 |
26 | // Check if version in Database matches client's session version
27 | const versionDiscrepencyExists =
28 | currentRemoteVersion !== currentClientVersion
29 |
30 | // Previous refresh or version set to state has happened
31 | const refreshHasOccurred = currentRemoteVersion === sessionVersion
32 |
33 | // Refresh if session contains different version than database
34 | if (
35 | versionDiscrepencyExists &&
36 | !refreshHasOccurred &&
37 | // refresh not enabled locally since DB update happens in deploy
38 | !window.location.host.includes('localhost')
39 | ) {
40 | window.location.reload(true)
41 | }
42 | }, [versionInfo])
43 |
44 | return null
45 | }
46 |
--------------------------------------------------------------------------------
/src/constants/analytics.js:
--------------------------------------------------------------------------------
1 | export default {
2 | login: 'Login',
3 | signup: 'Signup',
4 | createProject: 'Create Project',
5 | deleteProject: 'Delete Project',
6 | bucketAction: 'Storage Bucket Action',
7 | createEnvironment: 'Create Environment',
8 | updateEnvironment: 'Update Environment',
9 | deleteEnvironment: 'Delete Environment',
10 | createActionTemplate: 'Create Action Template',
11 | updateActionTemplate: 'Update Action Template',
12 | deleteActionTemplate: 'Delete Action Template',
13 | addRole: 'Add Role',
14 | updateRole: 'Update Role',
15 | removeRole: 'Delete Role',
16 | updatePermission: 'Update Permission',
17 | deletePermission: 'Delete Permission',
18 | requestActionRun: 'Request Action Run',
19 | addCollaborator: 'Add Collaborator',
20 | removeCollaborator: 'Remove Collaborator',
21 | deleteRole: 'Delete Role',
22 | denyMessagingPermission: 'Deny Messaging Permissions',
23 | invalidTemplateRunAttempt: 'Invalid Template Run Attempt'
24 | }
25 |
--------------------------------------------------------------------------------
/src/constants/defaultRoles.js:
--------------------------------------------------------------------------------
1 | export const defaultRoles = {
2 | owner: {
3 | name: 'Owner',
4 | permissions: {
5 | read: {
6 | environments: true,
7 | members: true,
8 | permissions: true,
9 | roles: true
10 | },
11 | update: {
12 | environments: true,
13 | members: true,
14 | permissions: true,
15 | roles: true
16 | },
17 | delete: {
18 | environments: true,
19 | members: true,
20 | permissions: true,
21 | roles: true
22 | },
23 | create: {
24 | environments: true,
25 | members: true,
26 | permissions: true,
27 | roles: true
28 | }
29 | }
30 | },
31 | editor: {
32 | name: 'Editor',
33 | permissions: {
34 | read: { environments: true },
35 | update: { environments: true },
36 | create: { environments: true }
37 | }
38 | },
39 | viewer: {
40 | permissions: { read: { environments: true } }
41 | }
42 | }
43 |
44 | export default defaultRoles
45 |
--------------------------------------------------------------------------------
/src/constants/docs.js:
--------------------------------------------------------------------------------
1 | export const DOCS_URL =
2 | process.env.NODE_ENV === 'production'
3 | ? window.location.hostname.includes('fireadmin-stage')
4 | ? 'https://fireadmin-stage-docs.firebaseapp.com'
5 | : 'https://docs.fireadmin.io'
6 | : 'http://localhost:8000'
7 |
--------------------------------------------------------------------------------
/src/constants/firebasePaths.js:
--------------------------------------------------------------------------------
1 | export const ACTION_TEMPLATES_PATH = 'actionTemplates'
2 | export const ACTIONS_PATH = 'actions'
3 | export const ACTION_RUNNER_REQUESTS_PATH = 'requests/actionRunner'
4 | export const ACTION_RUNNER_RESPONSES_PATH = 'responses/actionRunner'
5 | export const DISPLAY_NAMES_PATH = 'displayNames'
6 | export const PROJECTS_COLLECTION = 'projects'
7 | export const USERS_COLLECTION = 'users'
8 |
--------------------------------------------------------------------------------
/src/constants/formNames.js:
--------------------------------------------------------------------------------
1 | export const NEW_ACTION_TEMPLATE_FORM_NAME = 'newActionTemplate'
2 | export const ACTION_RUNNER_FORM_NAME = 'actionRunner'
3 | export const PROJECT_PERMISSIONS_FORM_NAME = 'projectPermissions'
4 | export const PROJECT_ROLES_FORM_NAME = 'projectRoles'
5 | export const NEW_ROLE_FORM_NAME = 'newRole'
6 |
--------------------------------------------------------------------------------
/src/constants/paths.js:
--------------------------------------------------------------------------------
1 | export const LIST_PATH = '/projects'
2 | export const ACCOUNT_PATH = '/account'
3 | export const LOGIN_PATH = '/login'
4 | export const ACTION_TEMPLATES_PATH = '/action-templates'
5 | export const PROJECT_ACTION_PATH = 'actions'
6 | export const PROJECT_ENVIRONMENTS_PATH = 'environments'
7 | export const PROJECT_EVENTS_PATH = 'events'
8 | export const PROJECT_BUCKET_CONFIG_PATH = 'bucketConfig'
9 | export const PROJECT_PERMISSIONS_PATH = 'permissions'
10 |
--------------------------------------------------------------------------------
/src/containers/App/index.js:
--------------------------------------------------------------------------------
1 | import App from './App'
2 |
3 | export default App
4 |
--------------------------------------------------------------------------------
/src/containers/Navbar/AccountMenu.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useFirebaseApp } from 'reactfire'
3 | import { useHistory } from 'react-router-dom'
4 | import Menu from '@material-ui/core/Menu'
5 | import MenuItem from '@material-ui/core/MenuItem'
6 | import IconButton from '@material-ui/core/IconButton'
7 | import AccountCircle from '@material-ui/icons/AccountCircle'
8 | import { makeStyles } from '@material-ui/core/styles'
9 | import { ACCOUNT_PATH } from 'constants/paths'
10 |
11 | const useStyles = makeStyles(() => ({
12 | buttonRoot: {
13 | color: 'white'
14 | }
15 | }))
16 |
17 | function AccountMenu() {
18 | const classes = useStyles()
19 | const [anchorEl, setMenu] = useState(null)
20 | const history = useHistory()
21 | const firebase = useFirebaseApp()
22 |
23 | function closeAccountMenu(e) {
24 | setMenu(null)
25 | }
26 | function handleMenu(e) {
27 | setMenu(e.target)
28 | }
29 | async function handleLogout() {
30 | closeAccountMenu()
31 | history.replace('/')
32 | await firebase.auth().signOut()
33 | }
34 | function goToAccount() {
35 | closeAccountMenu()
36 | history.push(ACCOUNT_PATH)
37 | }
38 |
39 | return (
40 | <>
41 |
46 |
47 |
48 |
58 | >
59 | )
60 | }
61 |
62 | export default AccountMenu
63 |
--------------------------------------------------------------------------------
/src/containers/Navbar/Navbar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { useUser } from 'reactfire'
4 | import Button from '@material-ui/core/Button'
5 | import { LIST_PATH, LOGIN_PATH } from 'constants/paths'
6 | import AccountMenu from './AccountMenu'
7 | import NavbarWithoutAuth from './NavbarWithoutAuth'
8 |
9 | const buttonStyle = {
10 | color: 'white',
11 | textDecoration: 'none',
12 | alignSelf: 'center'
13 | }
14 |
15 | function Navbar() {
16 | const user = useUser()
17 | const authExists = !!user && !!user.uid
18 |
19 | return (
20 |
21 | {authExists ? (
22 |
23 | ) : (
24 |
31 | )}
32 |
33 | )
34 | }
35 |
36 | export default Navbar
37 |
--------------------------------------------------------------------------------
/src/containers/Navbar/Navbar.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | flex: {
3 | flexGrow: 1
4 | },
5 | appBar: {
6 | backgroundColor: theme.palette.primary1Color
7 | },
8 | brand: {
9 | fontSize: '1.5rem',
10 | fontWeight: '300'
11 | },
12 | otherLink: {
13 | marginLeft: '4rem'
14 | },
15 | color: theme.palette.primary1Color
16 | })
17 |
--------------------------------------------------------------------------------
/src/containers/Navbar/NavbarWithoutAuth.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'react-router-dom'
4 | import AppBar from '@material-ui/core/AppBar'
5 | import Toolbar from '@material-ui/core/Toolbar'
6 | import Typography from '@material-ui/core/Typography'
7 | import Button from '@material-ui/core/Button'
8 | import IconButton from '@material-ui/core/IconButton'
9 | import Hidden from '@material-ui/core/Hidden'
10 | import Tooltip from '@material-ui/core/Tooltip'
11 | import { makeStyles } from '@material-ui/core/styles'
12 | import DocsIcon from '@material-ui/icons/LibraryBooks'
13 | import { DOCS_URL } from 'constants/docs'
14 | import styles from './Navbar.styles'
15 |
16 | const useStyles = makeStyles(styles)
17 |
18 | function NavbarWithoutAuth({ children, brandPath }) {
19 | const classes = useStyles()
20 |
21 | return (
22 |
23 |
24 |
31 | Fireadmin
32 |
33 |
34 |
42 |
43 |
44 |
45 |
46 |
51 |
52 |
53 |
54 |
55 | {children}
56 |
57 |
58 | )
59 | }
60 |
61 | NavbarWithoutAuth.propTypes = {
62 | children: PropTypes.element,
63 | brandPath: PropTypes.string
64 | }
65 |
66 | export default NavbarWithoutAuth
67 |
--------------------------------------------------------------------------------
/src/containers/Navbar/index.js:
--------------------------------------------------------------------------------
1 | import Navbar from './Navbar'
2 |
3 | export default Navbar
4 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | box-sizing: border-box;
3 | }
4 |
5 | html,
6 | body {
7 | margin: 0;
8 | padding: 0;
9 | height: 100%;
10 | background-color: #F2F2F2;
11 | font-family: 'Roboto', 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
12 | }
13 | a {
14 | text-decoration: none !important;
15 | }
16 | *,
17 | *:before,
18 | *:after {
19 | box-sizing: inherit;
20 | }
21 | /* [type=button]{
22 | -webkit-appearance: none !important;
23 | } */
24 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { initScripts } from './utils'
4 | import { version } from '../package.json'
5 | import App from './containers/App'
6 | import createRoutes from './routes'
7 | import './index.css'
8 |
9 | // import * as serviceWorker from './serviceWorker'
10 |
11 | // Window Variables
12 | // ------------------------------------
13 | window.version = version
14 | window.env = process.env.FIREADMIN_ENV || 'local'
15 | initScripts()
16 |
17 | const routes = createRoutes()
18 |
19 | ReactDOM.render(, document.getElementById('root'))
20 |
21 | // If you want your app to work offline and load faster, you can change
22 | // unregister() to register() below. Note this comes with some pitfalls.
23 | // Learn more about service workers: http://bit.ly/CRA-PWA
24 | // serviceWorker.unregister()
25 |
--------------------------------------------------------------------------------
/src/layouts/CoreLayout/CoreLayout.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { makeStyles } from '@material-ui/core/styles'
4 | import Navbar from 'containers/Navbar'
5 | import { SuspenseWithPerf } from 'reactfire'
6 | import NavbarWithoutAuth from 'containers/Navbar/NavbarWithoutAuth'
7 | import { Notifications } from 'modules/notification'
8 | import VersionChangeReloader from 'components/VersionChangeReloader'
9 | import styles from './CoreLayout.styles'
10 | import SetupFirestore from 'components/SetupFirestore'
11 |
12 | const useStyles = makeStyles(styles)
13 |
14 | function CoreLayout({ children }) {
15 | const classes = useStyles()
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
} traceId="load-navbar">
23 |
24 |
25 |
{children}
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | CoreLayout.propTypes = {
35 | children: PropTypes.element.isRequired
36 | }
37 |
38 | export default CoreLayout
39 |
--------------------------------------------------------------------------------
/src/layouts/CoreLayout/CoreLayout.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | height: '100%',
4 | '-webkit-overflow-scrolling': 'touch'
5 | },
6 | children: {
7 | height: 'calc(100% - 48px)',
8 | overflowY: 'scroll',
9 | backgroundColor: 'scroll'
10 | }
11 | })
12 |
--------------------------------------------------------------------------------
/src/layouts/CoreLayout/index.js:
--------------------------------------------------------------------------------
1 | import CoreLayout from './CoreLayout'
2 |
3 | export default CoreLayout
4 |
--------------------------------------------------------------------------------
/src/layouts/SidebarLayout/index.js:
--------------------------------------------------------------------------------
1 | import SidebarLayout from './SidebarLayout'
2 |
3 | export default SidebarLayout
4 |
--------------------------------------------------------------------------------
/src/layouts/SidebarLayout/sidebarOptions.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import LayersIcon from '@material-ui/icons/Layers'
3 | import HomeIcon from '@material-ui/icons/Home'
4 | import DeviceHubIcon from '@material-ui/icons/SettingsEthernet'
5 | import StorageIcon from '@material-ui/icons/Dns'
6 | import EventIcon from '@material-ui/icons/ViewList'
7 | import PeopleIcon from '@material-ui/icons/People'
8 | import {
9 | PROJECT_ENVIRONMENTS_PATH,
10 | PROJECT_ACTION_PATH,
11 | PROJECT_BUCKET_CONFIG_PATH,
12 | PROJECT_EVENTS_PATH,
13 | PROJECT_PERMISSIONS_PATH
14 | } from 'constants/paths'
15 |
16 | export default [
17 | {
18 | value: '',
19 | label: 'Project Overview',
20 | iconElement:
21 | },
22 | {
23 | value: PROJECT_ENVIRONMENTS_PATH,
24 | iconElement:
25 | },
26 | {
27 | value: PROJECT_ACTION_PATH,
28 | iconElement:
29 | },
30 | {
31 | value: PROJECT_BUCKET_CONFIG_PATH,
32 | label: 'Bucket Config',
33 | iconElement:
34 | },
35 | {
36 | value: PROJECT_EVENTS_PATH,
37 | label: 'Events',
38 | iconElement:
39 | },
40 | {
41 | value: PROJECT_PERMISSIONS_PATH,
42 | label: 'Users/Permissions',
43 | iconElement:
44 | }
45 | ]
46 |
--------------------------------------------------------------------------------
/src/modules/notification/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const NOTIFICATION_SHOW = 'NOTIFICATION_SHOW'
2 | export const NOTIFICATION_DISMISS = 'NOTIFICATION_DISMISS'
3 | export const NOTIFICATION_CLEAR = 'NOTIFICATION_CLEAR'
4 |
--------------------------------------------------------------------------------
/src/modules/notification/actions.js:
--------------------------------------------------------------------------------
1 | import {
2 | NOTIFICATION_SHOW,
3 | NOTIFICATION_DISMISS,
4 | NOTIFICATION_CLEAR
5 | } from './actionTypes'
6 |
7 | const defaultDismissTime = 5000 // 5 seconds
8 |
9 | /**
10 | * Publish a notification. if `dismissAfter` is set, the notification will be
11 | * auto dismissed after the given period.
12 | * @param {Object} notif - Object containing
13 | * @param {Object} notif.kind - Kinda of notification (success, warning, failure)
14 | * @param {Object} notif.message - Notification message
15 | * @param {Object} notif.dismissAfter - Time after which to dismiss
16 | * notification (default time set in constants)
17 | */
18 | export function showNotification(notif) {
19 | const payload = { ...notif }
20 | // Set default id to now if none provided
21 | if (!payload.id) {
22 | payload.id = Date.now()
23 | }
24 | return (dispatch) => {
25 | dispatch({ type: NOTIFICATION_SHOW, payload })
26 |
27 | setTimeout(() => {
28 | dispatch({
29 | type: NOTIFICATION_DISMISS,
30 | payload: payload.id
31 | })
32 | }, payload.dismissAfter || defaultDismissTime)
33 | }
34 | }
35 |
36 | /**
37 | * Show a success notification that hides itself
38 | * after 5 seconds.
39 | * @param {String} message - Message to show
40 | */
41 | export function showSuccess(message) {
42 | return showNotification({ type: 'success', message })
43 | }
44 |
45 | /**
46 | * Show an error notification that hides itself
47 | * after 5 seconds.
48 | * @param {String} message - Message to show
49 | */
50 | export function showError(message) {
51 | return showNotification({
52 | type: 'error',
53 | message: `Error: ${message || ''}`
54 | })
55 | }
56 |
57 | /**
58 | * Dismiss a notification by the given id.
59 | * @param {Number} id - notification id
60 | */
61 | export function dismissNotification(payload) {
62 | return {
63 | type: NOTIFICATION_DISMISS,
64 | payload
65 | }
66 | }
67 |
68 | /**
69 | * Clear all notifications
70 | */
71 | export function clearNotifications() {
72 | return { type: NOTIFICATION_CLEAR }
73 | }
74 |
--------------------------------------------------------------------------------
/src/modules/notification/index.js:
--------------------------------------------------------------------------------
1 | import Notifications from './Notifications'
2 | import useNotifications from './useNotifications'
3 |
4 | export { useNotifications, Notifications }
5 |
--------------------------------------------------------------------------------
/src/modules/notification/useNotifications.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react'
2 | import { NotificationsContext } from './NotificationsProvider'
3 |
4 | export default function useNotifications() {
5 | return useContext(NotificationsContext)
6 | }
7 |
--------------------------------------------------------------------------------
/src/routes/Account/components/AccountEditor/AccountEditor.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | avatarCurrent: {
3 | width: '100%',
4 | maxWidth: '13rem',
5 | marginTop: '3rem',
6 | height: 'auto',
7 | cursor: 'pointer'
8 | },
9 | gridItem: {
10 | textAlign: 'center',
11 | marginTop: theme.spacing(5)
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/src/routes/Account/components/AccountEditor/index.js:
--------------------------------------------------------------------------------
1 | import AccountEditor from './AccountEditor'
2 |
3 | export default AccountEditor
4 |
--------------------------------------------------------------------------------
/src/routes/Account/components/AccountForm/AccountForm.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | ...theme.flexColumnCenter,
4 | justifyContent: 'flex-start',
5 | width: '100%',
6 | height: '100%'
7 | },
8 | fields: {
9 | width: '100%',
10 | marginBottom: '2rem'
11 | }
12 | })
13 |
--------------------------------------------------------------------------------
/src/routes/Account/components/AccountForm/index.js:
--------------------------------------------------------------------------------
1 | import AccountForm from './AccountForm'
2 |
3 | export default AccountForm
4 |
--------------------------------------------------------------------------------
/src/routes/Account/components/AccountPage/AccountPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Paper from '@material-ui/core/Paper'
3 | import Grid from '@material-ui/core/Grid'
4 | import Typography from '@material-ui/core/Typography'
5 | import { makeStyles } from '@material-ui/core/styles'
6 | import { SuspenseWithPerf } from 'reactfire'
7 | import LoadingSpinner from 'components/LoadingSpinner'
8 | import AccountEditor from '../AccountEditor'
9 | import styles from './AccountPage.styles'
10 |
11 | const useStyles = makeStyles(styles)
12 |
13 | function AccountPage() {
14 | const classes = useStyles()
15 |
16 | return (
17 |
18 |
19 |
20 | Account
21 | }
23 | traceId="load-account">
24 |
25 |
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | export default AccountPage
33 |
--------------------------------------------------------------------------------
/src/routes/Account/components/AccountPage/AccountPage.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | paddingTop: theme.spacing(3),
4 | paddingBottom: theme.spacing(3),
5 | overflowY: 'scroll'
6 | },
7 | pane: {
8 | ...theme.flexColumnCenter,
9 | justifyContent: 'space-around',
10 | padding: theme.spacing(6)
11 | // maxHeight: '40rem'
12 | },
13 | settings: {},
14 | avatar: {
15 | maxWidth: '13rem',
16 | marginTop: '3rem'
17 | },
18 | avatarCurrent: {
19 | width: '100%',
20 | maxWidth: '13rem',
21 | marginTop: '3rem',
22 | height: 'auto',
23 | cursor: 'pointer'
24 | },
25 | title: {
26 | // marginBottom: theme.spacing(5)
27 | },
28 | gridItem: {
29 | textAlign: 'center',
30 | marginTop: theme.spacing(5)
31 | },
32 | meta: {
33 | ...theme.flexColumnCenter,
34 | flexBasis: '60%',
35 | marginBottom: '3rem',
36 | marginTop: '2rem'
37 | }
38 | })
39 |
--------------------------------------------------------------------------------
/src/routes/Account/components/AccountPage/index.js:
--------------------------------------------------------------------------------
1 | import AccountPage from './AccountPage'
2 |
3 | export default AccountPage
4 |
--------------------------------------------------------------------------------
/src/routes/Account/components/ProviderDataForm/ProviderDataForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import List from '@material-ui/core/List'
4 | import ListItem from '@material-ui/core/ListItem'
5 | import ListItemIcon from '@material-ui/core/ListItemIcon'
6 | import ListItemText from '@material-ui/core/ListItemText'
7 | import ListSubheader from '@material-ui/core/ListSubheader'
8 | import AccountCircle from '@material-ui/icons/AccountCircle'
9 |
10 | function ProviderData({ providerData }) {
11 | return (
12 | Accounts}>
13 | {providerData.map((providerAccount) => (
14 |
15 |
16 |
17 |
18 |
19 |
20 | ))}
21 |
22 | )
23 | }
24 |
25 | ProviderData.propTypes = {
26 | providerData: PropTypes.array.isRequired
27 | }
28 |
29 | export default ProviderData
30 |
--------------------------------------------------------------------------------
/src/routes/Account/components/ProviderDataForm/index.js:
--------------------------------------------------------------------------------
1 | import ProviderDataForm from './ProviderDataForm'
2 |
3 | export default ProviderDataForm
4 |
--------------------------------------------------------------------------------
/src/routes/Account/index.js:
--------------------------------------------------------------------------------
1 | import { loadable } from 'utils/router'
2 | import { ACCOUNT_PATH as path } from 'constants/paths'
3 |
4 | export default {
5 | path,
6 | authRequired: true,
7 | component: loadable(() =>
8 | import(/* webpackChunkName: 'Account' */ './components/AccountPage')
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionStepLocation/ActionStepLocation.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | field: theme.field
3 | })
4 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionStepLocation/index.js:
--------------------------------------------------------------------------------
1 | import ActionStepLocation from './ActionStepLocation'
2 |
3 | export default ActionStepLocation
4 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionTemplateBackups/ActionTemplateBackups.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | ...theme.flexColumn
4 | },
5 | field: theme.field,
6 | addAction: {
7 | marginTop: '.5rem',
8 | marginBottom: '1.5rem'
9 | },
10 | title: {
11 | fontSize: '1.2rem',
12 | flexBasis: '33.33%',
13 | flexShrink: '0'
14 | },
15 | sections: {
16 | ...theme.flexRow,
17 | alignItems: 'center'
18 | },
19 | deleteButton: {
20 | margin: '0px',
21 | padding: '0px'
22 | },
23 | delete: {
24 | ...theme.flexRow,
25 | justifyContent: 'flex-end'
26 | }
27 | })
28 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionTemplateBackups/index.js:
--------------------------------------------------------------------------------
1 | import ActionTemplateBackups from './ActionTemplateBackups'
2 |
3 | export default ActionTemplateBackups
4 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionTemplateEnvs/ActionTemplateEnvs.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | field: theme.field,
3 | addAction: {
4 | marginTop: '.5rem',
5 | marginBottom: '1.5rem'
6 | },
7 | title: {
8 | fontSize: '1.2rem',
9 | flexBasis: '33.33%',
10 | flexShrink: '0'
11 | },
12 | type: {
13 | color: 'rgba(0, 0, 0, 0.54)'
14 | },
15 | required: {
16 | marginTop: '2rem',
17 | maxWidth: '120px'
18 | },
19 | delete: {
20 | ...theme.flexRow,
21 | justifyContent: 'flex-end'
22 | },
23 | deleteButton: {
24 | alignSelf: 'flex-end'
25 | }
26 | })
27 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionTemplateEnvs/index.js:
--------------------------------------------------------------------------------
1 | import ActionTemplateEnvs from './ActionTemplateEnvs'
2 |
3 | export default ActionTemplateEnvs
4 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionTemplateForm/ActionTemplateForm.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | ...theme.flexColumn
4 | },
5 | field: theme.field,
6 | buttons: {
7 | ...theme.flexRow,
8 | justifyContent: 'flex-start',
9 | marginTop: '2rem',
10 | marginBottom: '2rem'
11 | },
12 | paper: {
13 | padding: '2rem',
14 | width: '60%' // TODO: Replace with grid
15 | },
16 | actions: {
17 | marginTop: '2rem'
18 | },
19 | header: {
20 | color: 'rgba(0, 0, 0, 0.54)',
21 | fontSize: '1.6rem',
22 | marginRight: '2rem'
23 | },
24 | addAction: {
25 | marginBottom: '2rem'
26 | },
27 | title: {
28 | fontSize: '1.2rem'
29 | },
30 | publicToggle: {
31 | maxWidth: '5rem',
32 | marginTop: '1.5rem',
33 | marginBottom: '0.5rem'
34 | },
35 | button: {
36 | margin: theme.spacing(),
37 | cursor: 'finger'
38 | }
39 | })
40 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionTemplateForm/index.js:
--------------------------------------------------------------------------------
1 | import ActionTemplateForm from './ActionTemplateForm'
2 |
3 | export default ActionTemplateForm
4 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionTemplateInputs/ActionTemplateInputs.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | ...theme.flexColumn
4 | },
5 | field: theme.field,
6 | addAction: {
7 | marginTop: '.5rem',
8 | marginBottom: '1.5rem'
9 | },
10 | title: {
11 | fontSize: '1.2rem',
12 | flexBasis: '33.33%',
13 | flexShrink: '0'
14 | },
15 | subcollectionOption: {
16 | marginTop: '1rem'
17 | },
18 | type: {
19 | color: 'rgba(0, 0, 0, 0.54)'
20 | },
21 | required: {
22 | marginTop: '2rem',
23 | maxWidth: '120px'
24 | },
25 | delete: {
26 | ...theme.flexRow,
27 | justifyContent: 'flex-end'
28 | },
29 | deleteButton: {
30 | alignSelf: 'flex-end'
31 | }
32 | })
33 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionTemplateInputs/index.js:
--------------------------------------------------------------------------------
1 | import ActionTemplateInputs from './ActionTemplateInputs'
2 |
3 | export default ActionTemplateInputs
4 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionTemplatePage/ActionTemplatePage.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | padding: '2rem'
4 | },
5 | paper: {
6 | padding: '2rem'
7 | },
8 | header: {
9 | color: 'rgba(0, 0, 0, 0.54)',
10 | fontSize: '2.25rem'
11 | },
12 | notFound: {
13 | ...theme.flexRowCenter,
14 | marginTop: '4rem'
15 | },
16 | notFoundText: {
17 | fontSize: '2rem'
18 | }
19 | })
20 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionTemplatePage/TemplateLoadingError.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Typography from '@material-ui/core/Typography'
4 | import { makeStyles } from '@material-ui/core/styles'
5 | import styles from './ActionTemplatePage.styles'
6 |
7 | const useStyles = makeStyles(styles)
8 |
9 | function TemplateLoadingError({ errorMessage }) {
10 | const classes = useStyles()
11 |
12 | return (
13 |
14 |
15 | Error loading templates: {errorMessage}
16 |
17 |
18 | )
19 | }
20 |
21 | TemplateLoadingError.propTypes = {
22 | errorMessage: PropTypes.string.isRequired
23 | }
24 |
25 | export default TemplateLoadingError
26 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionTemplatePage/TemplateNotFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Typography from '@material-ui/core/Typography'
3 | import { makeStyles } from '@material-ui/core/styles'
4 | import styles from './ActionTemplatePage.styles'
5 |
6 | const useStyles = makeStyles(styles)
7 |
8 | function TemplateNotFound() {
9 | const classes = useStyles()
10 |
11 | return (
12 |
13 |
14 | Template Not Found
15 |
16 |
17 | )
18 | }
19 |
20 | export default TemplateNotFound
21 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionTemplatePage/index.js:
--------------------------------------------------------------------------------
1 | import ActionTemplatePage from './ActionTemplatePage'
2 |
3 | export default ActionTemplatePage
4 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionTemplateSteps/ActionTemplateSteps.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | ...theme.flexColumn
4 | },
5 | field: theme.field,
6 | multilineField: theme.multilineField,
7 | alignCenter: {
8 | textAlign: 'center'
9 | },
10 | content: {
11 | ...theme.flexColumn
12 | },
13 | buttons: {
14 | ...theme.flexRow,
15 | justifyContent: 'flex-start',
16 | marginTop: '2rem',
17 | marginBottom: '2rem'
18 | },
19 | removeButton: {
20 | ...theme.flexRow,
21 | justifyContent: 'flex-end',
22 | alignSelf: 'flex-end',
23 | padding: '0px',
24 | width: '100%'
25 | },
26 | paper: {
27 | padding: '2rem',
28 | width: '60%' // TODO: Replace with grid
29 | },
30 | actions: {
31 | marginTop: '2rem'
32 | },
33 | header: {
34 | color: 'rgba(0, 0, 0, 0.54)',
35 | fontSize: '1.6rem',
36 | marginRight: '2rem'
37 | },
38 | addAction: {
39 | marginTop: '.5rem',
40 | marginBottom: '1.5rem'
41 | },
42 | title: {
43 | fontSize: '1.2rem'
44 | },
45 | subcollectionOption: {
46 | marginTop: '1rem'
47 | },
48 | delete: {
49 | ...theme.flexRow,
50 | justifyContent: 'flex-end'
51 | }
52 | })
53 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/ActionTemplateSteps/index.js:
--------------------------------------------------------------------------------
1 | import ActionTemplateSteps from './ActionTemplateSteps'
2 |
3 | export default ActionTemplateSteps
4 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/DeleteTemplateDialog/DeleteTemplateDialog.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Dialog from '@material-ui/core/Dialog'
4 | import DialogTitle from '@material-ui/core/DialogTitle'
5 | import DialogActions from '@material-ui/core/DialogActions'
6 | import DialogContent from '@material-ui/core/DialogContent'
7 | import Button from '@material-ui/core/Button'
8 |
9 | function DeleteTemplateDialog({ onClose, onDeleteClick, open, templateName }) {
10 | return (
11 |
22 | )
23 | }
24 |
25 | DeleteTemplateDialog.propTypes = {
26 | onClose: PropTypes.func.isRequired,
27 | onDeleteClick: PropTypes.func.isRequired,
28 | open: PropTypes.bool.isRequired,
29 | templateName: PropTypes.string
30 | }
31 |
32 | export default DeleteTemplateDialog
33 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/components/DeleteTemplateDialog/index.js:
--------------------------------------------------------------------------------
1 | import DeleteTemplateDialog from './DeleteTemplateDialog'
2 |
3 | export default DeleteTemplateDialog
4 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplate/index.js:
--------------------------------------------------------------------------------
1 | import { loadable } from 'utils/router'
2 | import { ACTION_TEMPLATES_PATH } from 'constants/paths'
3 |
4 | export default {
5 | path: `${ACTION_TEMPLATES_PATH}/:templateId`,
6 | component: loadable(() =>
7 | import(
8 | /* webpackChunkName: 'ActionTemplate' */ './components/ActionTemplatePage'
9 | )
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplates/components/ActionTemplateListCard/ActionTemplateListCard.styles.js:
--------------------------------------------------------------------------------
1 | import red from '@material-ui/core/colors/red'
2 |
3 | export default (theme) => ({
4 | card: {
5 | maxWidth: 400,
6 | height: '14rem'
7 | },
8 | cardTitle: {
9 | cursor: 'pointer',
10 | color: theme.palette.text.primary
11 | },
12 | media: {
13 | height: 194,
14 | overflow: 'hidden',
15 | textOverflow: 'ellipsis',
16 | wordWrap: 'no-wrap'
17 | },
18 | expand: {
19 | transform: 'rotate(0deg)',
20 | transition: theme.transitions.create('transform', {
21 | duration: theme.transitions.duration.shortest
22 | })
23 | },
24 | expandOpen: {
25 | transform: 'rotate(180deg)'
26 | },
27 | avatar: {
28 | backgroundColor: red[500]
29 | },
30 | flexGrow: {
31 | flex: '1 1 auto'
32 | }
33 | })
34 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplates/components/ActionTemplateListCard/index.js:
--------------------------------------------------------------------------------
1 | import ActionTemplateListCard from './ActionTemplateListCard'
2 |
3 | export default ActionTemplateListCard
4 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplates/components/ActionTemplatesList/ActionTemplatesList.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | flexGrow: 1
4 | },
5 | new: {
6 | marginBottom: '2rem'
7 | },
8 | sectionHeader: {
9 | color: 'rgba(0, 0, 0, 0.54)',
10 | fontSize: '1.6rem',
11 | fontWeight: '300',
12 | marginTop: '2rem',
13 | marginBottom: '2rem'
14 | }
15 | })
16 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplates/components/ActionTemplatesList/index.js:
--------------------------------------------------------------------------------
1 | import ActionTemplatesList from './ActionTemplatesList'
2 |
3 | export default ActionTemplatesList
4 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplates/components/ActionTemplatesPage/ActionTemplatesPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Typography from '@material-ui/core/Typography'
3 | import ActionTemplatesList from '../ActionTemplatesList'
4 | import { makeStyles } from '@material-ui/core/styles'
5 | import styles from './ActionTemplatesPage.styles'
6 |
7 | const useStyles = makeStyles(styles)
8 |
9 | function ActionTemplatesPage() {
10 | const classes = useStyles()
11 | return (
12 |
13 |
Action Templates
14 |
15 |
16 | )
17 | }
18 |
19 | export default ActionTemplatesPage
20 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplates/components/ActionTemplatesPage/ActionTemplatesPage.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | ...theme.flexColumn,
4 | justifyContent: 'flex-start',
5 | padding: '2rem',
6 | paddingTop: '30px',
7 | flexGrow: '2',
8 | boxSizing: 'border-box',
9 | overflowY: 'scroll'
10 | },
11 | header: {
12 | color: 'rgba(0, 0, 0, 0.54)',
13 | fontSize: '2.25rem',
14 | alignSelf: 'flex-start',
15 | marginBottom: '2rem'
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplates/components/ActionTemplatesPage/index.js:
--------------------------------------------------------------------------------
1 | import ActionTemplatesPage from './ActionTemplatesPage'
2 |
3 | export default ActionTemplatesPage
4 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplates/components/NewActionTemplateDialog/NewActionTemplateDialog.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | content: {
3 | minHeight: '400px',
4 | minWidth: '600px'
5 | }
6 | })
7 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplates/components/NewActionTemplateDialog/index.js:
--------------------------------------------------------------------------------
1 | import NewActionTemplateDialog from './NewActionTemplateDialog'
2 |
3 | export default NewActionTemplateDialog
4 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplates/components/PrivateActionTemplates/PrivateActionTemplates.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | flexGrow: 1
4 | },
5 | new: {
6 | marginBottom: '2rem'
7 | },
8 | sectionHeader: {
9 | color: 'rgba(0, 0, 0, 0.54)',
10 | fontSize: '1.6rem',
11 | fontWeight: '300',
12 | marginTop: '2rem',
13 | marginBottom: '2rem'
14 | }
15 | })
16 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplates/components/PrivateActionTemplates/index.js:
--------------------------------------------------------------------------------
1 | import PrivateActionTemplates from './PrivateActionTemplates'
2 |
3 | export default PrivateActionTemplates
4 |
--------------------------------------------------------------------------------
/src/routes/ActionTemplates/index.js:
--------------------------------------------------------------------------------
1 | import { loadable } from 'utils/router'
2 | import { ACTION_TEMPLATES_PATH as path } from 'constants/paths'
3 |
4 | export default {
5 | path,
6 | component: loadable(() =>
7 | import(
8 | /* webpackChunkName: 'ActionTemplates' */ './components/ActionTemplatesPage'
9 | )
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/routes/Home/components/HomePage/HomePage.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | paddingTop: '4rem',
4 | overflowY: 'scroll',
5 | '-webkit-overflow-scrolling': 'touch'
6 | },
7 | paper: {
8 | textAlign: 'center',
9 | // margin: '1rem',
10 | padding: '3rem'
11 | },
12 | section: {
13 | // ...theme.flexColumn,
14 | // margin: '1rem'
15 | },
16 | disclaimer: {
17 | marginBottom: '2rem',
18 | textAlign: 'center'
19 | },
20 | getStarted: {
21 | marginBottom: theme.spacing(4)
22 | },
23 | templatesButton: {
24 | marginTop: theme.spacing(2)
25 | }
26 | })
27 |
--------------------------------------------------------------------------------
/src/routes/Home/components/HomePage/index.js:
--------------------------------------------------------------------------------
1 | import HomePage from './HomePage'
2 |
3 | export default HomePage
4 |
--------------------------------------------------------------------------------
/src/routes/Home/index.js:
--------------------------------------------------------------------------------
1 | import HomePage from './components/HomePage'
2 |
3 | // Sync route definition
4 | export default {
5 | path: '/',
6 | component: HomePage
7 | }
8 |
--------------------------------------------------------------------------------
/src/routes/Login/components/LoginPage/LoginPage.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | ...theme.flexColumnCenter,
4 | justifyContent: 'flex-start',
5 | height: '100%',
6 | width: '100%',
7 | fontWeight: 400,
8 | paddingTop: '1.5rem'
9 | },
10 | panel: {
11 | ...theme.flexColumnCenter,
12 | justifyContent: 'center',
13 | padding: '1.25rem',
14 | minWidth: '4rem'
15 | },
16 | orLabel: {
17 | marginTop: '1rem',
18 | marginBottom: '.5rem'
19 | },
20 | signup: {
21 | ...theme.flexColumnCenter,
22 | justifyContent: 'center',
23 | marginTop: '2rem'
24 | },
25 | signupLabel: {
26 | fontSize: '1rem',
27 | fontWeight: 'bold'
28 | },
29 | signupLink: {
30 | fontSize: '1.2rem'
31 | },
32 | providers: {
33 | marginTop: '1rem'
34 | }
35 | })
36 |
--------------------------------------------------------------------------------
/src/routes/Login/components/LoginPage/index.js:
--------------------------------------------------------------------------------
1 | import LoginPage from './LoginPage'
2 |
3 | export default LoginPage
4 |
--------------------------------------------------------------------------------
/src/routes/Login/index.js:
--------------------------------------------------------------------------------
1 | import { loadable } from 'utils/router'
2 | import { LOGIN_PATH as path } from 'constants/paths'
3 |
4 | export default {
5 | path,
6 | component: loadable(() =>
7 | import(/* webpackChunkName: 'Login' */ './components/LoginPage')
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/src/routes/NotFound/components/NotFoundPage/NotFoundPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { makeStyles } from '@material-ui/core/styles'
3 | import styles from './NotFoundPage.styles'
4 |
5 | const useStyles = makeStyles(styles)
6 |
7 | function NotFoundPage() {
8 | const classes = useStyles()
9 |
10 | return (
11 |
12 |
Whoops! 404!
13 |
This page was not found.
14 |
15 | )
16 | }
17 |
18 | export default NotFoundPage
19 |
--------------------------------------------------------------------------------
/src/routes/NotFound/components/NotFoundPage/NotFoundPage.styles.js:
--------------------------------------------------------------------------------
1 | export default () => ({
2 | root: {
3 | display: 'flex',
4 | flexDirection: 'column',
5 | alignItems: 'center'
6 | }
7 | })
8 |
--------------------------------------------------------------------------------
/src/routes/NotFound/components/NotFoundPage/index.js:
--------------------------------------------------------------------------------
1 | import NotFoundPage from './NotFoundPage'
2 |
3 | export default NotFoundPage
4 |
--------------------------------------------------------------------------------
/src/routes/NotFound/index.js:
--------------------------------------------------------------------------------
1 | import { loadable } from 'utils/router'
2 |
3 | export default {
4 | component: loadable(() =>
5 | import(/* webpackChunkName: 'NotFound' */ './components/NotFoundPage')
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/src/routes/Projects/components/NewProjectDialog/NewProjectDialog.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | padding: theme.spacing(2)
4 | },
5 | inputs: {
6 | ...theme.flexColumnCenter
7 | },
8 | buttons: {
9 | ...theme.flexColumnCenter
10 | }
11 | })
12 |
--------------------------------------------------------------------------------
/src/routes/Projects/components/NewProjectDialog/index.js:
--------------------------------------------------------------------------------
1 | import NewProjectDialog from './NewProjectDialog'
2 |
3 | export default NewProjectDialog
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/components/NewProjectTile/NewProjectTile.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import ContentAddCircle from '@material-ui/icons/AddCircle'
4 | import Paper from '@material-ui/core/Paper'
5 | import { makeStyles } from '@material-ui/core/styles'
6 | import styles from './NewProjectTile.styles'
7 |
8 | const iconSize = '6rem'
9 | const useStyles = makeStyles(styles)
10 |
11 | function NewProjectTile({ onClick }) {
12 | const classes = useStyles()
13 |
14 | return (
15 |
19 |
20 |
21 | )
22 | }
23 |
24 | NewProjectTile.propTypes = {
25 | onClick: PropTypes.func.isRequired
26 | }
27 |
28 | export default NewProjectTile
29 |
--------------------------------------------------------------------------------
/src/routes/Projects/components/NewProjectTile/NewProjectTile.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | ...theme.flexRowCenter,
4 | alignItems: 'center',
5 | cursor: 'pointer',
6 | height: '200px',
7 | width: '300px',
8 | color: '#757575',
9 | margin: theme.spacing(0.5),
10 | padding: theme.spacing(1.3),
11 | overflow: 'hidden'
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/src/routes/Projects/components/NewProjectTile/index.js:
--------------------------------------------------------------------------------
1 | import NewProjectTile from './NewProjectTile'
2 |
3 | export default NewProjectTile
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/components/ProjectTile/index.js:
--------------------------------------------------------------------------------
1 | import ProjectTile from './ProjectTile'
2 |
3 | export default ProjectTile
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/components/ProjectsList/ProjectsList.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | display: 'flex',
4 | justifyContent: 'center',
5 | flexWrap: 'wrap',
6 | '-webkit-flex-flow': 'row wrap'
7 | }
8 | })
9 |
--------------------------------------------------------------------------------
/src/routes/Projects/components/ProjectsList/index.js:
--------------------------------------------------------------------------------
1 | import ProjectsList from './ProjectsList'
2 |
3 | export default ProjectsList
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/components/ProjectsPage/ProjectsPage.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | ...theme.flexColumnCenter,
4 | paddingTop: theme.spacing(4),
5 | flexGrow: '2',
6 | boxSizing: 'border-box',
7 | overflowY: 'scroll'
8 | },
9 | tiles: {
10 | display: 'flex',
11 | justifyContent: 'center',
12 | flexWrap: 'wrap',
13 | '-webkit-flex-flow': 'row wrap'
14 | }
15 | })
16 |
--------------------------------------------------------------------------------
/src/routes/Projects/components/ProjectsPage/index.js:
--------------------------------------------------------------------------------
1 | import ProjectsPage from './ProjectsPage'
2 |
3 | export default ProjectsPage
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/components/SharingDialog/SharingDialog.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | content: {
3 | ...theme.flexColumnCenter,
4 | width: '30rem'
5 | },
6 | search: {
7 | marginTop: '0rem'
8 | },
9 | current: {
10 | marginBottom: '2rem',
11 | ...theme.flexColumnCenter
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/src/routes/Projects/components/SharingDialog/index.js:
--------------------------------------------------------------------------------
1 | import SharingDialog from './SharingDialog'
2 |
3 | export default SharingDialog
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/index.js:
--------------------------------------------------------------------------------
1 | import { LIST_PATH as path } from 'constants/paths'
2 | import { loadable } from 'utils/router'
3 |
4 | export default {
5 | path,
6 | authRequired: true,
7 | component: loadable(() =>
8 | import(/* webpackChunkName: 'Projects' */ './components/ProjectsPage')
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/components/OverviewPanel/OverviewPanel.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | ...theme.flexColumnCenter,
4 | ...theme.mixins.gutters(),
5 | padding: theme.spacing(2),
6 | width: '90%',
7 | passing: theme.spacing(1.5),
8 | minHeight: '20rem'
9 | },
10 | name: {
11 | color: '#212121',
12 | fontSize: '2.75rem',
13 | fontWeight: '300'
14 | },
15 | description: {
16 | marginBottom: '2rem'
17 | },
18 | item: {
19 | ...theme.flexColumnCenter,
20 | justifyContent: 'space-between',
21 | textAlign: 'center'
22 | },
23 | environmentsLabel: {
24 | marginRight: '.5rem',
25 | display: 'inline'
26 | },
27 | environmentsNumber: {
28 | display: 'inline'
29 | }
30 | })
31 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/components/OverviewPanel/index.js:
--------------------------------------------------------------------------------
1 | import OverviewPanel from './OverviewPanel'
2 |
3 | export default OverviewPanel
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/components/ProjectPage/ProjectErrorPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Paper from '@material-ui/core/Paper'
4 | import { makeStyles } from '@material-ui/core/styles'
5 |
6 | const useStyles = makeStyles((theme) => ({
7 | errorPage: {
8 | ...theme.flexColumnCenter,
9 | marginTop: theme.spacing(4),
10 | fontSize: '1.8rem',
11 | width: '80%',
12 | marginLeft: '10%'
13 | },
14 | errorPaper: {
15 | ...theme.flexColumnCenter,
16 | padding: theme.spacing(2),
17 | width: '60%'
18 | },
19 | errorMessage: {
20 | ...theme.flexColumnCenter,
21 | padding: theme.spacing(2),
22 | width: '60%'
23 | }
24 | }))
25 |
26 | function ProjectErrorPage({ errorMessage }) {
27 | const classes = useStyles()
28 | return (
29 |
30 |
31 | Error Loading Project
32 | {errorMessage}
33 |
34 |
35 | )
36 | }
37 |
38 | ProjectErrorPage.propTypes = {
39 | errorMessage: PropTypes.string.isRequired
40 | }
41 |
42 | export default ProjectErrorPage
43 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/components/ProjectPage/ProjectNotFoundPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { makeStyles } from '@material-ui/core/styles'
3 |
4 | const useStyles = makeStyles((theme) => ({
5 | root: {
6 | ...theme.flexColumnCenter
7 | }
8 | }))
9 |
10 | function ProjectNotFoundPage() {
11 | const classes = useStyles()
12 | return Project Not Found
13 | }
14 |
15 | export default ProjectNotFoundPage
16 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/components/ProjectPage/ProjectPage.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | ...theme.flexColumnCenter,
4 | marginBottom: theme.spacing(4)
5 | },
6 | pageHeader: theme.pageHeader
7 | })
8 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/components/ProjectPage/index.js:
--------------------------------------------------------------------------------
1 | import ProjectPage from './ProjectPage'
2 |
3 | export default ProjectPage
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/index.js:
--------------------------------------------------------------------------------
1 | import { loadable } from 'utils/router'
2 |
3 | export default {
4 | path: ':projectId',
5 | component: loadable(() =>
6 | import(/* webpackChunkName: 'ProjectPage' */ './components/ProjectPage')
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Actions/components/ActionsPage/ActionPage.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | container: {
3 | overflowY: 'scroll',
4 | paddingBottom: '4rem',
5 | padding: '.125rem'
6 | },
7 | pageHeader: theme.pageHeader,
8 | button: {
9 | marginLeft: '2rem'
10 | },
11 | buttons: {
12 | ...theme.flexRow,
13 | marginBottom: '2rem',
14 | marginLeft: '.75rem'
15 | },
16 | or: {
17 | ...theme.flexRowCenter,
18 | marginTop: '1rem',
19 | marginBottom: '1rem'
20 | },
21 | orFont: {
22 | fontSize: '1.3rem'
23 | },
24 | search: {
25 | // ...theme.flexRowCenter,
26 | marginTop: theme.spacing(6)
27 | },
28 | sectionHeader: {
29 | display: 'inline-block'
30 | },
31 | paperHeader: {
32 | color: theme.palette.text.secondary,
33 | textAlign: 'center',
34 | paddingBottom: 0,
35 | marginBottom: 0
36 | },
37 | paperHeaderText: {
38 | color: theme.palette.text.secondary,
39 | paddingBottom: 0
40 | },
41 | paper: {
42 | padding: theme.spacing(2),
43 | textAlign: 'center',
44 | color: theme.palette.text.secondary
45 | },
46 | helpIcon: {
47 | marginBottom: theme.spacing(1),
48 | marginLeft: theme.spacing(1)
49 | }
50 | })
51 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Actions/components/ActionsPage/index.js:
--------------------------------------------------------------------------------
1 | import ActionsPage from './ActionsPage'
2 |
3 | export default ActionsPage
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Actions/components/ActionsRunnerForm/ActionsRunnerForm.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | marginTop: '.75rem'
4 | },
5 | button: {
6 | marginLeft: '2rem'
7 | },
8 | buttons: {
9 | ...theme.flexRow,
10 | justifyContent: 'flex-end',
11 | marginTop: '2rem',
12 | marginBottom: '2rem',
13 | marginLeft: '.75rem'
14 | }
15 | })
16 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Actions/components/ActionsRunnerForm/index.js:
--------------------------------------------------------------------------------
1 | import ActionsRunnerForm from './ActionsRunnerForm'
2 |
3 | export default ActionsRunnerForm
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Actions/components/PrivateActionTemplates/NoTemplatesFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { makeStyles } from '@material-ui/core/styles'
3 | import styles from './PrivateActionTemplates.styles'
4 |
5 | const useStyles = makeStyles(styles)
6 |
7 | function NoTemplatesFound() {
8 | const classes = useStyles()
9 |
10 | return No Private Templates Found
11 | }
12 |
13 | export default NoTemplatesFound
14 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Actions/components/PrivateActionTemplates/PrivateActionTemplates.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Divider from '@material-ui/core/Divider'
4 | import List from '@material-ui/core/List'
5 | import ListItem from '@material-ui/core/ListItem'
6 | import ListItemText from '@material-ui/core/ListItemText'
7 | import { makeStyles } from '@material-ui/core/styles'
8 | import { useFirestore, useUser, useFirestoreCollectionData } from 'reactfire'
9 | import styles from './PrivateActionTemplates.styles'
10 | import NoTemplatesFound from './NoTemplatesFound'
11 |
12 | const useStyles = makeStyles(styles)
13 |
14 | function PrivateActionTemplates({ onTemplateClick }) {
15 | const classes = useStyles()
16 | const firestore = useFirestore()
17 | const user = useUser()
18 | const actionTemplatesQuery = firestore
19 | .collection('actionTemplates')
20 | .where('createdBy', '==', user.uid)
21 | .where('public', '==', false)
22 | const templates = useFirestoreCollectionData(actionTemplatesQuery, {
23 | idField: 'id'
24 | })
25 |
26 | if (!templates.length) {
27 | return
28 | }
29 |
30 | return (
31 |
32 |
33 |
34 | {templates.map((item, idx) => [
35 | onTemplateClick(item)}>
39 |
40 | ,
41 |
42 | ])}
43 |
44 |
45 | )
46 | }
47 |
48 | PrivateActionTemplates.propTypes = {
49 | onTemplateClick: PropTypes.func.isRequired
50 | }
51 |
52 | export default PrivateActionTemplates
53 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Actions/components/PrivateActionTemplates/PrivateActionTemplates.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | ...theme.flexColumnCenter
4 | }
5 | })
6 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Actions/components/PrivateActionTemplates/index.js:
--------------------------------------------------------------------------------
1 | import PrivateActionTemplates from './PrivateActionTemplates'
2 |
3 | export default PrivateActionTemplates
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Actions/components/RecentActions/NoRecentActions.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Paper from '@material-ui/core/Paper'
3 | import { makeStyles } from '@material-ui/core/styles'
4 | import styles from './RecentActions.styles'
5 |
6 | const useStyles = makeStyles(styles)
7 |
8 | export default function NoRecentActions() {
9 | const classes = useStyles()
10 | return (
11 |
12 | No Recent Actions Found
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Actions/components/RecentActions/RecentActions.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | // paddingLeft: '.25rem',
4 | // paddingRight: '.25rem'
5 | },
6 | appFrame: {
7 | ...theme.flexRowCenter,
8 | padding: '2rem',
9 | fontSize: '1.4rem'
10 | },
11 | empty: {
12 | textAlign: 'center',
13 | padding: '3rem'
14 | }
15 | })
16 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Actions/components/RecentActions/index.js:
--------------------------------------------------------------------------------
1 | import RecentActions from './RecentActions'
2 |
3 | export default RecentActions
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Actions/components/StepsViewer/StepsViewer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Stepper from '@material-ui/core/Stepper'
4 | import Step from '@material-ui/core/Step'
5 | import StepLabel from '@material-ui/core/StepLabel'
6 | import StepContent from '@material-ui/core/StepContent'
7 | import Typography from '@material-ui/core/Typography'
8 |
9 | function StepsViewer({ steps, activeStep, disabled, watch }) {
10 | function convertEnv(step, name) {
11 | const { pathType, path: stepPath } = (step && step[name]) || {}
12 | if (pathType === 'input') {
13 | const inputValues = watch('inputValues')
14 | return inputValues && inputValues[stepPath]
15 | }
16 | return stepPath
17 | }
18 | return (
19 |
20 | {steps.map((step, index) => {
21 | return (
22 |
23 | {step.name || 'No Name'}
24 |
25 | Type: {step.type}
26 | {step.description}
27 | Source: {convertEnv(step, 'src')}
28 | Destination: {convertEnv(step, 'dest')}
29 |
30 |
31 | )
32 | })}
33 |
34 | )
35 | }
36 |
37 | StepsViewer.propTypes = {
38 | steps: PropTypes.array.isRequired,
39 | watch: PropTypes.func.isRequired,
40 | activeStep: PropTypes.number.isRequired,
41 | disabled: PropTypes.bool
42 | }
43 |
44 | export default StepsViewer
45 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Actions/components/StepsViewer/index.js:
--------------------------------------------------------------------------------
1 | import StepsViewer from './StepsViewer'
2 |
3 | export default StepsViewer
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Actions/index.js:
--------------------------------------------------------------------------------
1 | import { loadable } from 'utils/router'
2 | import { PROJECT_ACTION_PATH as path } from 'constants/paths'
3 |
4 | export default {
5 | path,
6 | component: loadable(() =>
7 | import(/* webpackChunkName: 'ProjectActions' */ './components/ActionsPage')
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/BucketConfig/components/BucketConfigForm/BucketConfigForm.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | container: {
3 | width: '80%'
4 | },
5 | paper: {
6 | marginTop: '2rem',
7 | width: '100%',
8 | padding: '2rem'
9 | },
10 | button: {
11 | marginRight: '1rem'
12 | },
13 | buttons: {
14 | width: '100%'
15 | },
16 | subHeader: {
17 | color: 'rgba(0, 0, 0, 0.54)'
18 | },
19 | formItem: {
20 | width: '100%'
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/BucketConfig/components/BucketConfigForm/index.js:
--------------------------------------------------------------------------------
1 | import BucketConfigForm from './BucketConfigForm'
2 |
3 | export default BucketConfigForm
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/BucketConfig/components/BucketConfigPage/BucketConfigPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Typography from '@material-ui/core/Typography'
4 | import { makeStyles } from '@material-ui/core/styles'
5 | import BucketConfigForm from '../BucketConfigForm'
6 | import styles from './BucketConfigPage.styles'
7 |
8 | const useStyles = makeStyles(styles)
9 |
10 | function BucketConfigPage({ project, projectId }) {
11 | const classes = useStyles()
12 |
13 | return (
14 |
15 |
16 | Storage Bucket Configuration
17 |
18 |
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | BucketConfigPage.propTypes = {
26 | project: PropTypes.object.isRequired,
27 | projectId: PropTypes.string.isRequired
28 | }
29 |
30 | export default BucketConfigPage
31 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/BucketConfig/components/BucketConfigPage/BucketConfigPage.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | container: {
3 | marginBottom: '3rem'
4 | },
5 | pageHeader: theme.pageHeader,
6 | subHeader: theme.subHeader
7 | })
8 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/BucketConfig/components/BucketConfigPage/index.js:
--------------------------------------------------------------------------------
1 | import BucketConfigPage from './BucketConfigPage'
2 |
3 | export default BucketConfigPage
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/BucketConfig/components/CorsList/CorsList.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | header: {
3 | display: 'flex',
4 | justifyContent: 'space-between'
5 | },
6 | add: {
7 | ...theme.flexRowCenter,
8 | marginBottom: '2rem',
9 | marginTop: '2rem',
10 | width: '100%'
11 | },
12 | item: {
13 | padding: '2rem',
14 | margin: '1rem'
15 | },
16 | subHeader: {
17 | color: ' rgba(0, 0, 0, 0.54)',
18 | fontSize: '.9rem'
19 | }
20 | })
21 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/BucketConfig/components/CorsList/index.js:
--------------------------------------------------------------------------------
1 | import CorsList from './CorsList'
2 |
3 | export default CorsList
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/BucketConfig/components/CorsOriginList/CorsOriginList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { useFormContext, useFieldArray } from 'react-hook-form'
4 | import IconButton from '@material-ui/core/IconButton'
5 | import Button from '@material-ui/core/Button'
6 | import TextField from '@material-ui/core/TextField'
7 | import DeleteIcon from '@material-ui/icons/Delete'
8 | import { makeStyles } from '@material-ui/core/styles'
9 | import styles from './CorsOriginList.styles'
10 |
11 | const useStyles = makeStyles(styles)
12 |
13 | function CorsOriginList({ name }) {
14 | const classes = useStyles()
15 | const { control, register } = useFormContext()
16 | const { fields, remove, append } = useFieldArray({ control, name })
17 | return (
18 |
19 | {fields.map((item, index) => (
20 |
21 |
28 | {index !== 0 && (
29 | remove(index)}
31 | style={{ marginTop: '1.5rem' }}>
32 |
33 |
34 | )}
35 |
36 | ))}
37 |
38 |
41 |
42 |
43 | )
44 | }
45 |
46 | CorsOriginList.propTypes = {
47 | name: PropTypes.string
48 | }
49 |
50 | export default CorsOriginList
51 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/BucketConfig/components/CorsOriginList/CorsOriginList.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | add: {
3 | ...theme.flexRowCenter
4 | },
5 | originItem: {
6 | display: 'flex'
7 | },
8 | originHeader: {
9 | fontSize: '.9rem'
10 | }
11 | })
12 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/BucketConfig/components/CorsOriginList/index.js:
--------------------------------------------------------------------------------
1 | import CorsOriginList from './CorsOriginList'
2 |
3 | export default CorsOriginList
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/BucketConfig/index.js:
--------------------------------------------------------------------------------
1 | import { loadable } from 'utils/router'
2 | import { PROJECT_BUCKET_CONFIG_PATH as path } from 'constants/paths'
3 |
4 | export default {
5 | path,
6 | component: loadable(() =>
7 | import(
8 | /* webpackChunkName: 'BucketConfig' */ './components/BucketConfigPage'
9 | )
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Environments/components/AddEnvironmentDialog/AddEnvironmentDialog.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | body: {
3 | ...theme.flexColumn,
4 | alignItems: 'center',
5 | padding: '3rem',
6 | minWidth: '400px'
7 | },
8 | serviceAccounts: {
9 | marginTop: '1rem',
10 | marginBottom: '1rem',
11 | minHeight: '10rem'
12 | },
13 | buttons: {
14 | ...theme.flexRow,
15 | justifyContent: 'flex-end',
16 | flexGrow: '1',
17 | width: '100%',
18 | marginTop: '2rem',
19 | marginBottom: '1rem',
20 | paddingRight: '1rem'
21 | },
22 | inputs: {
23 | marginBottom: '1rem'
24 | }
25 | })
26 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Environments/components/AddEnvironmentDialog/index.js:
--------------------------------------------------------------------------------
1 | import AddEnvironmentDialog from './AddEnvironmentDialog'
2 |
3 | export default AddEnvironmentDialog
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Environments/components/DeleteEnvironmentDialog/DeleteEnvironmentDialog.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Dialog from '@material-ui/core/Dialog'
4 | import DialogTitle from '@material-ui/core/DialogTitle'
5 | import DialogActions from '@material-ui/core/DialogActions'
6 | import DialogContent from '@material-ui/core/DialogContent'
7 | import Button from '@material-ui/core/Button'
8 |
9 | function DeleteEnvironmentDialog({ onSubmit, onRequestClose, open }) {
10 | return (
11 |
31 | )
32 | }
33 |
34 | DeleteEnvironmentDialog.propTypes = {
35 | onRequestClose: PropTypes.func.isRequired,
36 | open: PropTypes.bool.isRequired,
37 | onSubmit: PropTypes.func.isRequired
38 | }
39 |
40 | export default DeleteEnvironmentDialog
41 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Environments/components/DeleteEnvironmentDialog/index.js:
--------------------------------------------------------------------------------
1 | import DeleteEnvironmentDialog from './DeleteEnvironmentDialog'
2 |
3 | export default DeleteEnvironmentDialog
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Environments/components/EditEnvironmentDialog/EditEnvironmentDialog.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | card: {
3 | marginLeft: '1rem',
4 | marginRight: '1rem',
5 | marginBottom: '2rem',
6 | width: '15rem',
7 | height: '150px',
8 | textOverflow: 'ellipsis'
9 | },
10 | title: {
11 | cursor: 'pointer'
12 | },
13 | settings: {
14 | marginTop: '2rem'
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Environments/components/EditEnvironmentDialog/index.js:
--------------------------------------------------------------------------------
1 | import EditEnvironmentDialog from './EditEnvironmentDialog'
2 |
3 | export default EditEnvironmentDialog
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Environments/components/EnvironmentsPage/EnvironmentsPage.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | container: {
3 | ...theme.flexColumn,
4 | width: '70%',
5 | marginLeft: '15%',
6 | paddingTop: '5rem',
7 | marginBottom: '4rem'
8 | },
9 | pageHeader: theme.pageHeader,
10 | empty: {
11 | ...theme.flexRowCenter,
12 | paddingTop: '5rem'
13 | },
14 | paper: {
15 | ...theme.flexColumn,
16 | width: '100%',
17 | padding: '2rem',
18 | marginBottom: '1rem'
19 | },
20 | instances: {
21 | ...theme.flexRowCenter,
22 | justifyContent: 'center',
23 | paddingBottom: '4rem',
24 | '-webkit-flex-flow': 'row wrap',
25 | flexWrap: 'wrap',
26 | padding: '2rem',
27 | marginBottom: '1rem'
28 | },
29 | paragraph: {
30 | marginTop: '1rem',
31 | marginBottom: '1rem',
32 | fontSize: '1rem'
33 | }
34 | })
35 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Environments/components/EnvironmentsPage/index.js:
--------------------------------------------------------------------------------
1 | import EnvironmentsPage from './EnvironmentsPage'
2 |
3 | export default EnvironmentsPage
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Environments/components/FilesUploader/FilesUploader.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | ...theme.flexColumnCenter,
4 | flexBasis: '80%',
5 | width: '100%',
6 | marginBottom: '.75rem',
7 | marginTop: '1rem'
8 | },
9 | dropzone: {
10 | ...theme.flexColumnCenter,
11 | justifyContent: 'center',
12 | flexWrap: 'wrap',
13 | width: '100%',
14 | cursor: 'pointer',
15 | minHeight: '5rem',
16 | border: '2px dashed grey',
17 | borderRadius: '2px',
18 | padding: '2rem'
19 | },
20 | dropzoneTitle: {
21 | marginTop: '.875rem',
22 | marginBottom: '.875rem',
23 | textAlign: 'center',
24 | fontWeight: '700',
25 | fontSize: '1.5rem'
26 | },
27 | dropzoneText: {
28 | fontWeight: '300',
29 | textAlign: 'center',
30 | fontSize: '1.09375rem'
31 | },
32 | icon: {
33 | width: '4.625rem',
34 | height: '2.75rem'
35 | },
36 | iconCompact: {
37 | width: '3.75rem',
38 | height: '2.5rem',
39 | marginRight: '.5rem'
40 | }
41 | })
42 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Environments/components/FilesUploader/index.js:
--------------------------------------------------------------------------------
1 | import FilesUploader from './FilesUploader'
2 |
3 | export default FilesUploader
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Environments/components/Instance/Instance.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | card: {
3 | marginLeft: '1rem',
4 | marginRight: '1rem',
5 | marginBottom: '2rem',
6 | width: '16rem',
7 | height: '160px',
8 | textOverflow: 'ellipsis'
9 | },
10 | title: {
11 | cursor: 'pointer',
12 | color: 'black'
13 | },
14 | settings: {
15 | marginTop: '2rem'
16 | },
17 | content: {
18 | paddingBottom: '2rem'
19 | }
20 | })
21 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Environments/components/Instance/index.js:
--------------------------------------------------------------------------------
1 | import Instance from './Instance'
2 |
3 | export default Instance
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Environments/index.js:
--------------------------------------------------------------------------------
1 | import { loadable } from 'utils/router'
2 | import { PROJECT_ENVIRONMENTS_PATH as path } from 'constants/paths'
3 |
4 | export default {
5 | path,
6 | component: loadable(() =>
7 | import(
8 | /* webpackChunkName: 'ProjectEnvironments' */ './components/EnvironmentsPage'
9 | )
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/DeleteMemberModal/DeleteMemberModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import DialogTitle from '@material-ui/core/DialogTitle'
4 | import Dialog from '@material-ui/core/Dialog'
5 | import DialogActions from '@material-ui/core/DialogActions'
6 | import DialogContent from '@material-ui/core/DialogContent'
7 | import Button from '@material-ui/core/Button'
8 | import { makeStyles } from '@material-ui/core/styles'
9 | import styles from './DeleteMemberModal.styles'
10 |
11 | const useStyles = makeStyles(styles)
12 |
13 | function DeleteMemberModal({
14 | onRequestClose,
15 | removeDisabled,
16 | onDeleteClick,
17 | name,
18 | uid,
19 | open
20 | }) {
21 | const classes = useStyles()
22 |
23 | function removeAndClose() {
24 | onRequestClose && onRequestClose()
25 | onDeleteClick && onDeleteClick(uid || name)
26 | }
27 |
28 | return (
29 |
49 | )
50 | }
51 |
52 | DeleteMemberModal.propTypes = {
53 | removeDisabled: PropTypes.bool,
54 | onRequestClose: PropTypes.func.isRequired,
55 | onDeleteClick: PropTypes.func.isRequired,
56 | name: PropTypes.string,
57 | uid: PropTypes.string,
58 | open: PropTypes.bool.isRequired
59 | }
60 |
61 | export default DeleteMemberModal
62 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/DeleteMemberModal/DeleteMemberModal.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({})
2 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/DeleteMemberModal/index.js:
--------------------------------------------------------------------------------
1 | import DeleteMemberModal from './DeleteMemberModal'
2 |
3 | export default DeleteMemberModal
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/NewMemberModal/index.js:
--------------------------------------------------------------------------------
1 | import NewMemberModal from './NewMemberModal'
2 |
3 | export default NewMemberModal
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/NewRoleCard/NewRoleCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { useForm } from 'react-hook-form'
4 | import Button from '@material-ui/core/Button'
5 | import Paper from '@material-ui/core/Paper'
6 | import Typography from '@material-ui/core/Typography'
7 | import TextField from '@material-ui/core/TextField'
8 | import { makeStyles } from '@material-ui/core/styles'
9 | import styles from './NewRoleCard.styles'
10 |
11 | const useStyles = makeStyles(styles)
12 |
13 | function NewRoleCard({ onSubmit, onRequestClose }) {
14 | const classes = useStyles()
15 | const {
16 | register,
17 | handleSubmit,
18 | formState: { isValid }
19 | } = useForm({
20 | mode: 'onChange'
21 | })
22 |
23 | return (
24 |
25 |
54 |
55 | )
56 | }
57 |
58 | NewRoleCard.propTypes = {
59 | onSubmit: PropTypes.func.isRequired,
60 | onRequestClose: PropTypes.func.isRequired
61 | }
62 |
63 | export default NewRoleCard
64 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/NewRoleCard/NewRoleCard.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | ...theme.mixins.gutters(),
4 | paddingTop: theme.spacing(2),
5 | paddingBottom: theme.spacing(2),
6 | marginBottom: theme.spacing(2)
7 | },
8 | buttons: {
9 | display: 'flex',
10 | flexDirection: 'row',
11 | justifyContent: 'flex-end',
12 | marginTop: theme.spacing(4)
13 | }
14 | })
15 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/NewRoleCard/index.js:
--------------------------------------------------------------------------------
1 | import NewRoleCard from './NewRoleCard'
2 |
3 | export default NewRoleCard
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/Permissions/Permissions.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | overflowY: 'scroll',
4 | paddingBottom: '4rem',
5 | paddingLeft: '.125rem',
6 | paddingRight: '.125rem'
7 | },
8 | pageHeader: theme.pageHeader,
9 | button: {
10 | marginLeft: '2rem'
11 | },
12 | buttons: {
13 | ...theme.flexRow,
14 | marginLeft: '.75rem',
15 | marginBottom: '2rem',
16 | justifyContent: 'flex-end'
17 | }
18 | })
19 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/Permissions/index.js:
--------------------------------------------------------------------------------
1 | import Permissions from './Permissions'
2 |
3 | export default Permissions
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/PermissionsTable/NoPermissionsFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Paper from '@material-ui/core/Paper'
3 |
4 | function NoPermissionsFound() {
5 | return (
6 |
7 | No Members found
8 |
9 | )
10 | }
11 |
12 | export default NoPermissionsFound
13 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/PermissionsTable/PermissionsTable.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | heading: {
3 | fontSize: theme.typography.pxToRem(18),
4 | fontWeight: theme.typography.fontWeightRegular
5 | },
6 | header: {
7 | padding: theme.spacing(2)
8 | },
9 | headerLeft: {
10 | marginRight: theme.spacing(24),
11 | fontColor: theme.palette.text.primary,
12 | fontSize: theme.typography.pxToRem(16),
13 | fontWeight: theme.typography.fontWeightRegular
14 | },
15 | headingPaper: {
16 | ...theme.mixins.gutters(),
17 | paddingTop: theme.spacing(2),
18 | paddingBottom: theme.spacing(2),
19 | fontColor: theme.palette.text.secondary,
20 | fontWeight: theme.typography.fontWeightRegular
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/PermissionsTable/index.js:
--------------------------------------------------------------------------------
1 | import PermissionsTable from './PermissionsTable'
2 |
3 | export default PermissionsTable
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/PermissionsTableRow/PermissionsTableRow.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | width: '100%'
4 | },
5 | field: theme.field,
6 | heading: {
7 | fontSize: theme.typography.pxToRem(15),
8 | fontWeight: theme.typography.fontWeightRegular
9 | },
10 | displayName: {
11 | width: '15rem',
12 | marginRight: '2rem',
13 | overflow: 'hidden'
14 | },
15 | permission: {
16 | marginLeft: '-.75rem'
17 | },
18 | menu: {
19 | ...theme.flexRow,
20 | justifyContent: 'flex-end',
21 | width: '100%',
22 | marginTop: '.5rem'
23 | },
24 | roleSelect: {
25 | marginTop: '2.5rem',
26 | paddingLeft: '2rem',
27 | width: '350px'
28 | },
29 | content: {
30 | width: '100%',
31 | display: 'flex',
32 | flexDirection: 'column'
33 | },
34 | buttons: {
35 | ...theme.flexRow,
36 | justifyContent: 'flex-end',
37 | width: '100%'
38 | }
39 | })
40 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/PermissionsTableRow/index.js:
--------------------------------------------------------------------------------
1 | import PermissionsTableRow from './PermissionsTableRow'
2 |
3 | export default PermissionsTableRow
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/RolesTable/NoRolesFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Paper from '@material-ui/core/Paper'
3 |
4 | function NoRolesFound() {
5 | return (
6 |
7 | No Roles found
8 |
9 | )
10 | }
11 |
12 | export default NoRolesFound
13 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/RolesTable/RolesTable.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | height: '100%',
4 | marginTop: '3rem'
5 | },
6 | heading: {
7 | fontSize: '1.75rem',
8 | fontColor: theme.palette.text.primary,
9 | color: 'rgba(0, 0, 0, 0.54)',
10 | fontWeight: theme.typography.fontWeightRegular
11 | },
12 | filter: {
13 | display: 'flex',
14 | marginBottom: '.5rem'
15 | },
16 | filterText: {
17 | fontSize: '1rem',
18 | fontColor: theme.palette.text.primary,
19 | color: 'rgba(0, 0, 0, 0.54)',
20 | fontWeight: theme.typography.fontWeightRegular
21 | },
22 | buttons: {
23 | ...theme.flexRow,
24 | justifyContent: 'flex-end',
25 | width: '100%',
26 | marginBottom: '3rem'
27 | },
28 | rolesTable: {
29 | height: '100%',
30 | minHeight: '400px'
31 | }
32 | })
33 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/RolesTable/index.js:
--------------------------------------------------------------------------------
1 | import RolesTable from './RolesTable'
2 |
3 | export default RolesTable
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/RolesTableRow/RolesTableRow.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | root: {
3 | width: '100%'
4 | },
5 | heading: {
6 | fontSize: theme.typography.pxToRem(18),
7 | fontWeight: theme.typography.fontWeightRegular
8 | },
9 | content: {
10 | width: '100%',
11 | display: 'flex',
12 | flexDirection: 'column'
13 | },
14 | roleSelect: {
15 | display: 'flex',
16 | flexDirection: 'row',
17 | marginLeft: '2rem'
18 | },
19 | buttons: {
20 | display: 'flex',
21 | flexDirection: 'row',
22 | justifyContent: 'flex-end'
23 | },
24 | menu: {
25 | display: 'flex',
26 | flexDirection: 'row',
27 | justifyContent: 'flex-end',
28 | marginTop: '.5rem'
29 | },
30 | optionsLabels: {
31 | display: 'flex',
32 | flexDirection: 'column',
33 | marginTop: '1.5rem',
34 | marginRight: '1rem'
35 | },
36 | optionLabel: {
37 | marginBottom: '1rem',
38 | marginTop: '1rem'
39 | },
40 | roleOption: {
41 | marginLeft: '1rem'
42 | },
43 | roleOptions: {
44 | display: 'flex',
45 | flexDirection: 'column',
46 | justifyContent: 'center',
47 | alignItems: 'center',
48 | marginRight: '1rem',
49 | marginLeft: '1rem'
50 | },
51 | resourcePermissionsHeader: {
52 | marginBottom: '1rem',
53 | marginTop: '-2rem',
54 | fontSize: '1.1rem'
55 | }
56 | })
57 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/components/RolesTableRow/index.js:
--------------------------------------------------------------------------------
1 | import RolesTableRow from './RolesTableRow'
2 |
3 | export default RolesTableRow
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/Permissions/index.js:
--------------------------------------------------------------------------------
1 | import { loadable } from 'utils/router'
2 | import { PROJECT_PERMISSIONS_PATH as path } from 'constants/paths'
3 |
4 | export default {
5 | path,
6 | component: loadable(() =>
7 | import(
8 | /* webpackChunkName: 'ProjectPermissions' */ './components/Permissions'
9 | )
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/ProjectEvents/components/ProjectEventsPage/NoProjectEvents.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Paper from '@material-ui/core/Paper'
3 | import { makeStyles } from '@material-ui/core/styles'
4 |
5 | import styles from './ProjectEventsPage.styles'
6 |
7 | const useStyles = makeStyles(styles)
8 |
9 | function NoProjectEvents() {
10 | const classes = useStyles()
11 | return (
12 |
13 | No Project Events Found
14 |
15 | )
16 | }
17 |
18 | export default NoProjectEvents
19 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/ProjectEvents/components/ProjectEventsPage/ProjectEventsPage.styles.js:
--------------------------------------------------------------------------------
1 | export default (theme) => ({
2 | body: {
3 | overflowY: 'hidden',
4 | height: '100%'
5 | },
6 | content: {
7 | overflowY: 'scroll',
8 | heigth: '100%',
9 | padding: '.5rem',
10 | paddingBottom: '6rem'
11 | },
12 | pageHeader: theme.pageHeader,
13 | tableBody: {
14 | paddingBottom: '3rem'
15 | },
16 | sectionHeader: {
17 | fontSize: '1.3rem'
18 | },
19 | tableRowDivider: {
20 | color: 'rgba(0,0,0,0.54)',
21 | backgroundColor: '#f5f5f5',
22 | borderTop: '1px solid rgba(0,0,0,0.12)',
23 | fontSize: '1.1rem',
24 | fontWeight: 'bold'
25 | },
26 | empty: {
27 | textAlign: 'center',
28 | padding: '3rem'
29 | }
30 | })
31 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/ProjectEvents/components/ProjectEventsPage/index.js:
--------------------------------------------------------------------------------
1 | import ProjectEventsPage from './ProjectEventsPage'
2 |
3 | export default ProjectEventsPage
4 |
--------------------------------------------------------------------------------
/src/routes/Projects/routes/Project/routes/ProjectEvents/index.js:
--------------------------------------------------------------------------------
1 | import { loadable } from 'utils/router'
2 | import { PROJECT_EVENTS_PATH as path } from 'constants/paths'
3 |
4 | export default {
5 | path,
6 | component: loadable(() =>
7 | import(
8 | /* webpackChunkName: 'ProjectEvents' */ './components/ProjectEventsPage'
9 | )
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Switch, Route } from 'react-router-dom'
3 | import { SuspenseWithPerf } from 'reactfire'
4 | import AnalyticsPageViewLogger from 'components/AnalyticsPageViewLogger'
5 | import { PrivateRoute } from 'utils/router'
6 | import LoadingSpinner from 'components/LoadingSpinner'
7 | import CoreLayout from '../layouts/CoreLayout'
8 | import AccountRoute from './Account'
9 | import ActionTemplateRoute from './ActionTemplate'
10 | import ActionTemplatesRoute from './ActionTemplates'
11 | import Home from './Home'
12 | import LoginRoute from './Login'
13 | import NotFoundRoute from './NotFound'
14 | import ProjectsRoute from './Projects'
15 |
16 | export default function createRoutes(store) {
17 | return (
18 |
19 | } traceId="router-wait">
20 |
21 | {/* eslint-disable react/jsx-pascal-case */}
22 | } />
23 | {/* eslint-enable react/jsx-pascal-case */}
24 | {
25 | /* Build Route components from routeSettings */
26 | [
27 | ActionTemplateRoute,
28 | ActionTemplatesRoute,
29 | AccountRoute,
30 | ProjectsRoute,
31 | LoginRoute
32 | /* Add More Routes Here */
33 | ].map((settings) =>
34 | settings.authRequired ? (
35 |
36 | ) : (
37 |
38 | )
39 | )
40 | }
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/src/static/User.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prescottprue/fireadmin/7d49cb768a77078608c4f21d219cea2b447dfd1f/src/static/User.png
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | import orange from '@material-ui/core/colors/orange'
2 | import pink from '@material-ui/core/colors/pink'
3 | import teal from '@material-ui/core/colors/teal'
4 | import grey from '@material-ui/core/colors/grey'
5 | import blue from '@material-ui/core/colors/blue'
6 | // import { fade } from '@material-ui/core/styles/colorManipulator'
7 |
8 | export default {
9 | fontFamily: 'Roboto, sans-serif',
10 | palette: {
11 | primary: blue,
12 | primary1Color: orange[800],
13 | primary2Color: grey[400],
14 | primary3Color: orange[100],
15 | accent1Color: pink.A200,
16 | accent2Color: teal.A100
17 | },
18 | field: {
19 | width: '100%',
20 | marginTop: '2rem'
21 | // maxWidth: '350px'
22 | },
23 | multilineField: {
24 | width: '100%',
25 | marginTop: '2rem'
26 | // maxWidth: '500px'
27 | },
28 | typography: {
29 | // Enable typography v2: https://material-ui.com/style/typography/#migration-to-typography-v2
30 | useNextVariants: true,
31 | sub: {
32 | color: '#757575'
33 | },
34 | h3: {
35 | color: '#757575',
36 | fontWeight: 100,
37 | marginBottom: '3rem'
38 | },
39 | h5: {
40 | color: '#757575',
41 | fontWeight: 400,
42 | marginBottom: '2rem'
43 | }
44 | // h4: {
45 | // color: '#757575',
46 | // fontWeight: 100
47 | // }
48 | },
49 | flexColumnCenter: {
50 | display: 'flex',
51 | flexDirection: 'column',
52 | alignItems: 'center'
53 | },
54 | flexRow: {
55 | display: 'flex',
56 | flexDirection: 'row'
57 | },
58 | flexColumn: {
59 | display: 'flex',
60 | flexDirection: 'column'
61 | },
62 | flexRowCenter: {
63 | display: 'flex',
64 | flexDirection: 'row',
65 | justifyContent: 'center'
66 | },
67 | pageHeader: {
68 | color: 'rgba(0, 0, 0, 0.54)',
69 | fontSize: '2.25rem',
70 | alignSelf: 'flex-start',
71 | marginBottom: '2rem'
72 | },
73 | subHeader: {
74 | color: ' rgba(0, 0, 0, 0.54)',
75 | fontSize: '1.5rem',
76 | alignSelf: 'flex-start',
77 | marginTop: '2rem',
78 | marginBottom: '2rem'
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/utils/async.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Async await wrapper for easy error handling
3 | * @param {Promise} promise - Promise to wrap responses of
4 | * @return {Promise} Resolves and rejects with an array
5 | * @example
6 | * async function asyncFunctionWithThrow() {
7 | * const [err, snap] = await to(
8 | * admin.database().ref('some').once('value')
9 | * );
10 | * if (err) {
11 | * console.error('Error getting data:', err.message || err)
12 | * throw err
13 | * }
14 | * if (!snap.val()) throw new Error('Data not found');
15 | * console.log('Data found:', snap.val())
16 | * }
17 | */
18 | export function to(promise) {
19 | return promise.then((data) => [null, data]).catch((err) => [err])
20 | }
21 |
22 | /**
23 | * Run promises in a waterfall instead of all the same time (Promise.all)
24 | * @param {Array} callbacks - List of promises to run in order
25 | * @return {Promise} Resolves when all promises have completed in order
26 | */
27 | export function promiseWaterfall(callbacks) {
28 | return callbacks.reduce(
29 | (accumulator, callback) => accumulator.then(callback),
30 | Promise.resolve()
31 | )
32 | }
33 |
34 | export default { to, promiseWaterfall }
35 |
--------------------------------------------------------------------------------
/src/utils/components.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | export class ErrorBoundary extends React.Component {
5 | constructor(props) {
6 | super(props)
7 | this.state = { error: null, errorInfo: null }
8 | }
9 |
10 | componentDidCatch(error, errorInfo) {
11 | // Catch errors in any components below and re-render with error message
12 | this.setState({
13 | error: error,
14 | errorInfo: errorInfo
15 | })
16 | // You can also log error messages to an error reporting service here
17 | }
18 |
19 | render() {
20 | if (this.state.errorInfo) {
21 | if (this.props.errorComponent) {
22 | return React.cloneElement(this.props.errorComponent, { ...this.state })
23 | }
24 | // Error path
25 | return (
26 |
27 |
Something went wrong.
28 |
29 | {this.state.error && this.state.error.toString()}
30 |
31 | {this.state.errorInfo.componentStack}
32 |
33 |
34 | )
35 | }
36 | // Normally, just render children
37 | return this.props.children
38 | }
39 | }
40 |
41 | ErrorBoundary.propTypes = {
42 | children: PropTypes.element,
43 | errorComponent: PropTypes.element
44 | }
45 |
46 | export function withErrorBoundary(errorComponent) {
47 | return (WrappedComponent) => (props) => {
48 | return (
49 |
50 |
51 |
52 | )
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/utils/data.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash'
2 |
3 | /**
4 | * Create a permission checking function based on projct and user
5 | * @param {Object} project - Project object
6 | * @param {Object} project.permissions - Project permissions object
7 | * @param {String} userUid - User's UID for permission check
8 | * @example Basic
9 | * const userHasPermission = createPermissionGetter(project, user.uid)
10 | * userHasPermission('update.roles') // true if user has role update permission
11 | */
12 | export function createPermissionGetter(project, userUid) {
13 | const userRole = get(project, `permissions.${userUid}.role`)
14 | /**
15 | * Check if user has permission
16 | * @param {String} permission - Permission string
17 | * @example Check For Permission
18 | * userHasPermission('update.roles') // true if user has role update permission
19 | */
20 | return function userHasPermission(permission) {
21 | return get(project, `roles.${userRole}.permissions.${permission}`)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/utils/firebaseFunctions.js:
--------------------------------------------------------------------------------
1 | function waitForResponseWith(ref, pathForValue = 'completed', value) {
2 | return new Promise((resolve, reject) => {
3 | ref.on(
4 | 'value',
5 | (responseSnap) => {
6 | const response = responseSnap.val()
7 | if (response && response[pathForValue]) {
8 | if (value && response[pathForValue] !== value) {
9 | return
10 | }
11 | resolve(response)
12 | }
13 | },
14 | (err) => {
15 | console.error('Error waiting for response:', err.message || err) // eslint-disable-line no-console
16 | reject(err)
17 | }
18 | )
19 | })
20 | }
21 |
22 | function createWaitForValue(...args) {
23 | return (ref) => waitForResponseWith(ref, ...args)
24 | }
25 |
26 | export const waitForCompleted = createWaitForValue('completed', true)
27 |
--------------------------------------------------------------------------------
/src/utils/form.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns error message if value does not exist, otherwise returns
3 | * undefined
4 | * @param {String} value - Email to validate
5 | * @returns {String | undefined} Error message for missing required param
6 | * @example Required Field
7 | *
14 | */
15 | export function required(value) {
16 | return value ? undefined : 'Required'
17 | }
18 |
19 | /**
20 | * Returns error message if value is not a valid email, otherwise returns
21 | * undefined
22 | * @param {String} value - Email to validate
23 | * @returns {String | undefined} Error message for invalid email
24 | * @example Basic
25 | *
31 | */
32 | export function validateEmail(value) {
33 | return value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)
34 | ? 'Invalid email address'
35 | : undefined
36 | }
37 |
--------------------------------------------------------------------------------
/src/utils/formatters.js:
--------------------------------------------------------------------------------
1 | import { isDate } from 'lodash'
2 | import { format } from 'date-fns'
3 |
4 | /**
5 | * Convert date string or object into date object
6 | * @param {firebase.firestore.Timestamp|Date|String} [dateValue=null] - Date value which to format
7 | * @return {Date} Formatted time
8 | */
9 | function getDateObject(dateValue = null) {
10 | if (dateValue && typeof dateValue.toDate === 'function') {
11 | return dateValue.toDate()
12 | }
13 | return isDate(dateValue) ? dateValue : new Date(dateValue)
14 | }
15 |
16 | /**
17 | * Format date to time with am/pm
18 | * @param {firebase.firestore.Timestamp|Date|String} dateValue - Date value which to format
19 | * @return {String} Formatted time
20 | */
21 | export function formatTime(dateValue) {
22 | return format(getDateObject(dateValue), 'h:mm:ss.SSS a')
23 | }
24 |
25 | /**
26 | * Format date string or object into date string with format 1/22/2018
27 | * @param {firebase.firestore.Timestamp|Date|String} dateValue - Date value which to format
28 | * @return {String} Formatted date
29 | */
30 | export function formatDate(dateValue) {
31 | return format(getDateObject(dateValue), 'MM/dd/yy')
32 | }
33 |
34 | /**
35 | * Format date string or object into date string with format
36 | * 1/22/2018 - 3:30:25.123 AM
37 | * @param {firebase.firestore.Timestamp|Date|String} dateValue - Date value which to format
38 | * @return {String} Formatted date
39 | */
40 | export function formatDateTime(dateValue) {
41 | return format(getDateObject(dateValue), 'MM/dd/yy - h:mm:ss.SSS a')
42 | }
43 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import { initSegment } from './analytics'
2 | import { init as initErrorHandler } from './errorHandler'
3 |
4 | /**
5 | * Initialize global scripts including analytics and error handling
6 | */
7 | export function initScripts() {
8 | initErrorHandler()
9 | initSegment()
10 | }
11 |
12 | export function databaseURLToProjectName(databaseURL) {
13 | const strMatch = databaseURL?.match(/https:\/\/(.*)\.firebaseio\.com/)
14 | return strMatch && strMatch[1]
15 | }
16 |
--------------------------------------------------------------------------------
/storage.rules:
--------------------------------------------------------------------------------
1 | rules_version = '2';
2 | service firebase.storage {
3 | match /b/{bucket}/o {
4 | allow read: if false;
5 | match /serviceAccounts {
6 | match /{userId} {
7 | match /{allPaths=**} {
8 | allow write: if request.auth != null // Only authenticated users
9 | && request.auth.uid == userId // with UID matching parent folder name
10 | && request.resource.contentType.matches('application/json'); // Only json files
11 | }
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------