├── .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 | [![Mentioned in awesome-firebase](https://awesome.re/mentioned-badge.svg)](https://github.com/jthegedus/awesome-firebase) 2 | 3 | # Firestore Query Browser 4 | 5 | ![Overview](https://github.com/rechenberger/firestore-query-browser/raw/master/docs/overview.jpg) 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 | 21 | 22 | 23 | 24 | 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 |
36 | 38 |
39 |
40 | 41 | 43 | 44 |
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 |
2 | 3 | 4 | 5 |
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 |
3 |
4 | 5 | Icon 6 | Firestore Query Browser 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 | 25 | 26 | 28 | 29 | 30 |
31 |
32 |
33 | 34 | 35 | 36 |
37 | 38 |
39 |
40 | 41 | 42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 | 51 | exit_to_app 52 | Getting Stated Guide 53 | 54 | 55 |
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 | } --------------------------------------------------------------------------------