tokens.vue 28.4 KB
Newer Older
janis.streib's avatar
janis.streib committed
1
2
<template>
    <div class="main">
3
        <h1>Accounts & Tokens</h1>
4
5
        <b-modal id="modal-create-account" size="lg" title="Subaccount erstellen"
                 @hidden="resetAccountData">
6
7
8
9
10
            <b-form @submit="createAccount">
                <b-form-group label="Beschreibung:" label-for="input-account-create-description">
                    <b-form-textarea
                            id="input-account-create-description"
                            v-model.trim="new_account.description"
11
                            placeholder="Beschreibung eingeben"
12
                    />
13
                </b-form-group>
14
15
16
17
18
19
20
                <b-form-group label="Rolle:" label-for="input-account-create-role">
                    <b-form-select
                            id="input-account-create-role"
                            v-model="test_role"
                            :options="test_roles"
                    />
                </b-form-group>
21
            </b-form>
22
23
            <template v-slot:modal-footer="{cancel}">
                <b-button variant="outline-secondary" @click="cancel">
24
25
                    Abbrechen
                </b-button>
26
                <b-button type="submit" variant="success" @click="createAccount">
27
28
29
30
31
32
33
34
35
36
37
38
39
                    Erstellen
                </b-button>
            </template>
        </b-modal>
        <b-row>
            <b-col lg="9">
                <b-input-group>
                    <b-input-group-prepend>
                        <b-input-group-text>
                            <font-awesome-icon :icon="['fas', 'filter']"/>
                        </b-input-group-text>
                    </b-input-group-prepend>
                    <b-form-input id="filter-input" v-model.trim="filter_text" placeholder="Filter" autofocus
40
                                  debounce="300"/>
41
42
43
44
45
46
47
48
                </b-input-group>
            </b-col>
            <b-col lg="3" sm="*">
                <b-button block variant="outline-success" v-b-modal.modal-create-account>
                    Subaccount erstellen
                </b-button>
            </b-col>
        </b-row>
49
50
        <b-modal id="modal-edit-account" size="lg" title="Account bearbeiten"
                 @hidden="resetAccountData">
51
52
53
54
55
56
57
58
            <b-form>
                <b-form-group label="Beschreibung:" label-for="input-account-edit-description">
                    <b-form-textarea
                            id="input-account-edit-description"
                            v-model.trim="new_account.description"
                            placeholder="Beschreibung eingeben"
                    />
                </b-form-group>
59
60
61
62
63
64
65
                <b-form-group label="Rolle:" label-for="input-account-edit-role">
                    <b-form-select
                            id="input-account-edit-role"
                            v-model="test_role"
                            :options="test_roles"
                    />
                </b-form-group>
66
            </b-form>
67
68
69
70
71
72
            <template v-slot:modal-footer="{cancel}">
                <b-alert id="alert-edit-account" v-model="show_modal_alert"
                         variant="danger" dismissible fade class="mb-0 flex-grow">
                    {{modal_alert_content}}
                </b-alert>
                <b-button variant="outline-secondary" @click="cancel">
73
74
                    Abbrechen
                </b-button>
75
                <b-button id="button-delete-account" variant="danger">
76
77
                    Löschen
                </b-button>
78
79
                <b-popover ref="popoverAccountDelete" target="button-delete-account" triggers="click"
                           placement="bottom">
80
81
82
83
84
85
86
87
                    <template v-slot:title>Account wirklich löschen?</template>
                    <b-button variant="danger" @click="deleteAccount">
                        Löschen
                    </b-button>
                    <b-button variant="outline-secondary" @click="$refs.popoverAccountDelete.$emit('close')">
                        Abbrechen
                    </b-button>
                </b-popover>
88
                <b-button variant="primary" @click="editAccount">
89
90
91
92
                    Änderungen übernehmen
                </b-button>
            </template>
        </b-modal>
93
94
        <b-modal id="modal-create-token" size=lg title="Token erstellen"
                 @hidden="resetTokenData">
95
96
97
98
            <b-form>
                <b-form-group label="Beschreibung:" label-for="input-token-create-description">
                    <b-form-textarea
                            id="input-token-create-description"
99
                            v-model.trim="new_token.description"
100
                            required
101
102
                            placeholder="Beschreibung"
                    />
103
                </b-form-group>
Robert-K's avatar
Robert-K committed
104
105
                <div v-if="new_token.expiration_date != null">
                    <b-form-group label="Ablaufdatum:" label-for="input-token-create-expiration_date" class="mb-0">
106
107
108
109
110
111
112
113
114
                        <VueCtkDateTimePicker
                                :inline="!isMobile()"
                                format="DD-MM-YYYY HH:mm"
                                locale="de"
                                noButtonNow
                                minute-interval="5"
                                noLabel
                                no-keyboard
                                noClearButton
Robert-K's avatar
Robert-K committed
115
                                id="input-token-create-expiration_date"
116
117
                                color="#007BFF"
                                button-color="#007BFF"
Robert-K's avatar
Robert-K committed
118
                                v-model="new_token.expiration_date"/>
119
                    </b-form-group>
Robert-K's avatar
Robert-K committed
120
                    <b-button block variant="outline-secondary" @click="new_token.expiration_date = null">
121
122
123
124
                        Ablaufdatum entfernen
                    </b-button>
                </div>
                <b-button block variant="outline-secondary" v-else
Robert-K's avatar
Robert-K committed
125
                          @click="new_token.expiration_date = formatDate(getDate30DaysAhead())">
126
127
                    Ablaufdatum hinzufügen
                </b-button>
128
            </b-form>
129
130
131
132
133
134
            <template v-slot:modal-footer="{cancel}">
                <b-alert id="alert-create-token" v-model="show_modal_alert"
                         variant="danger" dismissible fade class="mb-0 flex-grow">
                    {{modal_alert_content}}
                </b-alert>
                <b-button variant="outline-secondary" @click="cancel">
135
136
                    Abbrechen
                </b-button>
137
                <b-button type="submit" variant="success" @click="createToken">
138
139
140
                    Erstellen
                </b-button>
            </template>
141
        </b-modal>
142
143
144
        <b-modal id="modal-token" size=lg title="Token erstellt">
            <b-input-group>
                <b-form-input id="input-token" v-model="token" readonly/>
145
146
                <b-tooltip target="input-token" :show.sync="token_copied" :disabled="!token_copied" placement="bottom"
                           variant="primary">
147
148
                    Kopiert.
                </b-tooltip>
149
                <b-input-group-append>
150
                    <b-button variant="primary" @click="copyToken">
151
152
153
154
                        <font-awesome-icon :icon="['fas', 'copy']"/>
                    </b-button>
                </b-input-group-append>
            </b-input-group>
155
156
157
            <b-alert show variant="warning" class="mb-0 mt-3">Bewahren Sie das Token gut auf. Hier werden Sie es nicht
                mehr einsehen können!
            </b-alert>
158
159
160
161
162
163
            <template v-slot:modal-footer="{ok}">
                <b-button variant="success" @click="ok">
                    Ok
                </b-button>
            </template>
        </b-modal>
164
        <b-modal id="modal-edit-token" size=lg title="Token bearbeiten">
165
166
167
168
            <b-form>
                <b-form-group label="Beschreibung:" label-for="input-token-edit-description">
                    <b-form-textarea
                            id="input-token-edit-description"
169
                            v-model.trim="new_token.description"
170
171
172
173
174
                            required
                            placeholder="Beschreibung"
                    />
                </b-form-group>
            </b-form>
Robert-K's avatar
Robert-K committed
175
176
            <div v-if="new_token.expiration_date != null">
                <b-form-group label="Ablaufdatum:" label-for="input-token-edit-expiration_date" class="mb-0">
177
178
179
180
181
182
183
184
185
                    <VueCtkDateTimePicker
                            :inline="!isMobile()"
                            format="DD-MM-YYYY HH:mm"
                            locale="de"
                            noButtonNow
                            minute-interval="5"
                            noLabel
                            no-keyboard
                            noClearButton
Robert-K's avatar
Robert-K committed
186
                            id="input-token-edit-expiration_date"
187
188
                            color="#007BFF"
                            button-color="#007BFF"
Robert-K's avatar
Robert-K committed
189
                            v-model="new_token.expiration_date"/>
190
                </b-form-group>
Robert-K's avatar
Robert-K committed
191
                <b-button block variant="outline-secondary" @click="new_token.expiration_date = null">
192
193
194
195
                    Ablaufdatum entfernen
                </b-button>
            </div>
            <b-button block variant="outline-secondary" v-else
Robert-K's avatar
Robert-K committed
196
                      @click="new_token.expiration_date = formatDate(getDate30DaysAhead())">
197
198
                Ablaufdatum hinzufügen
            </b-button>
199
            <template v-slot:modal-footer="{cancel, ok}">
200
201
202
203
204
                <b-alert id="alert-edit-token" v-model="show_modal_alert"
                         variant="danger" dismissible fade class="mb-0 flex-grow">
                    {{modal_alert_content}}
                </b-alert>
                <b-button variant="outline-secondary" @click="cancel">
205
206
                    Abbrechen
                </b-button>
207
                <b-button id="button-delete-token" variant="danger">
208
209
                    Löschen
                </b-button>
210
                <b-popover ref="popoverTokenDelete" target="button-delete-token" triggers="click" placement="bottom">
211
212
213
214
215
216
217
218
                    <template v-slot:title>Token wirklich löschen?</template>
                    <b-button variant="danger" @click="deleteToken">
                        Löschen
                    </b-button>
                    <b-button variant="outline-secondary" @click="$refs.popoverTokenDelete.$emit('close')">
                        Abbrechen
                    </b-button>
                </b-popover>
219
                <b-button type="submit" variant="primary" @click="editToken">
220
221
222
223
                    Änderungen übernehmen
                </b-button>
            </template>
        </b-modal>
224
225
        <template v-for="account in filtered_accounts">
            <b-card no-body :key="'card-account-' + account.login_name" class="mb-4">
226
227
228
229
230
231
                <template v-slot:header>
                    <b-row>
                        <b-col lg="3">
                            <h4>
                                {{account.login_name}}
                                <b-badge variant="success">
232
233
234
235
                                    <template v-if="account.login_name in tokens_by_account">
                                        <template v-if="tokens_by_account[account.login_name].length === 1">
                                            {{tokens_by_account[account.login_name].length}} Token
                                        </template>
236
237
                                        <template v-else>{{tokens_by_account[account.login_name].length}} Tokens
                                        </template>
238
239
                                    </template>
                                    <template v-else>0 Tokens</template>
240
241
242
243
244
                                </b-badge>
                            </h4>
                            <p class="text-muted">Login Name</p>
                        </b-col>
                        <b-col>
245
246
247
                            <div v-if="account.parent_login_name == null">Hauptaccount; enthält nur Session-Tokens; kann
                                nicht gelöscht werden.
                            </div>
248
249
                            <div v-else-if="account.description == null">Keine Beschreibung vorhanden.</div>
                            <div v-else>{{account.description}}</div>
250
251
252
                            <p class="text-muted">Beschreibung</p>
                        </b-col>
                        <b-col>
253
                            <h5 class="mb-0">
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
                                <template
                                        v-for="role in roles_by_account[account.login_name].slice(0, max_role_badge_count)">
                                    <b-badge
                                            :key="'role-badge-' + role.mgr_login_name + '-' + role.role_fq_name"
                                            :id="account.login_name + '-role-badge-' + role.mgr_login_name + '-' + role.role_fq_name"
                                            :style="{background: role.role_fq_name.toHSL({lit: [30, 40]})}"
                                            class="mr-1 mb-1">{{role.system.toUpperCase()}}<br>{{role.role}}
                                    </b-badge>
                                    <b-popover
                                            :key="'role-badge-tooltip' + role.mgr_login_name + '-' + role.role_fq_name"
                                            :target="account.login_name + '-role-badge-' + role.mgr_login_name + '-' + role.role_fq_name"
                                            triggers="hover"
                                            placement="bottom"
                                            title="Berechtigungen:">
                                        <div v-for="(permission, name) in role.contained_permissions" :key="name">
                                            <b>{{name}}:</b> {{permission}}
                                        </div>
                                    </b-popover>
                                </template>
273
274
275
                            </h5>
                            <h6 v-if="roles_by_account[account.login_name].length > max_role_badge_count">
                                + {{roles_by_account[account.login_name].length - max_role_badge_count}} Weitere
276
                            </h6>
277
                            <p class="text-muted">Rollen</p>
278
279
                        </b-col>
                        <b-col lg="1">
280
281
282
283
284
285
286
287
288
289
290
                            <template v-if="account.parent_login_name !== null">
                                <b-button block variant="outline-primary"
                                          :id="'button-edit-account-' + account.login_name"
                                          @click="showModalEditAccount(account)">
                                    <font-awesome-icon :icon="['far', 'edit']"/>
                                </b-button>
                                <b-tooltip placement="bottom" :target="'button-edit-account-' + account.login_name"
                                           triggers="hover" variant="primary">
                                    Account bearbeiten
                                </b-tooltip>
                            </template>
291
292
293
294
                        </b-col>
                    </b-row>
                </template>
                <b-button block squared variant="info" v-b-toggle="account.login_name + '-collapse'">
295
                    <font-awesome-icon class="collapse-icon" :icon="['fas','chevron-up']"/>
296
297
                </b-button>
                <b-collapse :id="account.login_name + '-collapse'">
298
299
300
301
302
303
304
                    <b-table :items="tokens_by_account[account.login_name]"
                             :fields="token_list_fields"
                             class="m-0"
                             striped
                             responsive
                             :tbody-transition-props="leaving_transition_properties"
                             primary-key="description"> <!-- TODO: Get table animations to work -->
305
                        <template v-slot:head(buttons)>
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
                            <template v-if="account.parent_login_name !== null">
                                <b-button block variant="outline-success"
                                          :id="'button-create-token-' +  account.login_name"
                                          @click="showModalCreateToken(account.login_name)">
                                    <font-awesome-icon :icon="['fas', 'plus']"/>
                                </b-button>
                                <b-tooltip :target="'button-create-token-' +  account.login_name" triggers="hover"
                                           variant="success" placement="left">
                                    Token erstellen
                                </b-tooltip>
                            </template>
                        </template>
                        <template v-slot:cell(expiration_date)="data">
                            <!-- TODO: Expired tokens should be red; might be ugly / difficult -->
                            {{formatDate(data.item.expiration_date)}}
                            <template v-if="data.item.is_expired">
                                (Expired)
                            </template>
324
325
326
327
                        </template>
                        <template v-slot:cell(buttons)="data">
                            <b-button block variant="outline-primary"
                                      :id="'button-edit-token-' + account.login_name+ '-' + data.index"
328
                                      @click="showModalEditToken(data.item)">
329
330
331
                                <font-awesome-icon :icon="['far', 'edit']"/>
                            </b-button>
                            <b-tooltip :target="'button-edit-token-' + account.login_name+ '-' + data.index"
332
                                       triggers="hover" variant="primary" placement="left">
333
334
335
336
337
338
339
                                Token bearbeiten
                            </b-tooltip>
                        </template>
                    </b-table>
                </b-collapse>
            </b-card>
        </template>
janis.streib's avatar
janis.streib committed
340
341
342
343
    </div>
</template>

<script>
344
345
    import AccountTokenService from '@/api-services/account_token.service';
    import AccountService from '@/api-services.gen/cntl.mgr'
346
    import TokenService from '@/api-services.gen/cntl.wapi_auth'
347
    import ApiUtil from '@/util/apiutil'
348
    import '@/util/colorutil'
janis.streib's avatar
janis.streib committed
349
350

    export default {
351
        name: 'tokens',
janis.streib's avatar
janis.streib committed
352
353
        data() {
            return {
354
355
356
357
358
359
360
361
362
                // TODO: remove test/wip data aka implement role management
                test_role: 'NetVS Overlord',
                test_roles: [
                    'NetVS Overlord',
                    'Dungeon Master',
                    'HiWi-Goblin',
                    'Guest',
                    'Individuell'
                ],
363
                tokens_by_account: null,
364
                roles_by_account: null,
365
366
                accounts: null,
                new_account: {
367
                    parent_login_name: null,
368
369
                    description: '',
                    login_name: '',
Robert-K's avatar
Robert-K committed
370
                    expiration_date: null
371
372
                },
                new_token: {
373
374
                    description: '',
                    login_name: '',
Robert-K's avatar
Robert-K committed
375
                    expiration_date: null
376
                },
377
378
379
380
381
382
383
384
                filter_text: "",
                token_list_fields: [
                    {
                        key: 'description',
                        label: "Beschreibung",
                        sortable: false
                    },
                    {
Robert-K's avatar
Robert-K committed
385
                        key: 'expiration_date',
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
                        label: "Ablaufdatum",
                        sortable: true
                    },
                    {
                        key: 'last_login_date',
                        label: "Zuletzt verwendet",
                        formatter: this.formatDate,
                        sortable: true
                    },
                    {
                        key: 'last_generate_date',
                        label: "Zuletzt generiert",
                        formatter: this.formatDate,
                        sortable: true
                    },
                    {
                        key: 'buttons',
                        label: '',
                        sortable: false
                    },
406
407
408
                ],
                show_modal_alert: false,
                modal_alert_content: "",
409
                token: "",
410
411
412
                token_copied: false,
                leaving_transition_properties: { // TODO: Get table animations to work
                    name: 'flip-list'
413
414
                },
                max_role_badge_count: 10
janis.streib's avatar
janis.streib committed
415
416
            }
        },
417
        computed: {
418
419
420
            filtered_accounts() {
                if (this.accounts == null) {
                    return null
421
                }
422
423
424
425
426
                if (this.filter_text === '') {
                    return this.accounts
                }
                return this.accounts.filter(account => {
                    return account.login_name.toLowerCase().includes(this.filter_text.toLowerCase())
427
                        || (account.description != null && account.description.toLowerCase().includes(this.filter_text.toLowerCase()))
428
429
                })
            }
430
        },
janis.streib's avatar
janis.streib committed
431
        created() {
432
            this.fetchData()
433
434
        },
        methods: {
435
            fetchData() {
436
                AccountTokenService.list(this.$store.state.netdb_axios_config, this.$store.state.user.login_name).then((response) => {
437
                    this.tokens_by_account = ApiUtil.dict_of_lists_by_value_of_array(this.formatExpiredTokens(response.data[2].concat(response.data[3])), 'login_name')
438
                    this.roles_by_account = ApiUtil.dict_of_lists_by_value_of_array(response.data[4].concat(response.data[5]), 'mgr_login_name')
439
                    this.accounts = response.data[0].concat(response.data[1])
440
441
442
                })
            },
            showModalEditAccount(account) {
443
444
                this.new_account.description = account.description
                this.new_account.login_name = account.login_name
445
446
447
                this.new_account.parent_login_name = account.parent_login_name
                this.show_modal_alert = false
                this.$bvModal.show('modal-edit-account')
448
            },
449
            showModalEditToken(token) {
450
451
                this.new_token.description = token.description
                this.new_token.login_name = token.login_name
Robert-K's avatar
Robert-K committed
452
                this.new_token.expiration_date = token.expiration_date != null ? this.formatDate(token.expiration_date) : null
453
454
455
456
457
458
                this.new_token.pk = token.pk
                this.show_modal_alert = false
                this.$bvModal.show('modal-edit-token')
            },
            showModalCreateToken(login_name) {
                this.new_token.description = ''
Robert-K's avatar
Robert-K committed
459
                this.new_token.expiration_date = null
460
461
462
                this.new_token.login_name = login_name
                this.show_modal_alert = false
                this.$bvModal.show('modal-create-token')
463
            },
464
            resetAccountData() {
465
466
                this.new_account.description = ''
                this.new_account.login_name = ''
Robert-K's avatar
Robert-K committed
467
                this.new_account.expiration_date = null
468
            },
469
            resetTokenData() {
470
471
                this.new_token.description = ''
                this.new_token.login_name = ''
Robert-K's avatar
Robert-K committed
472
                this.new_token.expiration_date = null
473
            },
474
475
            createAccount() {
                AccountService.create(this.$store.state.netdb_axios_config, {
476
477
478
479
480
481
482
483
                    do_copy_assignments_new: true,
                    description_new: this.new_account.description,
                    allow_data_manipulation_new: true,
                    login_name_new: null,
                    do_copy_roles_new: true
                }).then(() => {
                    this.$bvModal.hide('modal-create-account')
                    this.fetchData()
gj4210's avatar
gj4210 committed
484
485
                }).catch(error => {
                    this.modal_alert_content = error.response.data.error.type.text_descr
486
487
488
489
490
491
492
493
494
495
496
497
498
                    this.show_modal_alert = true
                })
            },
            editAccount() {
                AccountService.update(this.$store.state.netdb_axios_config, {
                    description_new: this.new_account.description,
                    login_name_old: this.new_account.login_name,
                    login_name_new: this.new_account.login_name,
                    allow_data_manipulation_new: true
                }).then(() => {
                    this.$bvModal.hide('modal-edit-account')
                    this.fetchData()
                }).catch(error => {
gj4210's avatar
gj4210 committed
499
                    this.modal_alert_content = error.response.data.error.type.text_descr
500
501
502
503
504
505
                    this.show_modal_alert = true
                })
            },
            deleteAccount() {
                AccountService.delete(this.$store.state.netdb_axios_config, {
                    do_delete_references: true,
506
                    login_name_old: this.new_account.login_name
507
508
509
                }).then(() => {
                    this.$bvModal.hide('modal-edit-account')
                }).catch(error => {
gj4210's avatar
gj4210 committed
510
                    this.modal_alert_content = error.response.data.error.type.text_descr
511
512
513
514
515
516
517
                    this.show_modal_alert = true
                })
            },
            createToken() {
                TokenService.create(this.$store.state.netdb_axios_config, {
                    description_new: this.new_token.description,
                    login_name_new: this.new_token.login_name,
Robert-K's avatar
Robert-K committed
518
                    expiration_date_new: this.new_token.expiration_date
519
520
521
522
523
524
                }).then(response => {
                    this.$bvModal.hide('modal-create-token')
                    this.token = response.data[0][0].token
                    this.$bvModal.show('modal-token')
                    this.fetchData()
                }).catch(error => {
gj4210's avatar
gj4210 committed
525
                    this.modal_alert_content = error.response.data.error.type.text_descr
526
527
528
529
530
531
532
533
                    this.show_modal_alert = true
                })
            },
            editToken() {
                TokenService.update(this.$store.state.netdb_axios_config, {
                    description_new: this.new_token.description,
                    pk_old: this.new_token.pk,
                    do_refresh_token_new: true,
Robert-K's avatar
Robert-K committed
534
                    expiration_date_new: this.new_token.expiration_date
535
536
537
538
                }).then(() => {
                    this.$bvModal.hide('modal-edit-token')
                    this.fetchData()
                }).catch(error => {
gj4210's avatar
gj4210 committed
539
                    this.modal_alert_content = error.response.data.error.type.text_descr
540
541
542
543
544
545
546
547
548
549
                    this.show_modal_alert = true
                })
            },
            deleteToken() {
                TokenService.delete(this.$store.state.netdb_axios_config, {
                    pk_old: this.new_token.pk
                }).then(() => {
                    this.$bvModal.hide('modal-edit-token')
                    this.fetchData()
                }).catch(error => {
gj4210's avatar
gj4210 committed
550
                    this.modal_alert_content = error.response.data.error.type.text_descr
551
                    this.show_modal_alert = true
552
553
554
                })
            },
            formatDate(value) {
555
556
557
558
                if (value == null) {
                    return 'N/A'
                }
                return new Date(Date.parse(value)).toLocaleString('de-DE')
559
            },
560
            getDate30DaysAhead() {
561
                let d = new Date()
562
                d.setDate(d.getDate() + 29)
563
                return d
564
565
566
            },
            copyToken() {
                // https://www.w3schools.com/howto/howto_js_copy_clipboard.asp
567
568
569
570
571
                let text = document.getElementById("input-token")
                text.select()
                text.setSelectionRange(0, 99999) /*For mobile devices*/
                document.execCommand("copy")
                this.token_copied = true
572
573
574
575
576
577
578
579
580
            },
            formatExpiredTokens(tokens) {
                tokens.forEach(token => {
                        if (token.is_expired) {
                            token._rowVariant = 'danger'
                        }
                    }
                )
                return tokens
581
            }
janis.streib's avatar
janis.streib committed
582
583
584
585
586
        }
    }
</script>

<style scoped>
587
588
589
590
591
592
593
    .collapse-icon {
        transition: .2s transform ease-in-out;
    }

    .collapsed .collapse-icon {
        transform: rotate(-180deg);

594
    }
595
596
597
598

    table .flip-list-move { /* TODO: Get table animations to work */
        transition: transform 1s;
    }
janis.streib's avatar
janis.streib committed
599
</style>