├── .angular-cli.json
├── .editorconfig
├── .firebaserc
├── .gitignore
├── LICENSE
├── README.md
├── docs
└── overview.jpg
├── e2e
├── app.e2e-spec.ts
├── app.po.ts
└── tsconfig.e2e.json
├── firebase.json
├── firestore.indexes.json
├── firestore.rules
├── karma.conf.js
├── package-lock.json
├── package.json
├── protractor.conf.js
├── src
├── app
│ ├── app-routing.module.ts
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── components
│ │ ├── add-app
│ │ │ ├── add-app.component.html
│ │ │ ├── add-app.component.scss
│ │ │ └── add-app.component.ts
│ │ ├── app-switcher
│ │ │ ├── app-switcher.component.html
│ │ │ ├── app-switcher.component.scss
│ │ │ └── app-switcher.component.ts
│ │ ├── auth-switcher
│ │ │ ├── auth-switcher.component.html
│ │ │ ├── auth-switcher.component.scss
│ │ │ └── auth-switcher.component.ts
│ │ ├── create-dialog
│ │ │ ├── create-dialog.component.html
│ │ │ ├── create-dialog.component.scss
│ │ │ └── create-dialog.component.ts
│ │ ├── delete-dialog
│ │ │ ├── delete-dialog.component.html
│ │ │ ├── delete-dialog.component.scss
│ │ │ └── delete-dialog.component.ts
│ │ ├── edit-dialog
│ │ │ ├── edit-dialog.component.html
│ │ │ ├── edit-dialog.component.scss
│ │ │ └── edit-dialog.component.ts
│ │ ├── editor
│ │ │ ├── editor.component.html
│ │ │ ├── editor.component.scss
│ │ │ └── editor.component.ts
│ │ ├── entry-menu-button
│ │ │ ├── entry-menu-button.component.html
│ │ │ ├── entry-menu-button.component.scss
│ │ │ └── entry-menu-button.component.ts
│ │ ├── login
│ │ │ ├── login.component.html
│ │ │ ├── login.component.scss
│ │ │ └── login.component.ts
│ │ ├── main
│ │ │ ├── main.component.html
│ │ │ ├── main.component.scss
│ │ │ └── main.component.ts
│ │ ├── query-actions
│ │ │ ├── query-actions.component.html
│ │ │ ├── query-actions.component.scss
│ │ │ └── query-actions.component.ts
│ │ ├── query-browser-result
│ │ │ ├── query-browser-result.component.html
│ │ │ ├── query-browser-result.component.scss
│ │ │ └── query-browser-result.component.ts
│ │ ├── query-browser
│ │ │ ├── query-browser.component.html
│ │ │ ├── query-browser.component.scss
│ │ │ └── query-browser.component.ts
│ │ ├── query-history
│ │ │ ├── query-history.component.html
│ │ │ ├── query-history.component.scss
│ │ │ └── query-history.component.ts
│ │ ├── query-input
│ │ │ ├── query-input.component.html
│ │ │ ├── query-input.component.scss
│ │ │ └── query-input.component.ts
│ │ ├── query-snippets
│ │ │ ├── query-snippets.component.html
│ │ │ ├── query-snippets.component.scss
│ │ │ └── query-snippets.component.ts
│ │ ├── result-table
│ │ │ ├── result-table.component.html
│ │ │ ├── result-table.component.scss
│ │ │ └── result-table.component.ts
│ │ └── toolbar
│ │ │ ├── toolbar.component.html
│ │ │ ├── toolbar.component.scss
│ │ │ └── toolbar.component.ts
│ ├── modules
│ │ └── material.module.ts
│ └── services
│ │ ├── apps.service.ts
│ │ ├── auth-switcher.service.ts
│ │ ├── auth.service.ts
│ │ ├── data.service.ts
│ │ ├── dialog.service.ts
│ │ ├── export.service.ts
│ │ ├── google-analytics.service.ts
│ │ ├── history.service.ts
│ │ ├── query.service.ts
│ │ ├── storage.service.ts
│ │ └── util.service.ts
├── assets
│ └── .gitkeep
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.png
├── index.html
├── main.ts
├── polyfills.ts
├── styles.scss
├── test.ts
├── tsconfig.app.json
├── tsconfig.spec.json
└── typings.d.ts
├── tsconfig.json
└── tslint.json
/.angular-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "project": {
4 | "name": "firestore-query-browser"
5 | },
6 | "apps": [
7 | {
8 | "root": "src",
9 | "outDir": "dist",
10 | "assets": [
11 | "assets",
12 | "favicon.png"
13 | ],
14 | "index": "index.html",
15 | "main": "main.ts",
16 | "polyfills": "polyfills.ts",
17 | "test": "test.ts",
18 | "tsconfig": "tsconfig.app.json",
19 | "testTsconfig": "tsconfig.spec.json",
20 | "prefix": "app",
21 | "styles": [
22 | "styles.scss"
23 | ],
24 | "scripts": [],
25 | "environmentSource": "environments/environment.ts",
26 | "environments": {
27 | "dev": "environments/environment.ts",
28 | "prod": "environments/environment.prod.ts"
29 | }
30 | }
31 | ],
32 | "e2e": {
33 | "protractor": {
34 | "config": "./protractor.conf.js"
35 | }
36 | },
37 | "lint": [
38 | {
39 | "project": "src/tsconfig.app.json",
40 | "exclude": "**/node_modules/**"
41 | },
42 | {
43 | "project": "src/tsconfig.spec.json",
44 | "exclude": "**/node_modules/**"
45 | },
46 | {
47 | "project": "e2e/tsconfig.e2e.json",
48 | "exclude": "**/node_modules/**"
49 | }
50 | ],
51 | "test": {
52 | "karma": {
53 | "config": "./karma.conf.js"
54 | }
55 | },
56 | "defaults": {
57 | "styleExt": "scss",
58 | "class": {
59 | "spec": false
60 | },
61 | "component": {
62 | "spec": false
63 | },
64 | "directive": {
65 | "spec": false
66 | },
67 | "guard": {
68 | "spec": false
69 | },
70 | "module": {
71 | "spec": false
72 | },
73 | "pipe": {
74 | "spec": false
75 | },
76 | "service": {
77 | "spec": false
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "firestore-query-browser"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /dist-server
6 | /tmp
7 | /out-tsc
8 |
9 | # dependencies
10 | /node_modules
11 |
12 | # IDEs and editors
13 | /.idea
14 | .project
15 | .classpath
16 | .c9/
17 | *.launch
18 | .settings/
19 | *.sublime-workspace
20 |
21 | # IDE - VSCode
22 | .vscode/*
23 | !.vscode/settings.json
24 | !.vscode/tasks.json
25 | !.vscode/launch.json
26 | !.vscode/extensions.json
27 |
28 | # misc
29 | /.sass-cache
30 | /connect.lock
31 | /coverage
32 | /libpeerconnection.log
33 | npm-debug.log
34 | yarn-error.log
35 | testem.log
36 | /typings
37 |
38 | # e2e
39 | /e2e/*.js
40 | /e2e/*.map
41 |
42 | # System Files
43 | .DS_Store
44 | Thumbs.db
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2018 Tristan Rechenberger & Sean Dieterle
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/jthegedus/awesome-firebase)
2 |
3 | # Firestore Query Browser
4 |
5 | 
6 |
7 | ## Features
8 |
9 | - Multiple projects
10 | - Easy account switching: "Does this query run as user XYZ?"
11 | - Querying with the standard JS query syntax for easy copying & pasting between firestore query browser & your app
12 | - Query history
13 | - Create documents in single or batch mode
14 | - Set / Update / Delete single documents
15 | - Set / Update / Delete all documents matching a query
16 | - Export documents as CSV or JSON
17 |
18 | ## Live Demo
19 |
20 | Checkout our live demo at [https://firestore-query-browser.firebaseapp.com/](https://firestore-query-browser.firebaseapp.com/).
21 |
22 | Just add the default application & start querying the 🦕
23 |
24 | ## Usage
25 |
26 | ### Adding your app
27 |
28 | 1. Get the your Firebase Project's config from the [Firebase Console](ttps://console.firebase.google.com/). The config should looks like this:
29 | ```js
30 | {
31 | apiKey: "AIzaSyCzpisEJhHYFR09Rh48NAQX6g3gwG2v2U0",
32 | authDomain: "firestore-query-browser.firebaseapp.com",
33 | databaseURL: "https://firestore-query-browser.firebaseio.com",
34 | projectId: "firestore-query-browser",
35 | storageBucket: "firestore-query-browser.appspot.com",
36 | messagingSenderId: "567385024694"
37 | }
38 | ```
39 | 2. Go to [https://firestore-query-browser.firebaseapp.com/](https://firestore-query-browser.firebaseapp.com/), paste the config and click on 'Add App'
40 | 3. Optionally, you can add `firestore-query-browser.firebaseapp.com/` to your Authorized domains in the [Firebase Console](ttps://console.firebase.google.com/) --> Authentication --> Sign-In-Methods --> Authorized domains
41 | 4. Insert a path and a query (or click on one of the Examples below)
42 | 5. Click 'fetch' and find the results below.
43 |
44 | ### Create an admin user
45 | Because firestore query browser does not use a service account it is limited by your security rules. An easy solution is to create an admin account via password authentication. Add the following code to your firestore.rules and replace `[YOUR ADMIN UID]` with the corresponding id:
46 |
47 | ```
48 | // Admin has access to every document
49 | match /{document=**} {
50 | allow read, write: if request.auth.uid == '[YOUR ADMIN UID]'
51 | }
52 | ```
53 | Then login to firestore query browser with your admin account.
54 |
55 | ## Advanced usage
56 |
57 | ### Dates
58 | Strings that match an ISO datetime regex will be transformed into a date object before saving them to the firestore. For example:
59 | ```json
60 | {
61 | "time": "2019-01-01T14:12:34.567Z"
62 | }
63 | ```
64 |
65 | If you want to query a date use `new Date()` around it:
66 |
67 | ```js
68 | ref
69 | .where('created', '>=', new Date('2020-08-12T00:00'))
70 | ```
71 |
72 | ### Deleting a key
73 | To delete a key in a document update it with `__del__` as value. For example:
74 | ```json
75 | {
76 | "oldThing": "__del__"
77 | }
78 | ```
79 |
80 | ### Timing
81 | Do you wonder how long your queries take? Just open the developer console in your browser. Every time you query something the query browser logs the time to the console: `get: 317.427978515625ms`
82 |
83 |
84 | ### Work with results in the browser console (lodash)
85 | Sometimes you want to work with the results of your query. Firestore query browser adds the results of your latest query to document.result. Further it exposes lodash so you could write something like this in your developer tools console:
86 | ```js
87 | _.map(result, doc => doc.info)
88 | ```
89 |
90 | ## Development
91 |
92 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli). Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
93 |
94 |
--------------------------------------------------------------------------------
/docs/overview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechenberger/firestore-query-browser/d4fc7180508a46b14a81ff6a0b66eecf1afe15da/docs/overview.jpg
--------------------------------------------------------------------------------
/e2e/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { AppPage } from './app.po';
2 |
3 | describe('firestore-query-browser App', () => {
4 | let page: AppPage;
5 |
6 | beforeEach(() => {
7 | page = new AppPage();
8 | });
9 |
10 | it('should display welcome message', () => {
11 | page.navigateTo();
12 | expect(page.getParagraphText()).toEqual('Welcome to app!');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/e2e/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class AppPage {
4 | navigateTo() {
5 | return browser.get('/');
6 | }
7 |
8 | getParagraphText() {
9 | return element(by.css('app-root h1')).getText();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": [
9 | "jasmine",
10 | "jasminewd2",
11 | "node"
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "firestore": {
3 | "rules": "firestore.rules",
4 | "indexes": "firestore.indexes.json"
5 | },
6 | "hosting": {
7 | "public": "dist",
8 | "ignore": [
9 | "firebase.json",
10 | "**/.*",
11 | "**/node_modules/**"
12 | ],
13 | "rewrites": [
14 | {
15 | "source": "**",
16 | "destination": "/index.html"
17 | }
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/firestore.indexes.json:
--------------------------------------------------------------------------------
1 | {
2 | // Example:
3 | //
4 | // "indexes": [
5 | // {
6 | // "collectionId": "widgets",
7 | // "fields": [
8 | // { "fieldPath": "foo", "mode": "ASCENDING" },
9 | // { "fieldPath": "bar", "mode": "DESCENDING" }
10 | // ]
11 | // }
12 | // ]
13 | "indexes": []
14 | }
--------------------------------------------------------------------------------
/firestore.rules:
--------------------------------------------------------------------------------
1 | service cloud.firestore {
2 | match /databases/{database}/documents {
3 | match /{document=**} {
4 | allow read, write;
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular/cli'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular/cli/plugins/karma')
14 | ],
15 | client:{
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | reports: [ 'html', 'lcovonly' ],
20 | fixWebpackSourcePaths: true
21 | },
22 | angularCli: {
23 | environment: 'dev'
24 | },
25 | reporters: ['progress', 'kjhtml'],
26 | port: 9876,
27 | colors: true,
28 | logLevel: config.LOG_INFO,
29 | autoWatch: true,
30 | browsers: ['Chrome'],
31 | singleRun: false
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "firestore-query-browser",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "ng": "ng",
7 | "start": "ng serve",
8 | "build": "ng build --prod",
9 | "deploy-without-build": "firebase deploy --only hosting",
10 | "deploy": "npm run build && npm run deploy-without-build"
11 | },
12 | "private": true,
13 | "dependencies": {
14 | "@angular/animations": "^5.2.0",
15 | "@angular/cdk": "^5.2.2",
16 | "@angular/common": "^5.2.0",
17 | "@angular/compiler": "^5.2.0",
18 | "@angular/core": "^5.2.0",
19 | "@angular/forms": "^5.2.0",
20 | "@angular/http": "^5.2.0",
21 | "@angular/material": "^5.2.2",
22 | "@angular/platform-browser": "^5.2.0",
23 | "@angular/platform-browser-dynamic": "^5.2.0",
24 | "@angular/router": "^5.2.0",
25 | "angularfire2": "^5.0.0-rc.6",
26 | "core-js": "^2.4.1",
27 | "dotize": "^0.2.0",
28 | "firebase": "^4.10.1",
29 | "firebaseui": "^2.6.1",
30 | "hammerjs": "^2.0.8",
31 | "lodash": "^4.17.5",
32 | "papaparse": "^4.5.0",
33 | "rxjs": "^5.5.8",
34 | "rxjs-compat": "^6.5.2",
35 | "sample-nodeserver-dinos": "github:auth0-blog/sample-nodeserver-dinos",
36 | "zone.js": "^0.8.19"
37 | },
38 | "devDependencies": {
39 | "@angular/cli": "~1.7.1",
40 | "@angular/compiler-cli": "^5.2.0",
41 | "@angular/language-service": "^5.2.0",
42 | "@types/jasmine": "~2.8.3",
43 | "@types/jasminewd2": "~2.0.2",
44 | "@types/node": "~6.0.60",
45 | "codelyzer": "^4.0.1",
46 | "jasmine-core": "~2.8.0",
47 | "jasmine-spec-reporter": "~4.2.1",
48 | "karma": "~2.0.0",
49 | "karma-chrome-launcher": "~2.2.0",
50 | "karma-coverage-istanbul-reporter": "^1.2.1",
51 | "karma-jasmine": "~1.1.0",
52 | "karma-jasmine-html-reporter": "^0.2.2",
53 | "protractor": "~5.1.2",
54 | "ts-node": "~4.1.0",
55 | "tslint": "~5.9.1",
56 | "typescript": "~2.5.3"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | const { SpecReporter } = require('jasmine-spec-reporter');
5 |
6 | exports.config = {
7 | allScriptsTimeout: 11000,
8 | specs: [
9 | './e2e/**/*.e2e-spec.ts'
10 | ],
11 | capabilities: {
12 | 'browserName': 'chrome'
13 | },
14 | directConnect: true,
15 | baseUrl: 'http://localhost:4200/',
16 | framework: 'jasmine',
17 | jasmineNodeOpts: {
18 | showColors: true,
19 | defaultTimeoutInterval: 30000,
20 | print: function() {}
21 | },
22 | onPrepare() {
23 | require('ts-node').register({
24 | project: 'e2e/tsconfig.e2e.json'
25 | });
26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { MainComponent } from './components/main/main.component'
2 | import { QueryBrowserComponent } from './components/query-browser/query-browser.component'
3 | import { NgModule } from '@angular/core'
4 | import { Routes, RouterModule } from '@angular/router'
5 | import { LoginComponent } from './components/login/login.component'
6 | import { AddAppComponent } from './components/add-app/add-app.component';
7 |
8 | const routes: Routes = [
9 | {
10 | path: '',
11 | component: MainComponent,
12 | children: [
13 | {
14 | path: '',
15 | component: QueryBrowserComponent
16 | },
17 | {
18 | path: 'login',
19 | component: LoginComponent
20 | },
21 | {
22 | path: 'add',
23 | component: AddAppComponent
24 | }
25 | ]
26 | },
27 | {
28 | path: '**',
29 | redirectTo: '/'
30 | }
31 | ]
32 |
33 | @NgModule({
34 | imports: [RouterModule.forRoot(routes)],
35 | exports: [RouterModule]
36 | })
37 | export class AppRoutingModule { }
38 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechenberger/firestore-query-browser/d4fc7180508a46b14a81ff6a0b66eecf1afe15da/src/app/app.component.scss
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | templateUrl: './app.component.html',
6 | styleUrls: ['./app.component.scss']
7 | })
8 | export class AppComponent {
9 | title = 'app';
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { GoogleAnalyticsService } from './services/google-analytics.service'
2 | import { HistoryService } from './services/history.service'
3 | import { AuthService } from './services/auth.service'
4 | import { AppsService } from './services/apps.service'
5 | import { MaterialModule } from './modules/material.module'
6 | import { DataService } from './services/data.service'
7 | import { BrowserModule } from '@angular/platform-browser'
8 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
9 | import { NgModule } from '@angular/core'
10 | import { FormsModule } from '@angular/forms'
11 | import { AppRoutingModule } from './app-routing.module'
12 | import { AppComponent } from './app.component'
13 |
14 | import { AngularFireModule } from 'angularfire2'
15 | import { AngularFirestoreModule } from 'angularfire2/firestore'
16 | import { AngularFireStorageModule } from 'angularfire2/storage'
17 | import { AngularFireAuthModule } from 'angularfire2/auth'
18 |
19 |
20 | import { QueryBrowserComponent } from './components/query-browser/query-browser.component'
21 | import { AppSwitcherComponent } from './components/app-switcher/app-switcher.component'
22 | import { MainComponent } from './components/main/main.component'
23 | import { LoginComponent } from './components/login/login.component'
24 | import { StorageService } from './services/storage.service'
25 | import { AuthSwitcherService } from './services/auth-switcher.service'
26 |
27 | import 'rxjs/add/operator/do'
28 | import 'rxjs/add/operator/take'
29 | import 'rxjs/add/operator/map'
30 | import 'rxjs/add/operator/filter'
31 |
32 | import { AuthSwitcherComponent } from './components/auth-switcher/auth-switcher.component'
33 | import { QueryBrowserResultComponent } from './components/query-browser-result/query-browser-result.component'
34 | import { DeleteDialogComponent } from './components/delete-dialog/delete-dialog.component'
35 | import { EditDialogComponent } from './components/edit-dialog/edit-dialog.component'
36 | import { CreateDialogComponent } from './components/create-dialog/create-dialog.component'
37 | import { EditorComponent } from './components/editor/editor.component'
38 | import { DialogService } from './services/dialog.service'
39 | import { ToolbarComponent } from './components/toolbar/toolbar.component'
40 | import { AddAppComponent } from './components/add-app/add-app.component'
41 | import { QueryInputComponent } from './components/query-input/query-input.component'
42 | import { QuerySnippetsComponent } from './components/query-snippets/query-snippets.component'
43 | import { QueryHistoryComponent } from './components/query-history/query-history.component'
44 | import { QueryActionsComponent } from './components/query-actions/query-actions.component'
45 | import { UtilService } from './services/util.service'
46 | import { ResultTableComponent } from './components/result-table/result-table.component'
47 | import { EntryMenuButtonComponent } from './components/entry-menu-button/entry-menu-button.component'
48 | import { ExportService } from './services/export.service'
49 | import { QueryService } from './services/query.service'
50 |
51 |
52 | @NgModule({
53 | declarations: [
54 | AppComponent,
55 | QueryBrowserComponent,
56 | AppSwitcherComponent,
57 | MainComponent,
58 | LoginComponent,
59 | AuthSwitcherComponent,
60 | QueryBrowserResultComponent,
61 | DeleteDialogComponent,
62 | EditDialogComponent,
63 | CreateDialogComponent,
64 | ToolbarComponent,
65 | AddAppComponent,
66 | EditorComponent,
67 | QueryInputComponent,
68 | QuerySnippetsComponent,
69 | QueryHistoryComponent,
70 | QueryActionsComponent,
71 | ResultTableComponent,
72 | EntryMenuButtonComponent
73 | ],
74 | imports: [
75 | BrowserModule,
76 | AppRoutingModule,
77 | AngularFireModule,
78 | FormsModule,
79 | MaterialModule,
80 | BrowserAnimationsModule,
81 | ],
82 | providers: [
83 | DataService,
84 | AppsService,
85 | AuthService,
86 | StorageService,
87 | AuthSwitcherService,
88 | DialogService,
89 | HistoryService,
90 | UtilService,
91 | GoogleAnalyticsService,
92 | ExportService,
93 | QueryService
94 | ],
95 | bootstrap: [AppComponent],
96 | entryComponents: [
97 | DeleteDialogComponent,
98 | EditDialogComponent,
99 | CreateDialogComponent
100 | ]
101 | })
102 | export class AppModule { }
103 |
--------------------------------------------------------------------------------
/src/app/components/add-app/add-app.component.html:
--------------------------------------------------------------------------------
1 |
2 | Add App
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app/components/add-app/add-app.component.scss:
--------------------------------------------------------------------------------
1 | mat-card {
2 | display: flex;
3 | flex-direction: column;
4 | }
--------------------------------------------------------------------------------
/src/app/components/add-app/add-app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core'
2 | import { AppsService } from '../../services/apps.service';
3 | import { MatSnackBar } from '@angular/material';
4 | import { Router } from '@angular/router';
5 |
6 | @Component({
7 | selector: 'app-add-app',
8 | templateUrl: './add-app.component.html',
9 | styleUrls: ['./add-app.component.scss']
10 | })
11 | export class AddAppComponent implements OnInit {
12 |
13 | configString = `{
14 | apiKey: "AIzaSyCzpisEJhHYFR09Rh48NAQX6g3gwG2v2U0",
15 | authDomain: "firestore-query-browser.firebaseapp.com",
16 | databaseURL: "https://firestore-query-browser.firebaseio.com",
17 | projectId: "firestore-query-browser",
18 | storageBucket: "firestore-query-browser.appspot.com",
19 | messagingSenderId: "567385024694"
20 | }`
21 |
22 | constructor(
23 | private appsService: AppsService,
24 | private snackbar: MatSnackBar,
25 | private router: Router
26 | ) { }
27 |
28 | ngOnInit() {
29 | }
30 |
31 | submitNew() {
32 | try {
33 | this.appsService.newApp(this.configString)
34 | this.router.navigate(['/'])
35 | } catch (e) {
36 | this.snackbar.open(e, 'OK', { duration: 5000 })
37 | }
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/components/app-switcher/app-switcher.component.html:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
--------------------------------------------------------------------------------
/src/app/components/app-switcher/app-switcher.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechenberger/firestore-query-browser/d4fc7180508a46b14a81ff6a0b66eecf1afe15da/src/app/components/app-switcher/app-switcher.component.scss
--------------------------------------------------------------------------------
/src/app/components/app-switcher/app-switcher.component.ts:
--------------------------------------------------------------------------------
1 | import { AppsService } from './../../services/apps.service';
2 | import { Component, OnInit } from '@angular/core'
3 | import { MatSnackBar } from '@angular/material';
4 |
5 | @Component({
6 | selector: 'app-app-switcher',
7 | templateUrl: './app-switcher.component.html',
8 | styleUrls: ['./app-switcher.component.scss']
9 | })
10 | export class AppSwitcherComponent implements OnInit {
11 | apps
12 |
13 | constructor(
14 | public appsService: AppsService,
15 | private snackbar: MatSnackBar
16 | ) {
17 | this.reloadApps()
18 | }
19 |
20 | ngOnInit() {
21 | this.appsService.activeProjectIdChanged
22 | .do(() => this.reloadApps())
23 | .subscribe(() => null)
24 | }
25 |
26 | reloadApps() {
27 | this.apps = this.appsService.apps()
28 | }
29 |
30 | pick(projectId) {
31 | this.appsService.activeProjectId = projectId
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/components/auth-switcher/auth-switcher.component.html:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
32 |
33 |
34 |
38 |
39 |
--------------------------------------------------------------------------------
/src/app/components/auth-switcher/auth-switcher.component.scss:
--------------------------------------------------------------------------------
1 | :host{
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
6 | button {
7 | align-self: flex-start;
8 | }
9 |
10 | mat-radio-group {
11 | display: flex;
12 | flex-direction: column;
13 | margin: -8px 0 0;
14 | mat-radio-button {
15 | margin: 8px 0;
16 | }
17 | }
--------------------------------------------------------------------------------
/src/app/components/auth-switcher/auth-switcher.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core'
2 | import { AuthSwitcherService } from '../../services/auth-switcher.service'
3 | import * as _ from 'lodash'
4 | import { AuthService } from '../../services/auth.service';
5 |
6 | @Component({
7 | selector: 'app-auth-switcher',
8 | templateUrl: './auth-switcher.component.html',
9 | styleUrls: ['./auth-switcher.component.scss']
10 | })
11 | export class AuthSwitcherComponent implements OnInit {
12 |
13 | constructor(
14 | public authSwitcher: AuthSwitcherService,
15 | public auth: AuthService
16 | ) { }
17 |
18 | users = this.authSwitcher.watchUsers()
19 | .map(users => _.map(users))
20 |
21 | activeUser
22 |
23 | ngOnInit() {
24 | }
25 |
26 | switchUser(email) {
27 | this.users
28 | .take(1)
29 | .map(users => _.find(users, user => user.email === email))
30 | .do(user => this.authSwitcher.switchUser(user))
31 | .subscribe()
32 | }
33 |
34 |
35 | isLoggedIn() {
36 | return !!this.auth.currentUser
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/components/create-dialog/create-dialog.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Creating
5 |
6 |
7 |
8 |
9 |
10 | {{ error }}
11 |
12 |
13 | {{(doneCount | async)}}
14 | /
15 | {{lengthToCreate}}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/app/components/create-dialog/create-dialog.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | width: 600px;
4 | max-width: calc(80vw - 100px);
5 | }
6 |
7 | mat-form-field {
8 | width: 100%;
9 | }
10 |
11 | textarea {
12 | font-family: 'Roboto Mono', monospace;
13 | font-size: 16px;
14 | height: 400px;
15 | }
16 |
17 | .counter {
18 | text-align: right;
19 | color: rgba(0,0,0,0.58);
20 | margin: 4px 0;
21 | }
22 |
23 | .actions {
24 | display: flex;
25 | flex-direction: row-reverse;
26 | margin-top: 16px;
27 | }
28 |
29 | .errored {
30 | .error {
31 | color: red;
32 | }
33 | }
--------------------------------------------------------------------------------
/src/app/components/create-dialog/create-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Inject } from '@angular/core'
2 | import { DataService } from '../../services/data.service'
3 | import { MatDialogRef, MAT_DIALOG_DATA, MatSnackBar } from '@angular/material'
4 | import * as _ from 'lodash'
5 | import { Observable } from 'rxjs'
6 |
7 | export interface CreateDialogOptions {
8 | path: string
9 | }
10 | @Component({
11 | selector: 'app-create-dialog',
12 | templateUrl: './create-dialog.component.html',
13 | styleUrls: ['./create-dialog.component.scss']
14 | })
15 | export class CreateDialogComponent implements OnInit {
16 |
17 | constructor(
18 | private data: DataService,
19 | public dialogRef: MatDialogRef,
20 | @Inject(MAT_DIALOG_DATA) public options: CreateDialogOptions,
21 | private snackbar: MatSnackBar
22 | ) { }
23 |
24 | input: ''
25 | lengthToCreate = 0
26 | loading = false
27 | done = false
28 | doneCount: Observable
29 | percentage
30 | error: string
31 |
32 | ngOnInit() {
33 | }
34 |
35 |
36 | doIt() {
37 | let createDocs
38 | try {
39 | createDocs = JSON.parse(this.input)
40 | } catch (error) {
41 | this.snackbar.open(error.toString(), 'OK', { duration: 4000 })
42 | return
43 | }
44 |
45 | createDocs = Array.isArray(createDocs) ? createDocs : [createDocs]
46 |
47 | this.lengthToCreate = createDocs.length
48 |
49 | if (this.lengthToCreate === 0) return
50 |
51 | this.loading = true
52 |
53 | this.doneCount = this.data.createMultiple(this.options.path, createDocs)
54 | .publishReplay(1)
55 | .refCount()
56 |
57 | this.percentage = Observable.combineLatest([this.doneCount])
58 | .map(([done]) => Math.floor(100 * done / this.lengthToCreate))
59 |
60 | this.doneCount
61 | .takeLast(1)
62 | .catch(error => {
63 | const err = error.toString()
64 | this.error = err
65 | this.snackbar.open(err, 'OK', { duration: 4000 })
66 | return Observable.of(null)
67 | })
68 | .finally(() => this.done = true)
69 | .subscribe(() => null)
70 | }
71 |
72 | answer(value) {
73 | this.dialogRef.close(value)
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/src/app/components/delete-dialog/delete-dialog.component.html:
--------------------------------------------------------------------------------
1 |
2 |
{{deleting?'Deleting':'Delete'}}
3 |
4 |
5 |
6 | {{(doneCount | async)}}
7 | /
8 | {{(results | async)?.length}}
9 |
10 |
11 |
12 |
13 |
14 |
15 | Do you really want to delete
16 | {{length}} {{length==1?'entry':'entries'}}?
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/app/components/delete-dialog/delete-dialog.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | width: 600px;
4 | max-width: calc(80vw - 100px);
5 | }
6 |
7 | .counter {
8 | text-align: right;
9 | color: rgba(0,0,0,0.58);
10 | margin: 4px 0;
11 | }
12 |
13 | .actions {
14 | display: flex;
15 | flex-direction: row-reverse;
16 | margin-top: 16px;
17 | }
18 |
19 | mat-dialog-content {
20 | height: 54px;
21 | }
--------------------------------------------------------------------------------
/src/app/components/delete-dialog/delete-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Inject } from '@angular/core'
2 | import { DataService } from '../../services/data.service'
3 | import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'
4 | import * as _ from 'lodash'
5 | import { Observable } from 'rxjs'
6 |
7 | @Component({
8 | selector: 'app-delete-dialog',
9 | templateUrl: './delete-dialog.component.html',
10 | styleUrls: ['./delete-dialog.component.scss']
11 | })
12 | export class DeleteDialogComponent implements OnInit {
13 |
14 | constructor(
15 | private data: DataService,
16 | public dialogRef: MatDialogRef,
17 | @Inject(MAT_DIALOG_DATA) public options: any
18 | ) { }
19 |
20 | results = this.data.get(this.options)
21 | .then(entries => Array.isArray(entries)
22 | ? entries
23 | : [entries]
24 | )
25 |
26 | doneCount: Observable
27 |
28 | percentage: Observable
29 |
30 | deleting = false
31 | deleted = false
32 |
33 | ngOnInit() {
34 | this.initResults()
35 | }
36 |
37 | async initResults() {
38 |
39 | }
40 |
41 | answer(value) {
42 | this.dialogRef.close(value)
43 | }
44 |
45 | doIt() {
46 | this.deleting = true
47 | this.doneCount = Observable.from(this.results)
48 | .map(entries => _.map(entries, e => e.path))
49 | .switchMap(paths => this.data.deletePaths(paths))
50 | .publishReplay(1)
51 | .refCount()
52 |
53 | this.percentage = Observable.combineLatest([this.doneCount, this.results])
54 | .map(([done, results]) => Math.floor(100 * done / results.length))
55 |
56 | this.doneCount
57 | .takeLast(1)
58 | .do(() => this.deleted = true)
59 | .subscribe(() => null)
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/src/app/components/edit-dialog/edit-dialog.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Editing
5 | {{options.paths.length}}
6 | {{options.paths.length==1?'entry':'entries'}}
7 |
8 | Override?
9 |
10 |
11 |
12 |
13 | {{ error }}
14 |
15 |
16 | {{(doneCount | async)}}
17 | /
18 | {{options.paths.length}}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/app/components/edit-dialog/edit-dialog.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | width: 600px;
4 | max-width: calc(80vw - 100px);
5 | }
6 |
7 | mat-form-field {
8 | width: 100%;
9 | }
10 |
11 | textarea {
12 | font-family: 'Roboto Mono', monospace;
13 | font-size: 16px;
14 | height: 400px;
15 | }
16 |
17 | .counter {
18 | text-align: right;
19 | color: rgba(0,0,0,0.58);
20 | margin: 4px 0;
21 | }
22 |
23 | .actions {
24 | display: flex;
25 | flex-direction: row-reverse;
26 | margin-top: 16px;
27 | }
28 |
29 | .errored {
30 | .error {
31 | color: red;
32 | }
33 | }
--------------------------------------------------------------------------------
/src/app/components/edit-dialog/edit-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Inject } from '@angular/core'
2 | import { DataService } from '../../services/data.service'
3 | import { MatDialogRef, MAT_DIALOG_DATA, MatSnackBar } from '@angular/material'
4 | import * as _ from 'lodash'
5 | import { Observable } from 'rxjs'
6 |
7 | export interface EditDialogOptions {
8 | paths: string[]
9 | template?: any
10 | }
11 |
12 | @Component({
13 | selector: 'app-edit-dialog',
14 | templateUrl: './edit-dialog.component.html',
15 | styleUrls: ['./edit-dialog.component.scss']
16 | })
17 | export class EditDialogComponent implements OnInit {
18 |
19 | constructor(
20 | private data: DataService,
21 | public dialogRef: MatDialogRef,
22 | @Inject(MAT_DIALOG_DATA) public options: EditDialogOptions,
23 | private snackbar: MatSnackBar
24 | ) { }
25 |
26 | override = false
27 | newEntity = `{
28 |
29 | }`
30 | loading = false
31 | done = false
32 | doneCount
33 | percentage
34 | error: string
35 |
36 | ngOnInit() {
37 | if (this.options.template) {
38 | this.newEntity = JSON.stringify(this.options.template, null, ' ')
39 | }
40 | }
41 |
42 | doIt() {
43 | let newEntity
44 | try {
45 | newEntity = JSON.parse(this.newEntity)
46 | } catch (error) {
47 | this.snackbar.open(error.toString(), 'OK', { duration: 4000 })
48 | return
49 | }
50 |
51 | this.loading = true
52 |
53 | this.doneCount = this.data.editMultiple(this.options.paths, newEntity, this.override)
54 | .publishReplay(1)
55 | .refCount()
56 |
57 | this.percentage = Observable.combineLatest([this.doneCount])
58 | .map(([done]) => Math.floor(100 * done / this.options.paths.length))
59 |
60 | this.doneCount
61 | .takeLast(1)
62 | .catch(error => {
63 | const err = error.toString()
64 | this.error = err
65 | this.snackbar.open(err, 'OK', { duration: 4000 })
66 | return Observable.of(null)
67 | })
68 | .finally(() => this.done = true)
69 | .subscribe(() => null)
70 | }
71 |
72 | answer(value) {
73 | this.dialogRef.close(value)
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/src/app/components/editor/editor.component.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/app/components/editor/editor.component.scss:
--------------------------------------------------------------------------------
1 | :host, mat-form-field {
2 | width: 100%;
3 | display: block;
4 | position: relative;
5 | }
6 |
7 | textarea {
8 | height: 80px;
9 | width: 100%;
10 | }
11 |
12 | .next {
13 | width: 32px;
14 | height: 32px;
15 | line-height: 0;
16 | position: absolute;
17 | bottom: 9px;
18 | right: -4px;
19 | background: white;
20 | opacity: 0;
21 | transition: opacity .3s ease;
22 |
23 | &.visible {
24 | opacity: 1;
25 | }
26 | }
--------------------------------------------------------------------------------
/src/app/components/editor/editor.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, ViewChild, ElementRef, Input, Output, EventEmitter } from '@angular/core'
2 |
3 | @Component({
4 | selector: 'app-editor',
5 | templateUrl: './editor.component.html',
6 | styleUrls: ['./editor.component.scss']
7 | })
8 | export class EditorComponent implements OnInit {
9 |
10 | constructor() { }
11 |
12 | @ViewChild('editor') editor: ElementRef
13 |
14 | @Input() query: string
15 | @Output() queryChange = new EventEmitter()
16 | snippetRegex = /#\{([^#.]*)\}/
17 |
18 | ngOnInit() {
19 |
20 | }
21 |
22 | add(text: string, defaultPos = null) {
23 | const pos = typeof defaultPos === 'number' ? defaultPos : this.getCursorPos()
24 | let val = this.query
25 | const splitVal = val.split('')
26 | if (pos >= splitVal.length) {
27 | splitVal.push(text)
28 | } else {
29 | splitVal[pos] = `${text}${splitVal[pos]}`
30 | }
31 | val = splitVal.join('')
32 | this.setQuery(val)
33 | this.editor.nativeElement.focus()
34 |
35 | setTimeout(() => {
36 | if (this.snippetInQuery()) return this.selectNextSnippet(pos)
37 | const newCursorPos = pos + text.length
38 | this.setCursorPos(newCursorPos)
39 | }, 0)
40 | }
41 |
42 | indentedAdd(text) {
43 | const pos = this.getCursorPos()
44 | const isNewLine = pos === 0 ? true : this.query[pos - 1] === '\n'
45 | const newText = isNewLine ? ' ' + text : text
46 | this.add(newText)
47 | }
48 |
49 | tab(event) {
50 | event.preventDefault()
51 | if (this.snippetInQuery()) return this.selectNextSnippet()
52 | this.add(' ')
53 | }
54 |
55 | toggleComment() {
56 | const pos = this.getCursorPos()
57 | const posAfterNewLine = this.posAfterPrevNewLine()
58 | if (this.query.substr(posAfterNewLine, 2) === '//') {
59 | this.setQuery(this.query.substr(0, posAfterNewLine) + this.query.substr(posAfterNewLine + 2))
60 | setTimeout(() => {
61 | this.setCursorPos(pos)
62 | }, 0)
63 | return
64 | }
65 | this.add('//', posAfterNewLine)
66 | }
67 |
68 | addAfterLine(inputText) {
69 | let text = inputText
70 | if (this.query.indexOf('\n') === -1) {
71 | text = '\n' + text
72 | }
73 | const posAfterNewLine = this.posAfterNextNewLine()
74 | this.add(text, posAfterNewLine)
75 | }
76 |
77 | snippetInQuery() {
78 | return !!this.query.match(this.snippetRegex)
79 | }
80 |
81 | protected setQuery(query) {
82 | this.query = query
83 | this.queryChange.emit(this.query)
84 | }
85 |
86 | protected posAfterPrevNewLine() {
87 | const pos = this.getCursorPos()
88 | const text: string = this.query.substr(0, pos)
89 | return text.lastIndexOf('\n') + 1
90 | }
91 |
92 | protected posAfterNextNewLine() {
93 | const pos = this.getCursorPos()
94 | const text: string = this.query.substr(pos)
95 | return pos + text.indexOf('\n') + 1
96 | }
97 |
98 | protected getCursorPos() {
99 | return this.editor.nativeElement.selectionEnd
100 | }
101 |
102 | protected setCursorPos(pos) {
103 | this.editor.nativeElement.selectionStart = pos
104 | this.editor.nativeElement.selectionEnd = pos
105 | }
106 |
107 | protected selectNextSnippet(defaultPos = null) {
108 | const pos = this.getNextSnippetPos(defaultPos)
109 | this.selectText(pos)
110 | }
111 |
112 | protected getNextSnippetPos(defaultPos = null) {
113 | const pos = typeof defaultPos === 'number' ? defaultPos : this.getCursorPos()
114 | const upcomingText = this.query.substr(pos)
115 | const found = upcomingText.match(this.snippetRegex)
116 | if (!found) return pos === 0 ? null : this.getNextSnippetPos(0)
117 | return {
118 | start: pos + found.index,
119 | end: pos + found.index + found[0].length
120 | }
121 | }
122 |
123 | protected selectText(pos?: { start: number, end: number }) {
124 | if (!pos) return
125 | const { start, end } = pos
126 | this.editor.nativeElement.selectionStart = start
127 | this.editor.nativeElement.selectionEnd = end
128 | this.editor.nativeElement.focus()
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/app/components/entry-menu-button/entry-menu-button.component.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
10 |
14 |
18 |
--------------------------------------------------------------------------------
/src/app/components/entry-menu-button/entry-menu-button.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechenberger/firestore-query-browser/d4fc7180508a46b14a81ff6a0b66eecf1afe15da/src/app/components/entry-menu-button/entry-menu-button.component.scss
--------------------------------------------------------------------------------
/src/app/components/entry-menu-button/entry-menu-button.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input } from '@angular/core'
2 | import { DataService } from '../../services/data.service'
3 | import { DialogService } from '../../services/dialog.service'
4 | import { UtilService } from '../../services/util.service'
5 |
6 | @Component({
7 | selector: 'app-entry-menu-button',
8 | templateUrl: './entry-menu-button.component.html',
9 | styleUrls: ['./entry-menu-button.component.scss']
10 | })
11 | export class EntryMenuButtonComponent implements OnInit {
12 |
13 | @Input() entity
14 |
15 | constructor(
16 | private data: DataService,
17 | private dialog: DialogService,
18 | private util: UtilService
19 | ) { }
20 |
21 | ngOnInit() {
22 | }
23 |
24 | gotoConsole() {
25 | this.util.gotoConsole(this.entity)
26 | }
27 |
28 | delete() {
29 | return this.data.delete({
30 | path: this.entity.path
31 | })
32 | }
33 |
34 | edit() {
35 | this.dialog.edit({
36 | paths: [this.entity.path],
37 | template: this.entity.data
38 | })
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/components/login/login.component.html:
--------------------------------------------------------------------------------
1 | Login
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/app/components/login/login.component.scss:
--------------------------------------------------------------------------------
1 | .hidden {
2 | display: none;
3 | }
--------------------------------------------------------------------------------
/src/app/components/login/login.component.ts:
--------------------------------------------------------------------------------
1 | import { AuthService } from './../../services/auth.service'
2 | import { Component, OnInit, AfterViewInit, OnDestroy } from '@angular/core'
3 | import * as firebaseui from 'firebaseui'
4 | import * as firebase from 'firebase'
5 | import { Router } from '@angular/router'
6 |
7 | @Component({
8 | selector: 'app-login',
9 | templateUrl: './login.component.html',
10 | styleUrls: ['./login.component.scss']
11 | })
12 | export class LoginComponent implements OnInit, AfterViewInit, OnDestroy {
13 |
14 | constructor(
15 | public auth: AuthService,
16 | private router: Router
17 | ) { }
18 |
19 | checkInterval
20 |
21 | ngOnInit() {
22 | this.checkInterval = setInterval(() => {
23 | this.checkRedirect()
24 | }, 500)
25 | }
26 |
27 | ngOnDestroy() {
28 | clearInterval(this.checkInterval)
29 | }
30 |
31 | checkRedirect() {
32 | if (!!this.auth.currentUser) {
33 | return this.router.navigate(['/'])
34 | }
35 | }
36 |
37 | ngAfterViewInit() {
38 | try {
39 | this.initUI()
40 | } catch (error) {
41 | window.location.reload()
42 | }
43 | }
44 |
45 | initUI() {
46 | const ui = new firebaseui.auth.AuthUI(this.auth.auth)
47 | // The start method will wait until the DOM is loaded.
48 | const uiConfig = {
49 | signInSuccessUrl: '/',
50 | signInOptions: [
51 | // Leave the lines as is for the providers you want to offer your users.
52 | firebase.auth.GoogleAuthProvider.PROVIDER_ID,
53 | firebase.auth.FacebookAuthProvider.PROVIDER_ID,
54 | firebase.auth.TwitterAuthProvider.PROVIDER_ID,
55 | firebase.auth.GithubAuthProvider.PROVIDER_ID,
56 | firebase.auth.EmailAuthProvider.PROVIDER_ID,
57 | firebase.auth.PhoneAuthProvider.PROVIDER_ID
58 | ],
59 | // Terms of service url.
60 | // tosUrl: ''
61 | }
62 |
63 | ui.start('.firebaseui', uiConfig)
64 | }
65 |
66 | isLoggedIn() {
67 | return !!this.auth.currentUser
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/app/components/main/main.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
22 |
--------------------------------------------------------------------------------
/src/app/components/main/main.component.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | // @media (min-width: 1300px) {
4 | // app-login {
5 | // position: absolute;
6 | // right:-258px;
7 | // width: 250px;
8 | // }
9 | // }
10 |
11 | .footer {
12 | a, span {
13 | color: rgba(0, 0, 0, 0.78);
14 | flex:1;
15 | text-align: center;
16 | font-size: 12px;
17 | }
18 | }
19 |
20 | .main {
21 | position: relative;
22 | min-height: calc(100vh - 2 * 64px - 20px - 6 * 8px);
23 | padding: 16px;
24 | }
25 |
26 | .top-row {
27 | margin: -4px;
28 | &>* {
29 | margin: 4px;
30 | }
31 | align-items: flex-start;
32 | @media (max-width: 1000px) {
33 | flex-direction: column;
34 | align-items: stretch;
35 | }
36 | }
--------------------------------------------------------------------------------
/src/app/components/main/main.component.ts:
--------------------------------------------------------------------------------
1 | import { AuthService } from './../../services/auth.service'
2 | import { AppsService } from './../../services/apps.service'
3 | import { Component, OnInit } from '@angular/core'
4 | import { GoogleAnalyticsService } from '../../services/google-analytics.service'
5 |
6 | @Component({
7 | selector: 'app-main',
8 | templateUrl: './main.component.html',
9 | styleUrls: ['./main.component.scss']
10 | })
11 | export class MainComponent implements OnInit {
12 |
13 | constructor(
14 | public apps: AppsService,
15 | public auth: AuthService,
16 | public ga: GoogleAnalyticsService
17 | ) { }
18 |
19 | ngOnInit() {
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/components/query-actions/query-actions.component.html:
--------------------------------------------------------------------------------
1 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/app/components/query-actions/query-actions.component.scss:
--------------------------------------------------------------------------------
1 | .more-btn {
2 | min-width: 0;
3 | padding: 0 4px;
4 | }
--------------------------------------------------------------------------------
/src/app/components/query-actions/query-actions.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'
2 | import { DataService } from '../../services/data.service';
3 | import { DialogService } from '../../services/dialog.service';
4 |
5 | @Component({
6 | selector: 'app-query-actions',
7 | templateUrl: './query-actions.component.html',
8 | styleUrls: ['./query-actions.component.scss']
9 | })
10 | export class QueryActionsComponent implements OnInit {
11 |
12 | @Input() path
13 | @Input() query
14 | @Output() fetch = new EventEmitter()
15 |
16 | constructor(
17 | private dialog: DialogService,
18 | private data: DataService
19 | ) { }
20 |
21 | ngOnInit() {
22 | }
23 |
24 | async deleteResults() {
25 | await this.dialog.delete({
26 | path: this.path,
27 | query: this.query
28 | })
29 | .take(1)
30 | .toPromise()
31 |
32 | this.fetch.emit()
33 | }
34 |
35 | async create() {
36 | await this.dialog.create({
37 | path: this.path
38 | })
39 | .take(1)
40 | .toPromise()
41 |
42 | this.fetch.emit()
43 | }
44 |
45 | async editResults() {
46 | const res = await this.data.get({
47 | path: this.path,
48 | query: this.query
49 | })
50 | const collection = Array.isArray(res) ? res : [res]
51 |
52 | if (collection.length === 0) return
53 |
54 | const paths = collection.map(doc => doc.path)
55 | const template = collection[0].data
56 |
57 | await this.dialog.edit({
58 | template,
59 | paths
60 | })
61 | .take(1)
62 | .toPromise()
63 |
64 | this.fetch.emit()
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/app/components/query-browser-result/query-browser-result.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Result
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | JSON
14 |
15 |
16 | TABLE
17 |
18 |
19 |
20 |
1; else jsonTemplate">
21 |
22 |
23 |
24 | maxJsonLength">
25 | Only Showing {{maxJsonLength}} of {{entries.length}}
26 |
27 |
28 |
29 |
30 |
{{entity?.data || null | json}}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {{result | json}}
46 |
--------------------------------------------------------------------------------
/src/app/components/query-browser-result/query-browser-result.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | }
4 |
5 | pre {
6 | max-width: calc(100vw - 100px);
7 | overflow: hidden;
8 | text-overflow: ellipsis;
9 | }
10 |
11 | .list {
12 |
13 | }
14 |
15 | .item {
16 | display: flex;
17 | flex-direction: row;
18 | margin: 0 -24px;
19 | padding: 0 24px;
20 | // cursor: pointer;
21 | &:hover {
22 | background: rgba(0,0,0,0.12)
23 | }
24 | &> :first-child {
25 | flex: 1;
26 | }
27 | button {
28 | margin-top: 16px;
29 | }
30 | }
31 |
32 | app-result-table {
33 | margin: 0 -24px;
34 | @media (max-width: 600px) {
35 | margin: 0 -16px;
36 | }
37 | }
--------------------------------------------------------------------------------
/src/app/components/query-browser-result/query-browser-result.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnChanges, Input } from '@angular/core'
2 | import { AppsService } from '../../services/apps.service'
3 | import { DataService } from '../../services/data.service'
4 | import { DialogService } from '../../services/dialog.service'
5 | import { UtilService } from '../../services/util.service'
6 | import { ExportService } from '../../services/export.service'
7 | import * as _ from 'lodash'
8 |
9 | @Component({
10 | selector: 'app-query-browser-result',
11 | templateUrl: './query-browser-result.component.html',
12 | styleUrls: ['./query-browser-result.component.scss']
13 | })
14 | export class QueryBrowserResultComponent implements OnChanges {
15 |
16 | @Input() result: any
17 | @Input() path: string
18 |
19 | entries
20 | maxJsonLength = 10
21 | entriesForJson
22 |
23 | isCollection
24 |
25 | showTable = true
26 |
27 | constructor(
28 | private apps: AppsService,
29 | private data: DataService,
30 | private dialog: DialogService,
31 | private util: UtilService,
32 | private exportSrv: ExportService
33 | ) { }
34 |
35 | ngOnChanges(changes) {
36 | if (changes.result) {
37 | this.isCollection = Array.isArray(this.result)
38 | this.entries = this.isCollection
39 | ? this.result
40 | : [this.result]
41 |
42 | this.entriesForJson = _.chunk(this.entries, this.maxJsonLength)[0]
43 | }
44 | }
45 |
46 | exportCsv() {
47 | this.exportSrv.asCsv(this.path, this.entries)
48 | }
49 |
50 | exportJson() {
51 | this.exportSrv.asJson(this.path, this.entries)
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/src/app/components/query-browser/query-browser.component.html:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
32 |
34 |
35 |
39 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/app/components/query-browser/query-browser.component.scss:
--------------------------------------------------------------------------------
1 | .root {
2 | display: flex;
3 | width: 1300px;
4 | align-items: flex-start;
5 | }
6 |
7 | pre {
8 | font-family: 'Roboto Mono', monospace;
9 | font-size: 12px;
10 | }
11 |
12 | .fetch {
13 | margin-bottom: 18px;
14 | }
15 |
16 | .option {
17 | padding: 18px 0;
18 | }
19 |
20 | .results {
21 | margin-left: 12px;
22 | }
23 |
24 | @media (max-width: 1300px) {
25 | .root {
26 | flex-direction: column;
27 | width: 100%;
28 | align-items: stretch;
29 | }
30 | .results {
31 | margin-left: 0;
32 | }
33 | }
34 |
35 | .top-row {
36 | margin: -8px;
37 | &>* {
38 | margin: 8px !important;
39 | }
40 | align-items: stretch;
41 | mat-card {
42 | display: flex;
43 | flex-direction: column;
44 | margin-bottom: 0;
45 | }
46 | }
--------------------------------------------------------------------------------
/src/app/components/query-browser/query-browser.component.ts:
--------------------------------------------------------------------------------
1 | import { MatSnackBar } from '@angular/material'
2 | import { DataService } from './../../services/data.service'
3 | import { Component, OnInit, ViewChild } from '@angular/core'
4 | import { StorageService } from '../../services/storage.service'
5 | import * as _ from 'lodash'
6 | import { DialogService } from '../../services/dialog.service'
7 | import { AppsService } from '../../services/apps.service'
8 | import { HistoryService } from '../../services/history.service'
9 | import { Router } from '@angular/router'
10 | import { EditorComponent } from '../editor/editor.component';
11 | import { QueryService } from '../../services/query.service';
12 |
13 | @Component({
14 | selector: 'app-query-browser',
15 | templateUrl: './query-browser.component.html',
16 | styleUrls: ['./query-browser.component.scss']
17 | })
18 | export class QueryBrowserComponent implements OnInit {
19 | constructor(
20 | private data: DataService,
21 | private snackbar: MatSnackBar,
22 | private storage: StorageService,
23 | private dialog: DialogService,
24 | private appsSrv: AppsService,
25 | private router: Router,
26 | private historySrv: HistoryService,
27 | public query: QueryService
28 | ) { }
29 |
30 | @ViewChild('editor') editor: EditorComponent
31 |
32 | result = Promise.resolve(null)
33 | loading = false
34 |
35 | ngOnInit() {
36 | if (!this.appsSrv.activeProjectId) {
37 | this.router.navigate(['/add'])
38 | return
39 | }
40 |
41 | this.fetchResults({
42 | addToHistory: false
43 | })
44 | }
45 |
46 | async fetchResults(options = { addToHistory: true }) {
47 | const fq = await this.query.fullQuery.take(1).toPromise()
48 |
49 | const { addToHistory } = options
50 | this.loading = true
51 | this.result = this.data.get(fq)
52 |
53 | this.result
54 | .catch(err => this.snackbar.open(err, 'OK', { duration: 5000 }))
55 | .then(() => this.loading = false)
56 |
57 | if (addToHistory) {
58 | this.historySrv.addEntry(fq)
59 | }
60 | }
61 |
62 | addSnippet(snippet) {
63 | if (snippet === '__toggleComment__') return this.editor.toggleComment()
64 | this.editor.addAfterLine(' ' + snippet + '\n')
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/src/app/components/query-history/query-history.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 | {{entry.path}}
12 |
13 | {{entry.query}}
14 |
15 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/app/components/query-history/query-history.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | font-size: 12px;
3 | }
4 |
5 | .historyEntry {
6 | margin-bottom: 8px;
7 | }
8 | .historyEntry > button {
9 | visibility: hidden
10 | }
11 | .historyEntry:hover > button {
12 | visibility: visible
13 | }
14 |
15 | .queryText {
16 | max-width: calc(100vw - 140px);
17 | overflow: hidden;
18 | word-wrap: break-word;
19 | .query {
20 | color: rgba(0,0,0,0.48)
21 | }
22 | }
23 |
24 | .historyEntries {
25 | // max-height: calc(100vh - 140px);
26 | max-height: calc(277px - 2 * 24px);
27 | overflow-y: scroll;
28 | }
29 |
30 | .clickable {
31 | cursor: pointer;
32 | }
--------------------------------------------------------------------------------
/src/app/components/query-history/query-history.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'
2 | import { HistoryService } from '../../services/history.service'
3 | import { QueryService } from '../../services/query.service'
4 |
5 | @Component({
6 | selector: 'app-query-history',
7 | templateUrl: './query-history.component.html',
8 | styleUrls: ['./query-history.component.scss']
9 | })
10 | export class QueryHistoryComponent implements OnInit {
11 |
12 | // @Input() path
13 | // @Input() query
14 | // @Output() pathChange = new EventEmitter()
15 | // @Output() queryChange = new EventEmitter()
16 | @Output() fetch = new EventEmitter()
17 |
18 |
19 | historyEntries = this.historySrv.history
20 |
21 | constructor(
22 | public historySrv: HistoryService,
23 | private query: QueryService
24 | ) { }
25 |
26 | ngOnInit() {
27 | }
28 |
29 | async setAndFetch(entry) {
30 | await this.query.setFullQuery(entry)
31 | // if (entry.path) this.pathChange.emit(entry.path)
32 | // if (entry.query) this.queryChange.emit(entry.query)
33 | this.fetch.emit()
34 | }
35 |
36 |
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/components/query-input/query-input.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/app/components/query-input/query-input.component.scss:
--------------------------------------------------------------------------------
1 | .query, .path {
2 | font-family: 'Roboto Mono', monospace;
3 | font-size: 16px;
4 | }
--------------------------------------------------------------------------------
/src/app/components/query-input/query-input.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'
2 |
3 | @Component({
4 | selector: 'app-query-input',
5 | templateUrl: './query-input.component.html',
6 | styleUrls: ['./query-input.component.scss']
7 | })
8 | export class QueryInputComponent implements OnInit {
9 |
10 | @Input() path
11 | @Input() query
12 | @Output() pathChange = new EventEmitter()
13 | @Output() queryChange = new EventEmitter()
14 | @Output() fetch = new EventEmitter()
15 |
16 | constructor() { }
17 |
18 | ngOnInit() {
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/components/query-snippets/query-snippets.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app/components/query-snippets/query-snippets.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechenberger/firestore-query-browser/d4fc7180508a46b14a81ff6a0b66eecf1afe15da/src/app/components/query-snippets/query-snippets.component.scss
--------------------------------------------------------------------------------
/src/app/components/query-snippets/query-snippets.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'
2 |
3 | @Component({
4 | selector: 'app-query-snippets',
5 | templateUrl: './query-snippets.component.html',
6 | styleUrls: ['./query-snippets.component.scss']
7 | })
8 | export class QuerySnippetsComponent implements OnInit {
9 |
10 | @Output() addSnippet = new EventEmitter()
11 |
12 | snippets = [
13 | {
14 | name: 'where',
15 | content: `.where('#{prop}', '==', '#{val}')`,
16 | },
17 | {
18 | name: 'limit',
19 | content: `.limit(#{limit})`,
20 | },
21 | {
22 | name: 'orderBy',
23 | content: `.orderBy('#{prop}', '#{direction}')`,
24 | },
25 | {
26 | name: '//',
27 | content: `__toggleComment__`,
28 | },
29 | {
30 | name: 'startAt',
31 | content: `.startAt(#{pos})`,
32 | more: true
33 | },
34 | {
35 | name: 'endAt',
36 | content: `.endAt(#{pos})`,
37 | more: true
38 | },
39 | {
40 | name: 'startAfter',
41 | content: `.startAfter(#{pos})`,
42 | more: true
43 | },
44 | {
45 | name: 'endAfter',
46 | content: `.endAfter(#{pos})`,
47 | more: true
48 | },
49 | ]
50 |
51 | constructor() { }
52 |
53 | ngOnInit() {
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/components/result-table/result-table.component.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{key}}
13 |
14 | {{row[key] | json}}
15 |
16 |
17 |
18 |
19 |
20 |
21 | ID
22 | {{row.ID}}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/app/components/result-table/result-table.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | }
4 |
5 |
6 | .header {
7 | margin: 0 24px;
8 | display: flex;
9 | flex-direction: column;
10 | }
11 |
12 | mat-cell, mat-header-cell {
13 | // max-height: 3em;
14 | // overflow: hidden;
15 | // text-overflow: ellipsis;
16 | padding: 4px;
17 | // word-break: normal !important;
18 | }
19 |
20 | mat-cell, mat-header-cell {
21 | min-width: 100px;
22 | }
23 |
24 | .table-container {
25 | max-width: 100%;
26 | overflow-x: auto;
27 | display: flex;
28 | mat-table {
29 | flex: 1
30 | }
31 | }
32 |
33 | .actions {
34 | position: absolute;
35 | right: 0;
36 | background: white;
37 | min-width: 40px !important;
38 | flex: 0;
39 | }
--------------------------------------------------------------------------------
/src/app/components/result-table/result-table.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnChanges, Input, ViewChild, AfterViewInit } from '@angular/core'
2 | import { MatPaginator, MatSort, MatTableDataSource } from '@angular/material'
3 | import * as _ from 'lodash'
4 |
5 | @Component({
6 | selector: 'app-result-table',
7 | templateUrl: './result-table.component.html',
8 | styleUrls: ['./result-table.component.scss']
9 | })
10 | export class ResultTableComponent implements OnChanges, AfterViewInit {
11 |
12 | @Input() entries: any[]
13 |
14 | displayedColumns = ['id', 'name', 'progress', 'color']
15 | dataSource: MatTableDataSource = new MatTableDataSource()
16 |
17 | @ViewChild(MatPaginator) paginator: MatPaginator
18 | @ViewChild(MatSort) sort: MatSort
19 |
20 | keys
21 |
22 | constructor() { }
23 |
24 | ngOnChanges(changes) {
25 | if (changes.entries) {
26 | this.dataSource.data = _.map(this.entries, entry => ({
27 | ...entry.data,
28 | ID: entry.id,
29 | entity: entry
30 | }))
31 | this.calcKeys()
32 | }
33 | }
34 |
35 | calcKeys() {
36 | this.keys = _(this.entries)
37 | .map(entry => _.keys(entry.data))
38 | .flatten()
39 | .uniq()
40 | .value()
41 |
42 | this.displayedColumns = [
43 | 'ID',
44 | ...this.keys,
45 | '$actions'
46 | ]
47 | }
48 |
49 | ngAfterViewInit() {
50 | this.dataSource.paginator = this.paginator
51 | this.dataSource.sort = this.sort
52 | }
53 |
54 | applyFilter(filterValue: string) {
55 | filterValue = filterValue.trim() // Remove whitespace
56 | filterValue = filterValue.toLowerCase() // Datasource defaults to lowercase matches
57 | this.dataSource.filter = filterValue
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/components/toolbar/toolbar.component.html:
--------------------------------------------------------------------------------
1 |
2 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
56 |
--------------------------------------------------------------------------------
/src/app/components/toolbar/toolbar.component.scss:
--------------------------------------------------------------------------------
1 | a[mat-menu-item] {
2 | text-decoration: none !important;
3 | }
4 |
5 |
6 | @media (max-width: 999px) {
7 | .switcherTop {
8 | display: none;
9 | }
10 | }
11 | @media (min-width: 1000px) {
12 | .switcherBottom {
13 | display: none;
14 | }
15 | }
16 |
17 | .fqb-link {
18 | display: flex;
19 | align-items: center;
20 |
21 | &:hover {
22 | text-decoration: none;
23 | }
24 |
25 | img {
26 | height: 28px;
27 | margin-right: 12px;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/components/toolbar/toolbar.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core'
2 | import { AppsService } from '../../services/apps.service'
3 |
4 | @Component({
5 | selector: 'app-toolbar',
6 | templateUrl: './toolbar.component.html',
7 | styleUrls: ['./toolbar.component.scss']
8 | })
9 | export class ToolbarComponent implements OnInit {
10 |
11 | constructor(
12 | public apps: AppsService
13 | ) { }
14 |
15 | ngOnInit() {
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/modules/material.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core'
2 | import {
3 | MatButtonModule, MatCardModule, MatProgressBarModule, MatIconModule, MatMenuModule,
4 | MatToolbarModule, MatFormFieldModule, MatInputModule, MatExpansionModule, MatCheckboxModule,
5 | MatTooltipModule, MatListModule, MatSnackBarModule, MatChipsModule, MatProgressSpinnerModule,
6 | MatDialogModule, MatTabsModule, MatButtonToggle, MatButtonToggleModule, MatRadioModule, MatSlideToggleModule, MatPaginatorModule, MatSortModule
7 | } from '@angular/material'
8 | import { MatSelectModule } from '@angular/material/select'
9 | import { MatTableModule } from '@angular/material/table'
10 |
11 | const modules = [
12 | MatButtonModule,
13 | MatCardModule,
14 | MatProgressBarModule,
15 | MatIconModule,
16 | MatMenuModule,
17 | MatToolbarModule,
18 | MatFormFieldModule,
19 | MatInputModule,
20 | MatExpansionModule,
21 | MatCheckboxModule,
22 | MatTooltipModule,
23 | MatListModule,
24 | MatSelectModule,
25 | MatTableModule,
26 | MatSnackBarModule,
27 | MatChipsModule,
28 | MatProgressSpinnerModule,
29 | MatDialogModule,
30 | MatTabsModule,
31 | MatButtonToggleModule,
32 | MatRadioModule,
33 | MatSlideToggleModule,
34 | MatPaginatorModule,
35 | MatSortModule
36 | ]
37 |
38 | @NgModule({
39 | imports: modules,
40 | exports: modules,
41 | declarations: []
42 | })
43 | export class MaterialModule { }
--------------------------------------------------------------------------------
/src/app/services/apps.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 | import * as firebase from 'firebase'
3 | import * as _ from 'lodash'
4 | import { ReplaySubject } from 'rxjs'
5 | import { StorageService } from './storage.service'
6 |
7 | @Injectable()
8 | export class AppsService {
9 | constructor(
10 | public storage: StorageService
11 | ) {
12 | this.loadApps()
13 | this.activeProjectId = this.storage.get(this.activeProjectIdStorageKey)
14 | }
15 |
16 | activeApp
17 | private _activeProjectId
18 | activeProjectIdChanged = new ReplaySubject(1)
19 | activeProjectIdStorageKey = 'active-project-id'
20 | appsStorageKey = 'apps'
21 |
22 | get activeProjectId() {
23 | return this._activeProjectId
24 | }
25 | set activeProjectId(activeProjectId: string) {
26 | this._activeProjectId = activeProjectId
27 | this.activeProjectIdChanged.next(activeProjectId)
28 | if (this.activeProjectId) {
29 | this.storage.set(this.activeProjectIdStorageKey, this.activeProjectId)
30 | this.activeApp = _.find(firebase.apps, app => app.options.projectId === this.activeProjectId)
31 | }
32 | }
33 |
34 | string2config(configString: string): any {
35 | const json = configString.replace(/([a-zA-Z]*)(: ")/g, `"$1"$2`)
36 | const config = JSON.parse(json)
37 | return config
38 | }
39 |
40 | newApp(config: any | string, store = true) {
41 | // console.log('config', config)
42 | if (typeof config === 'string') config = this.string2config(config)
43 | const projectId = config.projectId
44 | firebase.initializeApp(config, projectId)
45 | if (store) {
46 | this.storeApps()
47 | this.activeProjectId = projectId
48 | }
49 | }
50 |
51 | storeApps() {
52 | const configs = firebase.apps.map(app => app.options)
53 | this.storage.set(this.appsStorageKey, configs)
54 | }
55 |
56 | loadApps() {
57 | const configs = this.storage.get(this.appsStorageKey)
58 | if (!configs) return
59 | configs.forEach(config => {
60 | this.newApp(config, false)
61 | })
62 | }
63 |
64 | apps() {
65 | return firebase.apps.map(app => app.options)
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/app/services/auth-switcher.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 | import { AuthService } from './auth.service'
3 | import { AppsService } from './apps.service'
4 | import { StorageService } from './storage.service'
5 |
6 | @Injectable()
7 | export class AuthSwitcherService {
8 |
9 | constructor(
10 | private auth: AuthService,
11 | private apps: AppsService,
12 | private storage: StorageService
13 | ) {
14 | this.auth.auth.onAuthStateChanged(user => {
15 | if (!user) return
16 | const currentUserData = this.currentUserData
17 | this.currentUsers = {
18 | ...this.currentUsers,
19 | [currentUserData.email]: currentUserData
20 | }
21 | })
22 | }
23 |
24 | key = 'users'
25 |
26 | getKey() {
27 | return `${this.key}-${this.apps.activeProjectId}`
28 | }
29 |
30 | public get currentUsers() {
31 | return this.storage.get(this.getKey(), {})
32 | }
33 |
34 | public set currentUsers(users) {
35 | this.storage.set(this.getKey(), users)
36 | }
37 |
38 | public get currentUserData() {
39 | return JSON.parse(localStorage.getItem(this.firebaseAuthKey))
40 | }
41 |
42 | public set currentUserData(userData) {
43 | const value = JSON.stringify(userData)
44 | localStorage.setItem(this.firebaseAuthKey, value)
45 | }
46 |
47 | public get firebaseAuthKey(): string {
48 | const { projectId, apiKey } = this.apps.activeApp.options
49 | return `firebase:authUser:${apiKey}:${projectId}`
50 | }
51 |
52 |
53 | watchUsers() {
54 | return this.apps.activeProjectIdChanged
55 | .asObservable()
56 | .switchMap(projectId => {
57 | return this.storage.watch(this.getKey(), {})
58 | .asObservable()
59 | })
60 | }
61 |
62 | switchUser(user) {
63 | this.currentUserData = user
64 | window.location.reload()
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/src/app/services/auth.service.ts:
--------------------------------------------------------------------------------
1 | import { AppsService } from './apps.service'
2 | import * as firebase from 'firebase'
3 | import { Injectable } from '@angular/core'
4 |
5 | @Injectable()
6 | export class AuthService {
7 |
8 | get currentUser() {
9 | return this.auth.currentUser
10 | }
11 |
12 | constructor(
13 | private apps: AppsService
14 | ) { }
15 |
16 | get auth() {
17 | return firebase.auth(this.apps.activeApp)
18 | }
19 |
20 | logout() {
21 | return this.auth.signOut()
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/services/data.service.ts:
--------------------------------------------------------------------------------
1 | import { AppsService } from './apps.service'
2 | import { Injectable } from '@angular/core'
3 | import * as firebase from 'firebase'
4 | import 'firebase/firestore'
5 | import { MAT_DIALOG_SCROLL_STRATEGY } from '@angular/material'
6 | import * as _ from 'lodash'
7 | import { Observable } from '@firebase/util'
8 | import { Subject, BehaviorSubject } from 'rxjs'
9 | import { AngularFirestoreDocument } from 'angularfire2/firestore'
10 |
11 | const DATETIME_REGEX = /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+)|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d)|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d)/
12 |
13 | @Injectable()
14 | export class DataService {
15 |
16 | constructor(
17 | private apps: AppsService
18 | ) {
19 |
20 | }
21 |
22 | private isCollection(path: string = '') {
23 | return path.split('/').length % 2 === 1
24 | }
25 |
26 | private snapshotToData(snap) {
27 | return {
28 | id: snap.id,
29 | data: snap.data(),
30 | path: snap.ref.path
31 | }
32 | }
33 |
34 | protected setWindowResult(result, options) {
35 | const win = window as any
36 | const windowResults = this.isCollection(options.path)
37 | ? _.map(result, item => item.data)
38 | : result.data
39 | win.r = windowResults
40 | win.result = windowResults
41 | }
42 |
43 | async get(options: any = {}) {
44 | const ref = this.ref(options)
45 |
46 | console.time('get')
47 | const docs = await ref.get()
48 | console.timeEnd('get')
49 |
50 | const result = this.isCollection(options.path)
51 | ? docs.docs.map(this.snapshotToData)
52 | : this.snapshotToData(docs)
53 |
54 | this.setWindowResult(result, options)
55 |
56 | return result
57 | }
58 |
59 | ref(options: any = {}) {
60 | options.path = options.path || ''
61 | const firestore = firebase.firestore(this.apps.activeApp)
62 |
63 | if (this.isCollection(options.path)) {
64 | let ref = firestore.collection(options.path)
65 |
66 | const query = options.query
67 | ? eval(options.query)
68 | : ref
69 |
70 | return query
71 | } else {
72 | return firestore.doc(options.path)
73 | }
74 | }
75 |
76 | async delete(options: any = {}) {
77 | return this.ref(options)
78 | .delete()
79 | }
80 |
81 | deletePaths(paths: string[]) {
82 | const chunks = _.chunk(paths, 20)
83 |
84 | const subject = new BehaviorSubject(0)
85 | let doneCount = 0
86 |
87 | const doIt = async () => {
88 | for (const chunk of chunks) {
89 | await Promise.all(_.map(chunk, path =>
90 | this.delete({
91 | path
92 | })
93 | .then(() => {
94 | doneCount++
95 | subject.next(doneCount)
96 | })
97 | ))
98 | }
99 | }
100 |
101 | doIt()
102 | .then(() => subject.complete())
103 |
104 | return subject.asObservable()
105 | }
106 |
107 | singleRef(path) {
108 | const firestore = firebase.firestore(this.apps.activeApp)
109 | const ref = firestore.doc(path)
110 | return ref
111 | }
112 |
113 | edit(path: string, doc: any, override = false) {
114 | const ref = this.singleRef(path)
115 | const betterDoc = this.dateify(doc)
116 | if (override) {
117 | return ref.set(betterDoc)
118 | } else {
119 | return ref.update(betterDoc)
120 | }
121 | }
122 |
123 | dateify(doc: any) {
124 | // Delete
125 | if (doc === '__del__') {
126 | return firebase.firestore.FieldValue.delete()
127 | }
128 |
129 | // String + Date
130 | if (typeof doc === 'string') {
131 | if (!doc.match(DATETIME_REGEX)) {
132 | return doc
133 | }
134 | const date = Date.parse(doc)
135 | return new Date(date)
136 | }
137 |
138 | // Null
139 | if (doc === null) {
140 | return null
141 | }
142 |
143 | // Array
144 | if (Array.isArray(doc)) {
145 | return _.map(doc, (val, key) => this.dateify(val))
146 | }
147 |
148 | // Object
149 | if (typeof doc === 'object') {
150 | return _.mapValues(doc, (val, key) => this.dateify(val))
151 | }
152 |
153 | return doc
154 | }
155 |
156 | editMultiple(paths: string[], doc: any, override = false) {
157 | const chunks = _.chunk(paths, 20)
158 |
159 | const subject = new BehaviorSubject(0)
160 | let doneCount = 0
161 |
162 | const doIt = async () => {
163 | for (const chunk of chunks) {
164 | await Promise.all(_.map(chunk, path =>
165 | this.edit(path, doc, override)
166 | .then(() => {
167 | doneCount++
168 | subject.next(doneCount)
169 | })
170 | .catch(err => subject.error(err))
171 | ))
172 | }
173 | }
174 |
175 | doIt()
176 | .then(() => subject.complete())
177 |
178 | return subject.asObservable()
179 |
180 | }
181 |
182 | create(path: string, doc: any) {
183 | const betterDoc = this.dateify(doc)
184 | const firestore = firebase.firestore(this.apps.activeApp)
185 | const collection = firestore.collection(path)
186 | const id = betterDoc.id
187 | const docRef = id ? collection.doc(id.toString()) : collection.doc()
188 | return docRef.set(betterDoc)
189 | }
190 |
191 | createMultiple(path: string, docs: any[]) {
192 | const chunks = _.chunk(docs, 20)
193 |
194 | const subject = new BehaviorSubject(0)
195 | let doneCount = 0
196 |
197 | const doIt = async () => {
198 | for (const chunk of chunks) {
199 | await Promise.all(_.map(chunk, doc =>
200 | this.create(path, doc)
201 | .then(() => {
202 | doneCount++
203 | subject.next(doneCount)
204 | })
205 | .catch(err => subject.error(err))
206 | ))
207 | }
208 | }
209 |
210 | doIt()
211 | .then(() => subject.complete())
212 |
213 | return subject.asObservable()
214 |
215 | }
216 |
217 | }
218 |
--------------------------------------------------------------------------------
/src/app/services/dialog.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 | import { EditDialogComponent, EditDialogOptions } from '../components/edit-dialog/edit-dialog.component'
3 | import { MatDialog } from '@angular/material'
4 | import { DeleteDialogComponent } from '../components/delete-dialog/delete-dialog.component'
5 | import { CreateDialogOptions, CreateDialogComponent } from '../components/create-dialog/create-dialog.component'
6 |
7 | @Injectable()
8 | export class DialogService {
9 |
10 | constructor(
11 | private dialog: MatDialog
12 | ) { }
13 |
14 | edit(options: EditDialogOptions) {
15 | return this.dialog.open(EditDialogComponent, { data: options })
16 | .afterClosed()
17 | }
18 |
19 | delete(options: any = {}) {
20 | return this.dialog.open(DeleteDialogComponent, { data: options })
21 | .afterClosed()
22 | }
23 |
24 | create(options: CreateDialogOptions) {
25 | return this.dialog.open(CreateDialogComponent, { data: options })
26 | .afterClosed()
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/services/export.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 | import { unparse } from 'papaparse'
3 | import * as _ from 'lodash'
4 | import * as dotize from 'dotize'
5 |
6 | @Injectable()
7 | export class ExportService {
8 |
9 | constructor() { }
10 |
11 | download(filename, text) {
12 | const element = document.createElement('a')
13 | element.setAttribute('href', 'data:text/plaincharset=utf-8,' + encodeURIComponent(text))
14 | element.setAttribute('download', filename)
15 |
16 | element.style.display = 'none'
17 | document.body.appendChild(element)
18 |
19 | element.click()
20 |
21 | document.body.removeChild(element)
22 | }
23 |
24 | asCsv(path: string, entries: any[]) {
25 | const filename = path.split('/').pop()
26 | const doc = _.map(entries, entry => dotize.convert(entry.data))
27 | const csv = unparse(doc)
28 | this.download(`${filename}.csv`, csv)
29 | }
30 |
31 | asJson(path: string, entries: any[]) {
32 | const filename = path.split('/').pop()
33 | const docs = _.map(entries, entry => entry.data)
34 | const json = JSON.stringify(docs, null, 2)
35 | this.download(`${filename}.json`, json)
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/services/google-analytics.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 | import { environment } from '../../environments/environment'
3 | import { NavigationEnd, Router } from '@angular/router'
4 |
5 | declare let window: any
6 | declare let dataLayer: any
7 | declare let ga: any
8 |
9 | @Injectable()
10 | export class GoogleAnalyticsService {
11 |
12 | constructor(
13 | private router: Router
14 | ) {
15 | if (!environment.production) return
16 | if (!environment.googleAnalytics.active) return
17 |
18 | window.dataLayer = window.dataLayer || []
19 |
20 | function gtag(...args: any[]) {
21 | dataLayer.push(arguments)
22 | }
23 |
24 | gtag('js', new Date())
25 | gtag('config', environment.googleAnalytics.id)
26 |
27 | this.router.events
28 | .do(event => {
29 | if (event instanceof NavigationEnd) {
30 | gtag('config', environment.googleAnalytics.id, { 'page_path': event.url })
31 | }
32 | })
33 | .subscribe(() => null)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/services/history.service.ts:
--------------------------------------------------------------------------------
1 | import { StorageService } from './storage.service'
2 | import { AppsService } from './apps.service'
3 | import { Injectable } from '@angular/core'
4 | import * as _ from 'lodash'
5 | import { Observable } from 'rxjs'
6 |
7 | @Injectable()
8 | export class HistoryService {
9 |
10 | constructor(
11 | private apps: AppsService,
12 | private storage: StorageService
13 | ) { }
14 |
15 | history: Observable = this.getKey()
16 | .switchMap(key => this.storage.watch<{}[]>(key, []))
17 | .publishReplay(1)
18 | .refCount()
19 |
20 | getKey() {
21 | return this.apps.activeProjectIdChanged
22 | .asObservable()
23 | .map(projectId => `history-${projectId}`)
24 | }
25 |
26 | addEntry(newEntry) {
27 | Observable.combineLatest([
28 | this.history,
29 | this.getKey()
30 | ])
31 | .take(1)
32 | .do(([entries, key]) => {
33 | const entriesWithoutNewEntry = this.removeEntryFromList(newEntry, entries)
34 | this.storage.set(key, [newEntry, ...entriesWithoutNewEntry])
35 | })
36 | .subscribe(() => null)
37 | }
38 |
39 | removeEntry(entry) {
40 | Observable.combineLatest([
41 | this.history,
42 | this.getKey()
43 | ])
44 | .take(1)
45 | .do(([entries, key]) => {
46 | const newEntries = this.removeEntryFromList(entry, entries)
47 | this.storage.set(key, newEntries)
48 | })
49 | .subscribe(() => null)
50 | }
51 |
52 | removeAllEntries() {
53 | this.getKey()
54 | .take(1)
55 | .do(key => {
56 | this.storage.set(key, [])
57 | })
58 | .subscribe(() => null)
59 | }
60 |
61 | protected removeEntryFromList(entry, list) {
62 | return _.filter(list, e => !(e.path === entry.path && e.query === entry.query))
63 | }
64 |
65 | async getFirstHistoryEntry() {
66 | const entries = await this.history.take(1).toPromise()
67 | if (!!entries && !!entries.length) {
68 | return entries[0]
69 | } else {
70 | return null
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/app/services/query.service.ts:
--------------------------------------------------------------------------------
1 | import { Router, ActivatedRoute } from '@angular/router'
2 | import { BehaviorSubject, Observable } from 'rxjs'
3 | import { Injectable } from '@angular/core'
4 | import * as _ from 'lodash'
5 | import { HistoryService } from './history.service'
6 |
7 | export interface FullQuery {
8 | path: string
9 | query: string
10 | }
11 |
12 | const defaultFullQuery = {
13 | path: 'dinosaurs',
14 | query: 'ref'
15 | } as FullQuery
16 |
17 | @Injectable()
18 | export class QueryService {
19 |
20 | constructor(
21 | private router: Router,
22 | private route: ActivatedRoute,
23 | private history: HistoryService
24 | ) {
25 |
26 | this.fullQuery
27 | .do(fq => this.fullQueryCached = fq)
28 | .subscribe()
29 |
30 | this.route.queryParams
31 | .subscribe()
32 |
33 | this.initFirstHistory()
34 | }
35 |
36 | private fullQueryDefault = new BehaviorSubject(_.clone(defaultFullQuery))
37 | private fullQueryCached = _.clone(defaultFullQuery)
38 |
39 | private fullQueryByRoute = this.route.queryParams
40 | .map(params => ({
41 | path: params.path,
42 | query: params.query
43 | } as FullQuery))
44 |
45 | private fullQueryByRouteFiltered = this.fullQueryByRoute
46 | .filter(fq => this.isFullQueryValid(fq))
47 |
48 | fullQuery = this.fullQueryDefault
49 | .take(1)
50 | .concat(this.fullQueryByRouteFiltered)
51 | .publishReplay(1)
52 | .refCount()
53 |
54 | async initFirstHistory() {
55 | const firstHistoryEntry = await this.history.getFirstHistoryEntry()
56 | if (!firstHistoryEntry) return
57 | const fqByRoute = await this.fullQueryByRoute.take(1).toPromise()
58 | if (!this.isFullQueryValid(fqByRoute)) {
59 | await this.setFullQuery(firstHistoryEntry)
60 | }
61 | }
62 |
63 |
64 | async setFullQuery(fq: FullQuery) {
65 | this.fullQueryCached = fq
66 | const queryParams = fq
67 | this.router.navigate([], { relativeTo: this.route, queryParams })
68 | }
69 |
70 | async setPath(path: string) {
71 | const fq = this.fullQueryCached
72 | await this.setFullQuery({
73 | ...fq,
74 | path
75 | })
76 | }
77 |
78 | async setQuery(query: string) {
79 | const fq = this.fullQueryCached
80 | await this.setFullQuery({
81 | ...fq,
82 | query
83 | })
84 | }
85 |
86 | isFullQueryValid(fq: Partial) {
87 | return !!fq && !!fq.path && !!fq.query
88 | }
89 |
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/src/app/services/storage.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 | import { Subject } from 'rxjs/Subject'
3 | import { BehaviorSubject } from 'rxjs/BehaviorSubject'
4 |
5 | @Injectable()
6 | export class StorageService {
7 | storage = window.localStorage
8 | subjects = {}
9 |
10 | constructor(
11 | ) { }
12 |
13 | get(key, defaultValue = null): T {
14 | const value = localStorage.getItem(this.getKey(key))
15 | if (value === null) {
16 | return defaultValue
17 | }
18 |
19 | let res
20 | try {
21 | res = JSON.parse(value)
22 | } catch (e) {
23 | res = value
24 | }
25 | return res
26 | }
27 |
28 | watch(key, defaultValue = null): BehaviorSubject {
29 | if (this.subjects[key]) {
30 | return this.subjects[key]
31 | }
32 |
33 | const value = this.get(key, defaultValue)
34 | const subject = new BehaviorSubject(value)
35 | this.subjects[key] = subject
36 | return subject
37 | }
38 |
39 | set(key, value) {
40 | const isString = typeof value === 'string'
41 | const newValue = isString
42 | ? value
43 | : JSON.stringify(value)
44 | localStorage.setItem(this.getKey(key), newValue)
45 | const subject = this.subjects[key]
46 | if (subject) subject.next(value)
47 | return value
48 | }
49 |
50 | remove(key) {
51 | return localStorage.removeItem(this.getKey(key))
52 | }
53 |
54 | getKey(key) {
55 | return `query-browser-${key}`
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/app/services/util.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 | import { AppsService } from './apps.service';
3 |
4 | @Injectable()
5 | export class UtilService {
6 |
7 | constructor(
8 | private apps: AppsService
9 | ) { }
10 |
11 | gotoConsole(entity: { path: string }) {
12 | const relativeUrl = entity.path.split('/').join('~2F')
13 | const url = `https://console.firebase.google.com/project/${this.apps.activeProjectId}/database/firestore/data~2F${relativeUrl}`
14 | window.open(url)
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechenberger/firestore-query-browser/d4fc7180508a46b14a81ff6a0b66eecf1afe15da/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | googleAnalytics: {
4 | active: true,
5 | id: 'UA-117802406-1'
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false,
8 | googleAnalytics: {
9 | active: false,
10 | id: ''
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rechenberger/firestore-query-browser/d4fc7180508a46b14a81ff6a0b66eecf1afe15da/src/favicon.png
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Firestore Query Browser
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | import 'hammerjs';
8 |
9 | if (environment.production) {
10 | enableProdMode();
11 | }
12 |
13 | platformBrowserDynamic().bootstrapModule(AppModule)
14 | .catch(err => console.log(err));
15 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/weak-map';
35 | // import 'core-js/es6/set';
36 |
37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
38 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
39 |
40 | /** IE10 and IE11 requires the following for the Reflect API. */
41 | // import 'core-js/es6/reflect';
42 |
43 |
44 | /** Evergreen browsers require these. **/
45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
46 | import 'core-js/es7/reflect';
47 |
48 |
49 | /**
50 | * Required to support Web Animations `@angular/platform-browser/animations`.
51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
52 | **/
53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
54 |
55 | /**
56 | * By default, zone.js will patch all possible macroTask and DomEvents
57 | * user can disable parts of macroTask/DomEvents patch by setting following flags
58 | */
59 |
60 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
61 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
62 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
63 |
64 | /*
65 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
66 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
67 | */
68 | // (window as any).__Zone_enable_cross_context_check = true;
69 |
70 | /***************************************************************************************************
71 | * Zone JS is required by default for Angular itself.
72 | */
73 | import 'zone.js/dist/zone'; // Included with Angular CLI.
74 |
75 |
76 |
77 | /***************************************************************************************************
78 | * APPLICATION IMPORTS
79 | */
80 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Roboto+Mono|Material+Icons');
2 | @import "~@angular/material/prebuilt-themes/indigo-pink.css";
3 | @import "~firebaseui/dist/firebaseui.css";
4 |
5 | html, body {
6 | background: #c9d6e8;
7 | }
8 |
9 | body {
10 | min-height: calc(100vh - 16px);
11 | background: linear-gradient(135deg, #ecf0f6 0%,#c9d6e8 100%);
12 | font-family: 'Roboto', sans-serif;
13 | padding: 0;
14 | margin: 0;
15 | }
16 |
17 | .main-container {
18 | width: 100%;
19 | box-sizing: border-box;
20 | // max-width: 1200px;
21 | margin: 0 auto;
22 | // padding: 8px;
23 | position: relative;
24 | }
25 |
26 | .flex-row {
27 | display: flex;
28 | flex-direction: row;
29 | justify-content: space-between;
30 | align-items: center;
31 | }
32 |
33 | .flex-row-responsive {
34 | display: flex;
35 | flex-direction: row;
36 | justify-content: space-between;
37 | align-items: center;
38 | @media (max-width:1000px) {
39 | flex-direction: column;
40 | align-items: stretch;
41 | }
42 | }
43 |
44 | .flex-1 {
45 | flex: 1
46 | }
47 |
48 | mat-card {
49 | margin-bottom: 16px;
50 | }
51 |
52 | mat-card > mat-progress-bar:first-child {
53 | position: absolute;
54 | top: 0;
55 | left: 0;
56 | right :0;
57 | }
58 |
59 | .fa-btn {
60 | font-size: 1.5em;
61 | }
62 |
63 | a {
64 | color: rgba(0,0,0,0.78);
65 | text-decoration: none;
66 | &:hover {
67 | text-decoration: underline;
68 | }
69 | }
70 |
71 | @media (max-width: 600px) {
72 | .short-on-mobile {
73 | display: inline-block;
74 | max-width: 100px;
75 | text-overflow: ellipsis;
76 | overflow: hidden;
77 | }
78 | }
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/zone-testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: any;
11 |
12 | // First, initialize the Angular testing environment.
13 | getTestBed().initTestEnvironment(
14 | BrowserDynamicTestingModule,
15 | platformBrowserDynamicTesting()
16 | );
17 | // Then we find all the tests.
18 | const context = require.context('./', true, /\.spec\.ts$/);
19 | // And load the modules.
20 | context.keys().map(context);
21 |
--------------------------------------------------------------------------------
/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "baseUrl": "./",
6 | "module": "es2015",
7 | "types": []
8 | },
9 | "exclude": [
10 | "test.ts",
11 | "**/*.spec.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "types": [
8 | "jasmine",
9 | "node"
10 | ]
11 | },
12 | "files": [
13 | "test.ts"
14 | ],
15 | "include": [
16 | "**/*.spec.ts",
17 | "**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /* SystemJS module definition */
2 | declare var module: NodeModule;
3 | interface NodeModule {
4 | id: string;
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./dist/out-tsc",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "target": "es5",
11 | "typeRoots": [
12 | "node_modules/@types"
13 | ],
14 | "lib": [
15 | "es2017",
16 | "dom"
17 | ]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": [
14 | true,
15 | "ignore-same-line"
16 | ],
17 | "deprecation": {
18 | "severity": "warn"
19 | },
20 | "eofline": true,
21 | "forin": true,
22 | "import-blacklist": [
23 | true,
24 | "rxjs",
25 | "rxjs/Rx"
26 | ],
27 | "import-spacing": true,
28 | "indent": [
29 | true,
30 | "spaces"
31 | ],
32 | "interface-over-type-literal": true,
33 | "label-position": true,
34 | "max-line-length": [
35 | true,
36 | 140
37 | ],
38 | "member-access": false,
39 | "member-ordering": [
40 | true,
41 | {
42 | "order": [
43 | "static-field",
44 | "instance-field",
45 | "static-method",
46 | "instance-method"
47 | ]
48 | }
49 | ],
50 | "no-arg": true,
51 | "no-bitwise": true,
52 | "no-construct": true,
53 | "no-debugger": true,
54 | "no-duplicate-super": true,
55 | "no-empty": false,
56 | "no-empty-interface": true,
57 | "no-eval": true,
58 | "no-inferrable-types": [
59 | true,
60 | "ignore-params"
61 | ],
62 | "no-misused-new": true,
63 | "no-non-null-assertion": true,
64 | "no-shadowed-variable": true,
65 | "no-string-literal": false,
66 | "no-string-throw": true,
67 | "no-switch-case-fall-through": true,
68 | "no-trailing-whitespace": true,
69 | "no-unnecessary-initializer": true,
70 | "no-unused-expression": true,
71 | "no-use-before-declare": true,
72 | "no-var-keyword": true,
73 | "object-literal-sort-keys": false,
74 | "one-line": [
75 | true,
76 | "check-open-brace",
77 | "check-catch",
78 | "check-else",
79 | "check-whitespace"
80 | ],
81 | "prefer-const": true,
82 | "quotemark": [
83 | true,
84 | "single"
85 | ],
86 | "radix": true,
87 | "semicolon": [
88 | true,
89 | "never"
90 | ],
91 | "triple-equals": [
92 | true,
93 | "allow-null-check"
94 | ],
95 | "typedef-whitespace": [
96 | true,
97 | {
98 | "call-signature": "nospace",
99 | "index-signature": "nospace",
100 | "parameter": "nospace",
101 | "property-declaration": "nospace",
102 | "variable-declaration": "nospace"
103 | }
104 | ],
105 | "unified-signatures": true,
106 | "variable-name": false,
107 | "whitespace": [
108 | true,
109 | "check-branch",
110 | "check-decl",
111 | "check-operator",
112 | "check-separator",
113 | "check-type"
114 | ],
115 | "directive-selector": [
116 | true,
117 | "attribute",
118 | "app",
119 | "camelCase"
120 | ],
121 | "component-selector": [
122 | true,
123 | "element",
124 | "app",
125 | "kebab-case"
126 | ],
127 | "no-output-on-prefix": true,
128 | "use-input-property-decorator": true,
129 | "use-output-property-decorator": true,
130 | "use-host-property-decorator": true,
131 | "no-input-rename": true,
132 | "no-output-rename": true,
133 | "use-life-cycle-interface": true,
134 | "use-pipe-transform-interface": true,
135 | "component-class-suffix": true,
136 | "directive-class-suffix": true
137 | }
138 | }
--------------------------------------------------------------------------------