├── .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 |
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 | Server |
201 | {{ server }} |
202 |
203 |
204 | Database |
205 | {{ database }} |
206 |
207 |
208 | Collection |
209 | {{ collection }} |
210 |
211 |
212 | {% if filtered %}Document{% else %}Document{% endif %}
214 | highlight = indexed
215 | |
216 |
217 | {% if single %}
218 | {{ _id }}
219 | {% else %}
220 | {% if prev %}prev{% else %}prev{% endif %} |
221 | {{ page }} of {{ mongs.commaize(ndocs, places=0) }}
222 | | {% if next_ %} next{% else %}next{% endif %}
223 | {% endif %}
224 | |
225 |
226 | {% for pair in pairs %}
227 |
228 | {{ pair.k }} |
229 | {% if pair.link %}{{ pair.v }}{% else %}{{ pair.v }}{% endif %} |
230 |
231 | {% endfor %}
232 |
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 | Server |
23 | {{ server }} |
24 |
25 |
26 | Database |
28 | {{ database }} |
29 |
30 |
31 | Collection |
33 | {{ collection }} |
34 |
35 |
36 | Documents |
37 | No documents found. |
38 |
39 |
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 | Server |
189 | {{ server }} |
190 | {% if rows %}
191 | storageSize |
192 | dataSize |
193 | |
194 | {% endif %}
195 |
196 |
197 | Database |
198 | {{ database }} |
199 | {% if rows %}
200 | {{ f2s(dbsize, MB) }} MB % |
201 | % |
202 | |
203 | {% endif %}
204 |
205 | {% if not rows %}
206 |
207 | Collections |
208 | No collections found. |
209 |
210 | {% endif %}
211 | {% for row in rows %}
212 |
213 | {% if loop.index0 == 0 %}
214 |
215 | Collections
216 |
217 | and indices
218 |
219 | All percentages use
220 | total storageSize
221 | as the base
222 |
223 | | {% endif %}
224 |
225 | {% if not row.is_index %}{% endif %}{{ row.name }}{% if not row.is_index %}
227 | a>{% endif %} |
228 | {{ row.format_storage_size(dbsize) }} |
229 | {{ row.format_data_size(dbsize) }} |
230 | |
231 |
232 | {% endfor %}
233 |
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 | Server | {{ server }} |
14 |
15 | Databases |
16 |
17 | {% for database in databases %}
18 | {{ database }}
19 | {% endfor %}
20 | |
21 |
22 |
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 | Servers |
14 |
15 | {% for name in servers %}
16 | {{ name }}
17 | {% endfor %}
18 | |
19 |
20 |
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------