├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── base.html ├── launch.py ├── mongs.py ├── readme ├── mongs.jpg └── mongs.png ├── requirements.txt └── www ├── %server ├── %database │ ├── %collection │ │ ├── %filter │ │ │ ├── %page.int │ │ │ │ └── index.html.spt │ │ │ ├── %value.json.spt │ │ │ ├── %value.txt.spt │ │ │ └── index.html.spt │ │ └── index.html.spt │ └── index.html.spt └── index.html.spt ├── favicon.ico └── index.html.spt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | servers.txt 3 | env 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | WORKDIR /app 4 | COPY . /app 5 | RUN python -m pip install -r requirements.txt 6 | 7 | ENV PORT 80 8 | EXPOSE 80 9 | CMD python -m launch 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .tox/python: 2 | python3 -m venv .tox/python 3 | .tox/python/bin/pip install -r requirements.txt 4 | 5 | clean: 6 | rm -rf .tox 7 | 8 | run: .tox/python 9 | .tox/python/bin/python -m launch 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mongs 2 | 3 | Mongs is a **web-based data browser** for MongoDB. 5 | 6 | 7 | 8 | ## Installation 9 | 10 | ``` 11 | $ git clone https://github.com/chadwhitacre/mongs.git 12 | $ cd mongs 13 | $ make run 14 | Greetings, program! Now serving on http://0.0.0.0:29017/. 15 | ``` 16 | 17 | Now visit http://0.0.0.0:29017/. :-) 18 | 19 | ## Usage 20 | 21 | Click to **browse**. 22 | 23 | You can **query** using JSON expressions in the URL, where the `{}` is in the 24 | above screenshot. 25 | 26 | You can **sort** by adding a `"sort": [["field", -1]]` key/val to the `{}` in 27 | the URL. It will be popped and the rest of the dict will simply be a 28 | [PyMongo](https://api.mongodb.com/python/current/) query. 29 | 30 | To add more **servers**, create (or link) a `servers.txt` file in the Mongs 31 | distribution root with one server hostname per line, e.g.: 32 | 33 | ``` 34 | $ cat servers.txt 35 | localhost 36 | internal-1.mycorp 37 | internal-2.mycorp 38 | ``` 39 | 40 | 41 |

Development

42 | 43 | Mongs is implemented in Python using the [Aspen](http://aspen.io/) web 44 | framework. 45 | 46 | 47 |

Bonus Picture

48 | 49 | Mongs is named after a dairy. Herewith, an old milk box repurposed to hold 50 | computer cables, showing the Mong's Dairy logo: 51 | 52 | Old timey milk box with Mong's logo 53 | 54 | 55 |

Legal

56 | 57 | Copyright © Chad Whitacre and contributors. MIT-licensed. 59 | -------------------------------------------------------------------------------- /base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Mongs - MongoDB Viewer 4 | 116 | 117 | 118 | {% block content %}{% endblock %} 119 | 120 | 121 | -------------------------------------------------------------------------------- /launch.py: -------------------------------------------------------------------------------- 1 | # copy almost all of pando.__main__ to set the www_root 2 | 3 | import os 4 | import logging.config 5 | from wsgiref.simple_server import make_server 6 | 7 | from pando import website 8 | from pando.logging import log_dammit 9 | 10 | 11 | logging_cfg = { 12 | 'version': 1, 13 | 'formatters': { 14 | 'threadinfo': { 15 | 'format': 16 | "%(asctime)s pid-%(process)d thread-%(thread)d " 17 | "(%(threadName)s) %(levelname)s: %(message)s", 18 | }, 19 | }, 20 | 'handlers': { 21 | 'console': { 22 | 'class': 'logging.StreamHandler', 23 | 'formatter': 'threadinfo', 24 | 'level': 'INFO', 25 | 'stream': 'ext://sys.stderr', 26 | }, 27 | }, 28 | 'root': { 29 | 'handlers': ['console'], 30 | }, 31 | } 32 | 33 | 34 | if __name__ == '__main__': 35 | logging.config.dictConfig(logging_cfg) 36 | port = int(os.environ.get('PORT', '8080')) 37 | host = os.environ.get('PANDO_HOST', '0.0.0.0') 38 | log_dammit( 39 | "Greetings, program! Now serving on http://{0}:{1}/." 40 | .format(host, port)) 41 | website = website.Website( 42 | www_root='www', 43 | renderer_default='jinja2', 44 | project_root='.', 45 | ) 46 | make_server(host, port, website).serve_forever() 47 | -------------------------------------------------------------------------------- /mongs.py: -------------------------------------------------------------------------------- 1 | """Helper functions for Mongs. 2 | """ 3 | import datetime 4 | import math 5 | import os 6 | import json 7 | 8 | import pymongo 9 | from bson.objectid import ObjectId, InvalidId 10 | 11 | 12 | def commaize(n, places=1): 13 | """Given a number, return a string with commas and a decimal -- 1,000.0. 14 | """ 15 | out = ("%%.0%df" % places) % n 16 | try: 17 | whole, fraction = out.split('.') 18 | except ValueError: 19 | whole, fraction = (out, '') 20 | _whole = [] 21 | for i, digit in enumerate(reversed(whole), start=1): 22 | _whole.insert(0, digit) 23 | if i % 3 == 0: 24 | _whole.insert(0, ',') 25 | out = ''.join(_whole + ['.', fraction]).lstrip(',').rstrip('.') 26 | return out 27 | 28 | 29 | def get_single_document_filter(_id): 30 | filter = {"_id": {"$in": [_id]}} 31 | try: 32 | # Under some combination of MongoDB/PyMongo versions, this is 33 | # necessary. 34 | object_id = ObjectId(_id) 35 | except InvalidId: 36 | pass 37 | else: 38 | filter['_id']['$in'].append(object_id) 39 | return filter 40 | 41 | 42 | def get_value(request): 43 | """Given a request object, return a value. Use for *.txt and *.json. 44 | """ 45 | server = request.path['server'] 46 | database = request.path['database'] 47 | collection = request.path['collection'] 48 | _id = request.path['filter'] 49 | key = request.path['value'] # derp 50 | 51 | db = pymongo.MongoClient(server)[database][collection] 52 | filter = get_single_document_filter(_id) 53 | document = db.find_one(filter) 54 | return document[key] 55 | 56 | 57 | def dt2age(dt): 58 | """Given a Unix timestamp (UTC) or a datetime object, return an age string 59 | relative to now. 60 | 61 | range denomination example 62 | ====================================================================== 63 | 0-1 second "just a moment" 64 | 1-59 seconds seconds 13 seconds 65 | 60 sec - 59 min minutes 13 minutes 66 | 60 min - 23 hrs, 59 min hours 13 hours 67 | 24 hrs - 13 days, 23 hrs, 59 min days 13 days 68 | 14 days - 27 days, 23 hrs, 59 min weeks 3 weeks 69 | 28 days - 12 months, 31 days, 23 hrs, 59 mn months 6 months 70 | 1 year - years 1 year 71 | 72 | We'll go up to years for now. 73 | 74 | Times in the future are indicated by "in (denomination)" and times 75 | already passed are indicated by "(denomination) ago". 76 | 77 | """ 78 | 79 | if not isinstance(dt, datetime.datetime): 80 | dt = datetime.datetime.utcfromtimestamp(dt) 81 | 82 | # Define some helpful constants. 83 | # ============================== 84 | 85 | sec = 1 86 | min = 60 * sec 87 | hr = 60 * min 88 | day = 24 * hr 89 | wk = 7 * day 90 | mn = 4 * wk 91 | yr = 365 * day 92 | 93 | # Get the raw age in seconds. 94 | # =========================== 95 | 96 | now = datetime.datetime.utcnow() 97 | age = abs(now - dt).total_seconds() 98 | 99 | # Convert it to a string. 100 | # ======================= 101 | # We start with the coarsest unit and filter 102 | # to the finest. Pluralization is centralized. 103 | 104 | if age < 1: 105 | return 'just a moment' 106 | 107 | elif age >= yr: # years 108 | amount = age / yr 109 | unit = 'year' 110 | elif age >= mn: # months 111 | amount = age / mn 112 | unit = 'month' 113 | elif age >= (2 * wk): # weeks 114 | amount = age / wk 115 | unit = 'week' 116 | elif age >= day: # days 117 | amount = age / day 118 | unit = 'day' 119 | elif age >= hr: # hours 120 | amount = age / hr 121 | unit = 'hour' 122 | elif age >= min: # minutes 123 | amount = age / min 124 | unit = 'minute' 125 | else: # seconds 126 | amount = age 127 | unit = 'second' 128 | 129 | # Pluralize and return. 130 | # ===================== 131 | 132 | amount = int(math.floor(amount)) 133 | if amount != 1: 134 | unit += 's' 135 | age = ' '.join([str(amount), unit]) 136 | fmt = 'in {age}' if dt > now else '{age} ago' 137 | return fmt.format(age=age) 138 | 139 | 140 | def has_documents(coll): 141 | """ 142 | Return a boolean for the presence of documents in the collection. 143 | """ 144 | return bool(coll.count()) 145 | 146 | 147 | def connect(server): 148 | """ 149 | Allow SERVER_URIS to override the URI for a server. 150 | """ 151 | full_uris = json.loads(os.environ.get('SERVER_URIS', '{}')) 152 | return pymongo.MongoClient(full_uris.get(server, server)) 153 | -------------------------------------------------------------------------------- /readme/mongs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chadwhitacre/mongs/945662a575d1f20a02d9fb8397788a45cdded556/readme/mongs.jpg -------------------------------------------------------------------------------- /readme/mongs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chadwhitacre/mongs/945662a575d1f20a02d9fb8397788a45cdded556/readme/mongs.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pando 2 | pymongo 3 | dnspython 4 | aspen-jinja2 5 | -------------------------------------------------------------------------------- /www/%server/%database/%collection/%filter/%page.int/index.html.spt: -------------------------------------------------------------------------------- 1 | ../index.html.spt -------------------------------------------------------------------------------- /www/%server/%database/%collection/%filter/%value.json.spt: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from bson import json_util 4 | 5 | import mongs 6 | [---] 7 | value = mongs.get_value(request) 8 | if isinstance(value, str): 9 | # Smartly send the user to a text/plain representation, so their in-browser 10 | # JSON prettifier doesn't choke. 11 | path, ext, _ = request.path.raw.rpartition('.json') 12 | request.redirect(path + ".txt") 13 | response.body = json.dumps(value, default=json_util.default) 14 | raise response 15 | [---] 16 | -------------------------------------------------------------------------------- /www/%server/%database/%collection/%filter/%value.txt.spt: -------------------------------------------------------------------------------- 1 | import mongs 2 | 3 | # Never smartly send the user to a json representation. This is the lowest 4 | # common denominator and should always be available to the user. 5 | 6 | [---] 7 | [---] 8 | {{ mongs.get_value(request) }} 9 | -------------------------------------------------------------------------------- /www/%server/%database/%collection/%filter/index.html.spt: -------------------------------------------------------------------------------- 1 | """Spaghetti! Who likes spaghetti? It's mildly documented spaghetti, at least. 2 | 3 | Okay, I moved a couple functions out of here to .aspen/mongs.py. So now maybe 4 | more like ziti. Alright, angelhair. Can you give me angelhair? 5 | 6 | """ 7 | import datetime 8 | import pprint 9 | import functools 10 | import urllib.parse 11 | 12 | import mongs 13 | import bson 14 | from pando import json, Response 15 | from bson.objectid import ObjectId, InvalidId 16 | 17 | SIZE_THRESHOLD = 2048 # number of bytes above which we link out 18 | 19 | class Pair: 20 | """Represent a single key/value pair from a document. 21 | """ 22 | 23 | is_filtered = False 24 | is_indexed = False 25 | 26 | def __init__(self, base, _id, k, v): 27 | escape = True 28 | if isinstance(v, (bson.Binary, bytes)): 29 | v = repr(v) 30 | if isinstance(v, ObjectId): 31 | v = '%s (%s)' % (v, v.generation_time) 32 | if isinstance(v, datetime.datetime): 33 | v = "%s (%s)" % (v.isoformat(), mongs.dt2age(v)) 34 | if isinstance(v, (int, float)): 35 | v = "%s" % v 36 | escape = False 37 | nv = len(str(v)) 38 | link = '' 39 | if k == '_id': 40 | link = "%s/%s/" % (base, _id) 41 | elif _id and nv > SIZE_THRESHOLD: 42 | v = "%d bytes" % nv 43 | link = "%s/%s/%s.json" % (base, _id, k) 44 | else: 45 | if not isinstance(v, str): 46 | v = pprint.pformat(v, indent=1) 47 | if escape: 48 | v = v.replace('&', '&').replace('<', '<').replace('>', '>') 49 | if '\n' in v: 50 | v = "
%s
" % v 51 | 52 | self.k = k 53 | self.v = v 54 | self.link = link 55 | 56 | 57 | [----] 58 | 59 | # Parse input 60 | # =========== 61 | 62 | server = request.path['server'] 63 | database = request.path['database'] 64 | collection = request.path['collection'] 65 | filter = request.path['filter'] 66 | page = request.path.get('page', None) 67 | single = page is None 68 | filtered = single 69 | 70 | 71 | # Compute base, filter, and page. 72 | # =============================== 73 | # This simplate is symlinked to be called in two contexts: 74 | # 75 | # - when a document is specified by a page number into a number of documents 76 | # matched by a filter 77 | # - when a single document is specified by its _id 78 | # 79 | # Base is used to compute links, filter is the query spec, and page is the 80 | # 1-index into the query results. 81 | 82 | optimize_count = False # If we can, compute the count without filtering. 83 | 84 | if not single: # /server/database/collection/filter/page/ 85 | # Parse the filter as JSON, possibly base64-encoded. 86 | base = '../..' 87 | filter = urllib.parse.unquote(filter).strip() 88 | if filter: 89 | filtered = True 90 | if not filter.startswith('{'): 91 | filter = filter.decode('base64') 92 | filter = json.loads(filter) 93 | else: 94 | filter = {} 95 | if not filter: 96 | # If there is no filter, we can safely compute the count from the 97 | # unfiltered collection, which appears to be O(1) instead of O(N). 98 | optimize_count = True 99 | 100 | else: # /server/database/collection/_id/ 101 | # Convert a request for a specific _id into a filter with one page. 102 | page = 1 103 | base = '..' 104 | filter = mongs.get_single_document_filter(filter) 105 | 106 | 107 | # Sort. 108 | # ===== 109 | # The user passes sort in as part of the {filter} hash, which otherwise is a 110 | # MongoDB query spec. Pull sort out after the filter has been decoded but 111 | # before we actually use it. 112 | 113 | sort = filter.pop('sort', None) 114 | 115 | 116 | # Load documents. 117 | # =============== 118 | 119 | coll = mongs.connect(server)[database][collection] 120 | documents = coll.find(filter) 121 | ndocs = coll.count() if optimize_count else documents.count() 122 | if (page < 1) or (page > ndocs): 123 | raise Response(404) 124 | documents.rewind() 125 | 126 | 127 | # Sort. 128 | # ===== 129 | 130 | if sort: 131 | documents.sort(sort) 132 | 133 | 134 | # Compute prev/next. 135 | # ================== 136 | 137 | prev = None # or int 138 | next_ = None # or int 139 | if page > 1: 140 | prev = page - 1 141 | if page < ndocs: 142 | next_ = page + 1 143 | 144 | 145 | # Advance the cursor to the requested document. 146 | # ============================================= 147 | # This appears to be O(N), which means it is fast for early pages and slow for 148 | # late pages. 149 | 150 | document = next(documents.skip(page - 1)) 151 | 152 | 153 | # Compute a set of indexed keys. 154 | # ============================== 155 | 156 | indices = coll.index_information() 157 | indexed = set() 158 | for v in indices.values(): 159 | indexed.add(v['key'][0][0]) 160 | 161 | 162 | # Convert the document to a generator for the template. 163 | # ===================================================== 164 | 165 | if document is not None: # XXX Can document ever be None? 166 | _id = document.get('_id', '') 167 | def generate_pairs(document): 168 | """Yield key/value pairs for document. 169 | """ 170 | for k,v in sorted(document.items()): 171 | pair = Pair(base, _id, k, v) 172 | pair.is_filtered = False 173 | pair.is_indexed = pair.k in indexed 174 | pair.sort = '' 175 | if pair.k != '_id': 176 | pair.is_filtered = pair.k in filter 177 | if pair.k == sort: 178 | pair.sort = 'ascending' if direction > 0 else 'descending' 179 | yield pair 180 | pairs = generate_pairs(document) 181 | if single: 182 | # For documents that were specified by an explicit _id, we show 183 | # that _id above the rest of the key/values. Advancing the pairs 184 | # generator here means that we don't display _id again with the 185 | # rest of the key/values. 186 | next(pairs) 187 | 188 | #============================================================================ 189 | [----] 190 | {% extends "base.html" %} 191 | {% block content %} 192 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 216 | 225 | 226 | {% for pair in pairs %} 227 | 228 | 229 | 230 | 231 | {% endfor %} 232 |
Server{{ server }}
Database{{ database }}
Collection{{ collection }}
{% if filtered %}Document{% else %}Document{% endif %} 214 | highlight = indexed 215 | 217 | {% if single %} 218 | {{ _id }} 219 | {% else %} 220 | {% if prev %}{% else %}prev{% endif %} | 221 | {{ page }} of {{ mongs.commaize(ndocs, places=0) }} 222 | | {% if next_ %} next{% else %}next{% endif %} 223 | {% endif %} 224 |
{{ pair.k }}{% if pair.link %}{{ pair.v }}{% else %}{{ pair.v }}{% endif %}
233 | {% endblock %} 234 | -------------------------------------------------------------------------------- /www/%server/%database/%collection/index.html.spt: -------------------------------------------------------------------------------- 1 | """/server/database/collection/ 2 | """ 3 | from pando import Response 4 | import mongs 5 | 6 | [----] 7 | server = request.path['server'] 8 | database = request.path['database'] 9 | collection = request.path['collection'] 10 | 11 | coll = mongs.connect(server)[database][collection] 12 | 13 | if mongs.has_documents(coll): 14 | # Redirect users to the first document in this collection. 15 | raise Response(302, headers={'Location': b'.//1/'}) 16 | 17 | [----] 18 | {% extends "base.html" %} 19 | {% block content %} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
Server{{ server }}
Database{{ database }}
Collection{{ collection }}
DocumentsNo documents found.
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /www/%server/%database/index.html.spt: -------------------------------------------------------------------------------- 1 | """/server/database/ 2 | """ 3 | import math 4 | 5 | import pymongo 6 | import mongs 7 | from mongs import commaize 8 | 9 | 10 | MB = 1024 ** 2.0 11 | 12 | 13 | def f2s(n, scale=1.0): 14 | """Given float, return str. 15 | """ 16 | if n == 0.0: 17 | out = "0  " 18 | else: 19 | out = commaize(n / scale) 20 | return out 21 | 22 | 23 | class Collection(object): 24 | """Model a MongoDB collection. 25 | """ 26 | 27 | is_index = False 28 | 29 | def __init__(self, db, collname): 30 | self.name = collname 31 | self.stats = stats = db.command({'collStats': collname}) 32 | self.storage_size = float(stats['storageSize']) 33 | self.data_size = float(stats['size']) 34 | self.indexes = [ 35 | Index(name=key, size=value) 36 | for key, value in stats.get('indexSizes', {}).items() 37 | ] 38 | 39 | @property 40 | def index_size(self): 41 | return sum(index.storage_size for index in self.indexes) 42 | 43 | def format_storage_size(self, dbsize): 44 | total = f2s(dbsize, MB).replace(" ", "N") 45 | 46 | absolute = f2s(self.storage_size, MB) 47 | absolute = absolute.replace(" ", "N") 48 | absolute = (" " * (len(total) - len(absolute))) + absolute 49 | absolute = absolute.replace("N", " ") 50 | absolute = "%s  " % absolute 51 | 52 | percent = f2s(self.storage_size / dbsize * 100) 53 | percent = ((6 - len(percent)) * " ") + percent 54 | 55 | return absolute + percent 56 | 57 | def format_data_size(self, dbsize): 58 | """Return a string indicating dataSize. 59 | """ 60 | out = f2s(self.data_size / dbsize * 100) 61 | out = out.replace(" ", "N") 62 | out = (" " * (4 - len(out))) + out 63 | out = out.replace("N", " ") 64 | return out 65 | 66 | 67 | class Index(Collection): 68 | is_index = True 69 | 70 | def __init__(self, name, size): 71 | self.name = name 72 | self.storage_size = size 73 | self.data_size = size 74 | 75 | 76 | # ========================================================================== 77 | # dodge weird 2.6 issue 78 | [----] 79 | 80 | 81 | # parse and hydrate 82 | # ================= 83 | 84 | server = request.path['server'] 85 | database = request.path['database'] 86 | db = mongs.connect(server)[database] 87 | 88 | 89 | # dbsize 90 | # ====== 91 | # We need the disk size of the database as a whole in order to calculate 92 | # percentages. However, the dbstats call blocks the whole db server while it 93 | # runs, and it takes a long time (we killed it after 15 minutes in the case 94 | # where this problem came to light. Oops!). See: 95 | # 96 | # http://www.mongodb.org/display/DOCS/Monitoring+and+Diagnostics#MonitoringandDiagnostics-mongoShellDiagnosticCommands 97 | # 98 | # Now instead we sum storageSize from collstats, which is a safe call (per 99 | # jaraco). Apparently there are three size metrics, however, and I'm not sure 100 | # that summing collstats.storageSize is guaranteed to equal dbstats.fileSize 101 | # (which is what I want). Here's me trying to figure out what to do: 102 | # 103 | # http://stackoverflow.com/questions/10339852/ 104 | # 105 | # The bottom line for now is that I'm showing storageSize and dataSize and 106 | # pretending not to care about fileSize (though of course that's the very thing 107 | # I care about!). 108 | # 109 | # Update: I've filed an issue with 10gen to see about safely exposing fileSize 110 | # per-db and per-collection in a future release. 111 | 112 | dbsize = 0.0 113 | 114 | 115 | # rows 116 | # ==== 117 | # We have to build this as a list rather than using a generator because we need 118 | # to fully compute dbsize before formatting any given row for display. We also 119 | # take pains to sort by storage_size, with indices grouped by collection. 120 | 121 | rows = [] 122 | collnames = db.collection_names() 123 | 124 | # first build a list of collections, sorted by storage_size ascending 125 | for collname in collnames: 126 | rows.append(Collection(db, collname)) 127 | dbsize += rows[-1].storage_size 128 | rows.sort(key=lambda row: row.storage_size) 129 | 130 | # now add in indices, reversing in the process (we want biggest first) 131 | _rows = [] 132 | while rows: 133 | collection = rows.pop() # this has the effect of reversing rows 134 | _rows.append(collection) 135 | _rows.extend(collection.indexes) 136 | dbsize += collection.index_size 137 | rows = _rows 138 | 139 | 140 | # ========================================================================== 141 | # dodge weird 2.6 issue 142 | [----] 143 | {% extends "base.html" %} 144 | {% block content %} 145 | 186 | 187 | 188 | 189 | 190 | {% if rows %} 191 | 192 | 193 | 194 | {% endif %} 195 | 196 | 197 | 198 | 199 | {% if rows %} 200 | 201 | 202 | 203 | {% endif %} 204 | 205 | {% if not rows %} 206 | 207 | 208 | 209 | 210 | {% endif %} 211 | {% for row in rows %} 212 | 213 | {% if loop.index0 == 0 %}{% endif %} 224 | 225 | 228 | 229 | 230 | 231 | 232 | {% endfor %} 233 |
Server{{ server }}storageSizedataSize
Database{{ database }}{{ f2s(dbsize, MB) }} MB  %  %
CollectionsNo collections found.
214 | 215 | Collections 216 | 217 | and indices
218 |
219 | All percentages use
220 | total storageSize
221 | as the base
222 |
223 |
{% if not row.is_index %}{% endif %}{{ row.name }}{% if not row.is_index %}{% endif %}{{ row.format_storage_size(dbsize) }}{{ row.format_data_size(dbsize) }}
234 | {% endblock %} 235 | -------------------------------------------------------------------------------- /www/%server/index.html.spt: -------------------------------------------------------------------------------- 1 | import mongs 2 | 3 | [----] 4 | server = request.path['server'] 5 | 6 | conn = mongs.connect(server) 7 | databases = sorted(conn.database_names(), key=lambda a: a.lower()) 8 | 9 | [----] 10 | {% extends "base.html" %} 11 | {% block content %} 12 | 13 | 14 | 15 | 16 | 21 | 22 |
Server{{ server }}
Databases 17 | {% for database in databases %} 18 | {{ database }}
19 | {% endfor %} 20 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chadwhitacre/mongs/945662a575d1f20a02d9fb8397788a45cdded556/www/favicon.ico -------------------------------------------------------------------------------- /www/index.html.spt: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | servers = sorted(os.environ.get('SERVERS', 'localhost').split()) 4 | 5 | [----] 6 | 7 | [----] 8 | 9 | {% extends "base.html" %} 10 | {% block content %} 11 | 12 | 13 | 14 | 19 | 20 |
Servers 15 | {% for name in servers %} 16 | {{ name }}
17 | {% endfor %} 18 |
21 | {% endblock %} 22 | --------------------------------------------------------------------------------