Compare commits
15 Commits
test
...
packages@6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
987ef9aab5 | ||
|
|
d799e8ba36 | ||
|
|
da8dfdb13f | ||
|
|
c052d89af4 | ||
|
|
f4e5d38c99 | ||
|
|
895aba437b | ||
|
|
ffa9429c68 | ||
|
|
6649c5d75b | ||
|
|
d6e8129588 | ||
|
|
6a3a2f5f94 | ||
|
|
5d3a60d46e | ||
|
|
5f0a7f43c3 | ||
|
|
ebff883016 | ||
|
|
81ff856568 | ||
|
|
648aa62264 |
@@ -60,9 +60,9 @@ aliases = ["/v1.1", "/guides/reference/admin", "/v3.1"]
|
||||
<h4>Provisioning</h4>
|
||||
<p>A guide to help you automate your Grafana setup & configuration.</p>
|
||||
</a>
|
||||
<a href="{{< relref "guides/whats-new-in-v6-2.md" >}}" class="nav-cards__item nav-cards__item--guide">
|
||||
<h4>What's new in v6.2</h4>
|
||||
<p>Article on all the new cool features and enhancements in v6.2</p>
|
||||
<a href="{{< relref "guides/whats-new-in-v6-3.md" >}}" class="nav-cards__item nav-cards__item--guide">
|
||||
<h4>What's new in v6.3</h4>
|
||||
<p>Article on all the new cool features and enhancements in v6.3</p>
|
||||
</a>
|
||||
<a href="{{< relref "tutorials/screencasts.md" >}}" class="nav-cards__item nav-cards__item--guide">
|
||||
<h4>Screencasts</h4>
|
||||
|
||||
@@ -99,3 +99,18 @@ allow_sign_up = true
|
||||
allowed_organizations = github google
|
||||
```
|
||||
|
||||
### Team Sync (Enterprise only)
|
||||
|
||||
> Only available in Grafana Enterprise v6.3+
|
||||
|
||||
With Team Sync you can map your GitHub org teams to teams in Grafana so that your users will automatically be added to
|
||||
the correct teams.
|
||||
|
||||
Your GitHub teams can be referenced in two ways:
|
||||
|
||||
- `https://github.com/orgs/<org>/teams/<team name>`
|
||||
- `@<org>/<team name>`
|
||||
|
||||
Example: `@grafana/developers`
|
||||
|
||||
[Learn more about Team Sync]({{< relref "auth/enhanced_ldap.md" >}})
|
||||
|
||||
144
docs/sources/guides/whats-new-in-v6-3.md
Normal file
144
docs/sources/guides/whats-new-in-v6-3.md
Normal file
@@ -0,0 +1,144 @@
|
||||
+++
|
||||
title = "What's New in Grafana v6.3"
|
||||
description = "Feature & improvement highlights for Grafana v6.3"
|
||||
keywords = ["grafana", "new", "documentation", "6.3"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Version 6.3"
|
||||
identifier = "v6.3"
|
||||
parent = "whatsnew"
|
||||
weight = -14
|
||||
+++
|
||||
|
||||
# What's New in Grafana v6.3
|
||||
|
||||
For all details please read the full [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md)
|
||||
|
||||
## Highlights
|
||||
|
||||
- New Explore features
|
||||
- [Loki Live Streaming]({{< relref "#loki-live-streaming" >}})
|
||||
- [Loki Context Queries]({{< relref "#loki-context-queries" >}})
|
||||
- [Elasticsearch Logs Support]({{< relref "#elasticsearch-logs-support" >}})
|
||||
- [InfluxDB Logs Support]({{< relref "#influxdb-logs-support" >}})
|
||||
- [Data links]({{< relref "#data-links" >}})
|
||||
- [New Time Picker]({{< relref "#new-time-picker" >}})
|
||||
- [Graph Area Gradients]({{< relref "#graph-gradients" >}}) - A new graph display option!
|
||||
- Grafana Enterprise
|
||||
- [LDAP Active Sync]({{< relref "#ldap-active-sync" >}}) - LDAP Active Sync
|
||||
- [SAML Authentication]({{< relref "#saml-authentication" >}}) - SAML Authentication
|
||||
|
||||
## Explore improvements
|
||||
|
||||
This release adds a ton of enhancements to Explore. Both in terms of new general enhancements but also in
|
||||
new data source specific features.
|
||||
|
||||
### Loki live streaming
|
||||
|
||||
For log queries using the Loki data source you can now stream logs live directly to the Explore UI.
|
||||
|
||||
### Loki context queries
|
||||
|
||||
After finding a log line through the heavy use of query filters it can then be useful to
|
||||
see the log lines surrounding the line your searched for. The `show context` feature
|
||||
allows you to view lines before and after the line of interest.
|
||||
|
||||
### Elasticsearch logs support
|
||||
|
||||
This release adds support for searching & visualizing logs stored in Elasticsearch in the Explore mode. With a special
|
||||
simplified query interface specifically designed for logs search.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v63/elasticsearch_explore_logs.png" max-width="600px" caption="New Time Picker" >}}
|
||||
|
||||
Please read [Using Elasticsearch in Grafana](/features/datasources/elasticsearch/#querying-logs-beta) for more detailed information on how to get started and use it.
|
||||
|
||||
### InfluxDB logs support
|
||||
|
||||
This release adds support for searching & visualizing logs stored in InfluxDB in the Explore mode. With a special
|
||||
simplified query interface specifically designed for logs search.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v63/influxdb_explore_logs.png" max-width="600px" caption="New Time Picker" >}}
|
||||
|
||||
Please read [Using InfluxDB in Grafana](/features/datasources/influxdb/#querying-logs-beta) for more detailed information on how to get started and use it.
|
||||
|
||||
## Data Links
|
||||
|
||||
We have simplified the UI for defining panel drilldown links (and renamed them to Panel links). We have also added a
|
||||
new type of link named `Data link`. The reason to have two different types is to make it clear how they are used
|
||||
and what variables you can use in the link. Panel links are only shown in the top left corner of
|
||||
the panel and you cannot reference series name or any data field.
|
||||
|
||||
While `Data links` are used by the actual visualization and can reference data fields.
|
||||
|
||||
Example:
|
||||
```url
|
||||
http://my-grafana.com/d/bPCI6VSZz/other-dashboard?var-server=${__series_name}
|
||||
```
|
||||
|
||||
You have access to these variables:
|
||||
|
||||
Name | Description
|
||||
------------ | -------------
|
||||
*${__series_name}* | The name of the time series (or table)
|
||||
*${__value_time}* | The time of the point your clicking on (in millisecond epoch)
|
||||
*${__url_time_range}* | Interpolates as the full time range (i.e. from=21312323412&to=21312312312)
|
||||
*${__all_variables}* | Adds all current variables (and current values) to the url
|
||||
|
||||
You can then click on point in the Graph.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v63/graph_datalink.png" max-width="400px" caption="New Time Picker" >}}
|
||||
|
||||
For now only the Graph panel supports `Data links` but we hope to add these to many visualizations.
|
||||
|
||||
## New Time Picker
|
||||
|
||||
The time picker has been re-designed and with a more basic design that makes accessing quick ranges more easy.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v63/time_picker.png" max-width="400px" caption="New Time Picker" >}}
|
||||
|
||||
## Graph Gradients
|
||||
|
||||
Want more eye candy in your graphs? Then the fill gradient option might be for you! Works really well for
|
||||
graphs with only a single series.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v63/graph_gradient_area.jpeg" max-width="800px" caption="Graph Gradient Area" >}}
|
||||
|
||||
Looks really nice in light theme as well.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v63/graph_gradients_white.png" max-width="800px" caption="Graph Gradient Area" >}}
|
||||
|
||||
## Grafana Enterprise
|
||||
|
||||
Substantial refactoring and improvements to the external auth systems has gone in to this release making the features
|
||||
listed below possible as well as laying a foundation for future enhancements.
|
||||
|
||||
### LDAP Active Sync
|
||||
|
||||
This is a new Enterprise feature that enables background syncing of user information, org role and teams memberships.
|
||||
This syncing is otherwise only done at login time. With this feature you can schedule how often this user synchronization should
|
||||
occur.
|
||||
|
||||
For example, lets say a user is removed from an LDAP group. In previous versions of Grafana an admin would have to
|
||||
wait for the user to logout or the session to expire for the Grafana permissions to update, a process that can take days.
|
||||
|
||||
With active sync the user would be automatically removed from the corresponding team in Grafana or even logged out and disabled if no longer
|
||||
belonging to an LDAP group that gives them access to Grafana.
|
||||
|
||||
[Read more](/auth/enhanced_ldap/#active-ldap-synchronization)
|
||||
|
||||
### SAML Authentication
|
||||
|
||||
Built-in support for SAML is now available in Grafana Enterprise.
|
||||
|
||||
### Team Sync for GitHub OAuth
|
||||
|
||||
When setting up OAuth with GitHub it's now possible to sync GitHub teams with Teams in Grafana.
|
||||
|
||||
[See docs]({{< relref "auth/github.md" >}})
|
||||
|
||||
### Team Sync for Auth Proxy
|
||||
|
||||
We've added support for enriching the Auth Proxy headers with Teams information, which makes it possible
|
||||
to use Team Sync with Auth Proxy.
|
||||
|
||||
[See docs](/auth/auth-proxy/#auth-proxy-authentication)
|
||||
@@ -2,5 +2,5 @@
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"packages": ["packages/*"],
|
||||
"version": "6.3.0-alpha.40"
|
||||
"version": "6.3.0-beta.1"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@grafana/data",
|
||||
"version": "6.3.0-alpha.39",
|
||||
"version": "6.3.0-beta.1",
|
||||
"description": "Grafana Data Library",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "../../public/app/types/jquery/*.ts"],
|
||||
"exclude": ["dist", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"rootDirs": ["."],
|
||||
"module": "esnext",
|
||||
"outDir": "compiled",
|
||||
"declaration": true,
|
||||
"declarationDir": "dist",
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"typeRoots": ["./node_modules/@types", "types"],
|
||||
"skipLibCheck": true, // Temp workaround for Duplicate identifier tsc errors,
|
||||
"removeComments": false
|
||||
"declarationDir": "dist",
|
||||
"outDir": "compiled"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@grafana/runtime",
|
||||
"version": "6.3.0-alpha.39",
|
||||
"version": "6.3.0-beta.1",
|
||||
"description": "Grafana Runtime Library",
|
||||
"keywords": [
|
||||
"grafana"
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "../../public/app/types/jquery/*.ts"],
|
||||
"exclude": ["dist", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"rootDirs": ["."],
|
||||
"module": "esnext",
|
||||
"outDir": "compiled",
|
||||
"declaration": true,
|
||||
"declarationDir": "dist",
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"typeRoots": ["./node_modules/@types", "types"],
|
||||
"skipLibCheck": true, // Temp workaround for Duplicate identifier tsc errors,
|
||||
"removeComments": false
|
||||
"declarationDir": "dist",
|
||||
"outDir": "compiled"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ Adidtionaly, you can also provide additional Jest config via package.json file.
|
||||
|
||||
|
||||
## Working with CSS & static assets
|
||||
We support pure css, SASS and CSS in JS approach (via Emotion). All static assets referenced in your code (i.e. images) should be placed under `src/static` directory and referenced using relative paths.
|
||||
We support pure css, SASS and CSS in JS approach (via Emotion).
|
||||
|
||||
1. Single css/sass file
|
||||
Create your css/sass file and import it in your plugin entry point (typically module.ts):
|
||||
@@ -101,6 +101,8 @@ If you want to provide different stylesheets for dark/light theme, create `dark.
|
||||
|
||||
TODO: add note about loadPluginCss
|
||||
|
||||
Note that static files (png, svg, json, html) are all copied to dist directory when the plugin is bundled. Relative paths to those files does not change.
|
||||
|
||||
3. Emotion
|
||||
Starting from Grafana 6.2 our suggested way of styling plugins is by using [Emotion](https://emotion.sh). It's a css-in-js library that we use internaly at Grafana. The biggest advantage of using Emotion is that you will get access to Grafana Theme variables.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@grafana/toolkit",
|
||||
"version": "6.3.0-alpha.40",
|
||||
"version": "6.3.0-beta.1",
|
||||
"description": "Grafana Toolkit",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import axios from 'axios';
|
||||
// @ts-ignore
|
||||
import * as _ from 'lodash';
|
||||
import { Task, TaskRunner } from './task';
|
||||
|
||||
@@ -3,7 +3,6 @@ import execa = require('execa');
|
||||
import * as fs from 'fs';
|
||||
// @ts-ignore
|
||||
import * as path from 'path';
|
||||
import { changeCwdToGrafanaUi, restoreCwd, changeCwdToPackage } from '../utils/cwd';
|
||||
import chalk from 'chalk';
|
||||
import { useSpinner } from '../utils/useSpinner';
|
||||
import { Task, TaskRunner } from './task';
|
||||
|
||||
@@ -4,7 +4,6 @@ import execa = require('execa');
|
||||
import path = require('path');
|
||||
import fs = require('fs');
|
||||
import glob = require('glob');
|
||||
import util = require('util');
|
||||
import { Linter, Configuration, RuleFailure } from 'tslint';
|
||||
import * as prettier from 'prettier';
|
||||
|
||||
@@ -17,7 +16,6 @@ interface PluginBuildOptions {
|
||||
|
||||
export const bundlePlugin = useSpinner<PluginBundleOptions>('Compiling...', async options => await bundleFn(options));
|
||||
|
||||
const readFileAsync = util.promisify(fs.readFile);
|
||||
// @ts-ignore
|
||||
export const clean = useSpinner<void>('Cleaning', async () => await execa('rimraf', [`${process.cwd()}/dist`]));
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import path = require('path');
|
||||
import fs = require('fs');
|
||||
import webpack = require('webpack');
|
||||
import { getWebpackConfig } from '../../../config/webpack.plugin.config';
|
||||
import formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import path = require('path');
|
||||
import * as jestCLI from 'jest-cli';
|
||||
import { useSpinner } from '../../utils/useSpinner';
|
||||
import { jestConfig } from '../../../config/jest.plugin.config';
|
||||
|
||||
@@ -46,7 +46,6 @@ export async function getTeam(team: any): Promise<any> {
|
||||
}
|
||||
|
||||
export async function addToTeam(team: any, user: any): Promise<any> {
|
||||
const members = await client.get(`/teams/${team.id}/members`);
|
||||
console.log(`Adding user ${user.name} to team ${team.name}`);
|
||||
await client.post(`/teams/${team.id}/members`, { userId: user.id });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import execa = require('execa');
|
||||
import * as fs from 'fs';
|
||||
import { changeCwdToGrafanaUi, restoreCwd, changeCwdToGrafanaToolkit } from '../utils/cwd';
|
||||
import chalk from 'chalk';
|
||||
import { useSpinner } from '../utils/useSpinner';
|
||||
import { Task, TaskRunner } from './task';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import path = require('path');
|
||||
|
||||
// See: packages/grafana-ui/src/types/plugin.ts
|
||||
interface PluginJSONSchema {
|
||||
id: string;
|
||||
|
||||
@@ -7,7 +7,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
|
||||
import * as webpack from 'webpack';
|
||||
import { hasThemeStylesheets, getStyleLoaders, getStylesheetEntries, getFileLoaders } from './webpack/loaders';
|
||||
import { getStyleLoaders, getStylesheetEntries, getFileLoaders } from './webpack/loaders';
|
||||
|
||||
interface WebpackConfigurationOptions {
|
||||
watch?: boolean;
|
||||
@@ -51,6 +51,7 @@ const getManualChunk = (id: string) => {
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getEntries = () => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { getStylesheetEntries, hasThemeStylesheets } from './loaders';
|
||||
describe('Loaders', () => {
|
||||
describe('stylesheet helpers', () => {
|
||||
const logSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
afterAll(() => {
|
||||
logSpy.mockRestore();
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { getPluginJson } from '../utils/pluginValidation';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
@@ -122,8 +119,8 @@ export const getFileLoaders = () => {
|
||||
? {
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
outputPath: 'static',
|
||||
name: '[name].[hash:8].[ext]',
|
||||
outputPath: '/',
|
||||
name: '[path][name].[ext]',
|
||||
},
|
||||
}
|
||||
: // When using single css import images are inlined as base64 URIs in the result bundle
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"rootDirs": ["."],
|
||||
"outDir": "dist/src",
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"declaration": false,
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"skipLibCheck": true, // Temp workaround for Duplicate identifier tsc errors,
|
||||
"removeComments": false,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["es2015", "es2017.string"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@grafana/ui",
|
||||
"version": "6.3.0-alpha.39",
|
||||
"version": "6.3.0-beta.1",
|
||||
"description": "Grafana Components Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["dist", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"rootDirs": [".", "stories"],
|
||||
"module": "esnext",
|
||||
"outDir": "compiled",
|
||||
"declaration": true,
|
||||
"declarationDir": "dist",
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"typeRoots": ["./node_modules/@types", "types"],
|
||||
"skipLibCheck": true, // Temp workaround for Duplicate identifier tsc errors,
|
||||
"removeComments": false
|
||||
"declarationDir": "dist",
|
||||
"outDir": "compiled"
|
||||
}
|
||||
}
|
||||
|
||||
13
packages/tsconfig.json
Normal file
13
packages/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"skipLibCheck": true, // Temp workaround for Duplicate identifier tsc errors,
|
||||
"removeComments": false
|
||||
}
|
||||
}
|
||||
@@ -30,23 +30,6 @@ func GetTeamMembers(c *m.ReqContext) Response {
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
func GetAuthProviderLabel(authModule string) string {
|
||||
switch authModule {
|
||||
case "oauth_github":
|
||||
return "GitHub"
|
||||
case "oauth_google":
|
||||
return "Google"
|
||||
case "oauth_gitlab":
|
||||
return "GitLab"
|
||||
case "oauth_grafana_com", "oauth_grafananet":
|
||||
return "grafana.com"
|
||||
case "ldap", "":
|
||||
return "LDAP"
|
||||
default:
|
||||
return "OAuth"
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/teams/:teamId/members
|
||||
func (hs *HTTPServer) AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response {
|
||||
cmd.OrgId = c.OrgId
|
||||
|
||||
@@ -29,8 +29,11 @@ func getUserUserProfile(userID int64) Response {
|
||||
}
|
||||
|
||||
getAuthQuery := m.GetAuthInfoQuery{UserId: userID}
|
||||
query.Result.AuthLabels = []string{}
|
||||
if err := bus.Dispatch(&getAuthQuery); err == nil {
|
||||
query.Result.AuthModule = []string{getAuthQuery.Result.AuthModule}
|
||||
authLabel := GetAuthProviderLabel(getAuthQuery.Result.AuthModule)
|
||||
query.Result.AuthLabels = append(query.Result.AuthLabels, authLabel)
|
||||
query.Result.IsExternal = true
|
||||
}
|
||||
|
||||
return JSON(200, query.Result)
|
||||
@@ -277,6 +280,12 @@ func searchUser(c *m.ReqContext) (*m.SearchUsersQuery, error) {
|
||||
|
||||
for _, user := range query.Result.Users {
|
||||
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
|
||||
user.AuthLabels = make([]string, 0)
|
||||
if user.AuthModule != nil && len(user.AuthModule) > 0 {
|
||||
for _, authModule := range user.AuthModule {
|
||||
user.AuthLabels = append(user.AuthLabels, GetAuthProviderLabel(authModule))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query.Result.Page = page
|
||||
@@ -315,3 +324,20 @@ func ClearHelpFlags(c *m.ReqContext) Response {
|
||||
|
||||
return JSON(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
|
||||
}
|
||||
|
||||
func GetAuthProviderLabel(authModule string) string {
|
||||
switch authModule {
|
||||
case "oauth_github":
|
||||
return "GitHub"
|
||||
case "oauth_google":
|
||||
return "Google"
|
||||
case "oauth_gitlab":
|
||||
return "GitLab"
|
||||
case "oauth_grafana_com", "oauth_grafananet":
|
||||
return "grafana.com"
|
||||
case "ldap", "":
|
||||
return "LDAP"
|
||||
default:
|
||||
return "OAuth"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func EncryptDatasourcePaswords(c utils.CommandLine, sqlStore *sqlstore.SqlStore)
|
||||
}
|
||||
|
||||
func migrateColumn(session *sqlstore.DBSession, column string) (int, error) {
|
||||
var rows []map[string]string
|
||||
var rows []map[string][]byte
|
||||
|
||||
session.Cols("id", column, "secure_json_data")
|
||||
session.Table("data_source")
|
||||
@@ -78,7 +78,7 @@ func migrateColumn(session *sqlstore.DBSession, column string) (int, error) {
|
||||
return rowsUpdated, errutil.Wrapf(err, "failed to update column: %s", column)
|
||||
}
|
||||
|
||||
func updateRows(session *sqlstore.DBSession, rows []map[string]string, passwordFieldName string) (int, error) {
|
||||
func updateRows(session *sqlstore.DBSession, rows []map[string][]byte, passwordFieldName string) (int, error) {
|
||||
var rowsUpdated int
|
||||
|
||||
for _, row := range rows {
|
||||
@@ -94,7 +94,7 @@ func updateRows(session *sqlstore.DBSession, rows []map[string]string, passwordF
|
||||
|
||||
newRow := map[string]interface{}{"secure_json_data": data, passwordFieldName: ""}
|
||||
session.Table("data_source")
|
||||
session.Where("id = ?", row["id"])
|
||||
session.Where("id = ?", string(row["id"]))
|
||||
// Setting both columns while having value only for secure_json_data should clear the [passwordFieldName] column
|
||||
session.Cols("secure_json_data", passwordFieldName)
|
||||
|
||||
@@ -108,16 +108,20 @@ func updateRows(session *sqlstore.DBSession, rows []map[string]string, passwordF
|
||||
return rowsUpdated, nil
|
||||
}
|
||||
|
||||
func getUpdatedSecureJSONData(row map[string]string, passwordFieldName string) (map[string]interface{}, error) {
|
||||
encryptedPassword, err := util.Encrypt([]byte(row[passwordFieldName]), setting.SecretKey)
|
||||
func getUpdatedSecureJSONData(row map[string][]byte, passwordFieldName string) (map[string]interface{}, error) {
|
||||
encryptedPassword, err := util.Encrypt(row[passwordFieldName], setting.SecretKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var secureJSONData map[string]interface{}
|
||||
|
||||
if err := json.Unmarshal([]byte(row["secure_json_data"]), &secureJSONData); err != nil {
|
||||
return nil, err
|
||||
if len(row["secure_json_data"]) > 0 {
|
||||
if err := json.Unmarshal(row["secure_json_data"], &secureJSONData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
secureJSONData = map[string]interface{}{}
|
||||
}
|
||||
|
||||
jsonFieldName := util.ToCamelCase(passwordFieldName)
|
||||
|
||||
@@ -20,19 +20,30 @@ func TestPasswordMigrationCommand(t *testing.T) {
|
||||
datasources := []*models.DataSource{
|
||||
{Type: "influxdb", Name: "influxdb", Password: "foobar"},
|
||||
{Type: "graphite", Name: "graphite", BasicAuthPassword: "foobar"},
|
||||
{Type: "prometheus", Name: "prometheus", SecureJsonData: securejsondata.GetEncryptedJsonData(map[string]string{})},
|
||||
{Type: "prometheus", Name: "prometheus"},
|
||||
{Type: "elasticsearch", Name: "elasticsearch", Password: "pwd"},
|
||||
}
|
||||
|
||||
// set required default values
|
||||
for _, ds := range datasources {
|
||||
ds.Created = time.Now()
|
||||
ds.Updated = time.Now()
|
||||
ds.SecureJsonData = securejsondata.GetEncryptedJsonData(map[string]string{})
|
||||
if ds.Name == "elasticsearch" {
|
||||
ds.SecureJsonData = securejsondata.GetEncryptedJsonData(map[string]string{
|
||||
"key": "value",
|
||||
})
|
||||
} else {
|
||||
ds.SecureJsonData = securejsondata.GetEncryptedJsonData(map[string]string{})
|
||||
}
|
||||
}
|
||||
|
||||
_, err := session.Insert(&datasources)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// force secure_json_data to be null to verify that migration can handle that
|
||||
_, err = session.Exec("update data_source set secure_json_data = null where name = 'influxdb'")
|
||||
assert.Nil(t, err)
|
||||
|
||||
//run migration
|
||||
err = EncryptDatasourcePaswords(&commandstest.FakeCommandLine{}, sqlstore)
|
||||
assert.Nil(t, err)
|
||||
@@ -41,7 +52,7 @@ func TestPasswordMigrationCommand(t *testing.T) {
|
||||
var dss []*models.DataSource
|
||||
err = session.SQL("select * from data_source").Find(&dss)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(dss), 3)
|
||||
assert.Equal(t, len(dss), 4)
|
||||
|
||||
for _, ds := range dss {
|
||||
sj := ds.SecureJsonData.Decrypt()
|
||||
@@ -63,5 +74,15 @@ func TestPasswordMigrationCommand(t *testing.T) {
|
||||
if ds.Name == "prometheus" {
|
||||
assert.Equal(t, len(sj), 0)
|
||||
}
|
||||
|
||||
if ds.Name == "elasticsearch" {
|
||||
assert.Equal(t, ds.Password, "")
|
||||
key, exist := sj["key"]
|
||||
assert.True(t, exist)
|
||||
password, exist := sj["password"]
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, password, "pwd", "expected password to be moved to securejson")
|
||||
assert.Equal(t, key, "value", "expected existing key to be kept intact in securejson")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func InstallPlugin(pluginName, version string, c utils.CommandLine) error {
|
||||
}
|
||||
|
||||
logger.Infof("installing %v @ %v\n", pluginName, version)
|
||||
logger.Infof("from url: %v\n", downloadURL)
|
||||
logger.Infof("from: %v\n", downloadURL)
|
||||
logger.Infof("into: %v\n", pluginFolder)
|
||||
logger.Info("\n")
|
||||
|
||||
@@ -145,18 +145,27 @@ func downloadFile(pluginName, filePath, url string) (err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
resp, err := http.Get(url) // #nosec
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var bytes []byte
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
if _, err := os.Stat(url); err == nil {
|
||||
bytes, err = ioutil.ReadFile(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
resp, err := http.Get(url) // #nosec
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bytes, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return extractFiles(body, pluginName, filePath)
|
||||
return extractFiles(bytes, pluginName, filePath)
|
||||
}
|
||||
|
||||
func extractFiles(body []byte, pluginName string, filePath string) error {
|
||||
|
||||
@@ -216,7 +216,8 @@ type UserProfileDTO struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
|
||||
IsDisabled bool `json:"isDisabled"`
|
||||
AuthModule []string `json:"authModule"`
|
||||
IsExternal bool `json:"isExternal"`
|
||||
AuthLabels []string `json:"authLabels"`
|
||||
}
|
||||
|
||||
type UserSearchHitDTO struct {
|
||||
@@ -229,7 +230,8 @@ type UserSearchHitDTO struct {
|
||||
IsDisabled bool `json:"isDisabled"`
|
||||
LastSeenAt time.Time `json:"lastSeenAt"`
|
||||
LastSeenAtAge string `json:"lastSeenAtAge"`
|
||||
AuthModule AuthModuleConversion `json:"authModule"`
|
||||
AuthLabels []string `json:"authLabels"`
|
||||
AuthModule AuthModuleConversion `json:"-"`
|
||||
}
|
||||
|
||||
type UserIdDTO struct {
|
||||
|
||||
@@ -31,7 +31,8 @@ type IConnection interface {
|
||||
type IServer interface {
|
||||
Login(*models.LoginUserQuery) (*models.ExternalUserInfo, error)
|
||||
Users([]string) ([]*models.ExternalUserInfo, error)
|
||||
Auth(string, string) error
|
||||
Bind() error
|
||||
UserBind(string, string) error
|
||||
Dial() error
|
||||
Close()
|
||||
}
|
||||
@@ -43,6 +44,23 @@ type Server struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// Bind authenticates the connection with the LDAP server
|
||||
// - with the username and password setup in the config
|
||||
// - or, anonymously
|
||||
func (server *Server) Bind() error {
|
||||
if server.shouldAuthAdmin() {
|
||||
if err := server.AuthAdmin(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err := server.Connection.UnauthenticatedBind(server.Config.BindDN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UsersMaxRequest is a max amount of users we can request via Users().
|
||||
// Since many LDAP servers has limitations
|
||||
// on how much items can we return in one request
|
||||
@@ -149,7 +167,7 @@ func (server *Server) Login(query *models.LoginUserQuery) (
|
||||
}
|
||||
} else if server.shouldSingleBind() {
|
||||
authAndBind = true
|
||||
err = server.Auth(server.singleBindDN(query.Username), query.Password)
|
||||
err = server.UserBind(server.singleBindDN(query.Username), query.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -179,7 +197,7 @@ func (server *Server) Login(query *models.LoginUserQuery) (
|
||||
|
||||
if !authAndBind {
|
||||
// Authenticate user
|
||||
err = server.Auth(user.AuthId, query.Password)
|
||||
err = server.UserBind(user.AuthId, query.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -380,9 +398,9 @@ func (server *Server) shouldAuthAdmin() bool {
|
||||
return server.Config.BindPassword != ""
|
||||
}
|
||||
|
||||
// Auth authentificates user in LDAP
|
||||
func (server *Server) Auth(username, password string) error {
|
||||
err := server.auth(username, password)
|
||||
// UserBind authenticates the connection with the LDAP server
|
||||
func (server *Server) UserBind(username, password string) error {
|
||||
err := server.userBind(username, password)
|
||||
if err != nil {
|
||||
server.log.Error(
|
||||
fmt.Sprintf("Cannot authentificate user %s in LDAP", username),
|
||||
@@ -397,7 +415,7 @@ func (server *Server) Auth(username, password string) error {
|
||||
|
||||
// AuthAdmin authentificates LDAP admin user
|
||||
func (server *Server) AuthAdmin() error {
|
||||
err := server.auth(server.Config.BindDN, server.Config.BindPassword)
|
||||
err := server.userBind(server.Config.BindDN, server.Config.BindPassword)
|
||||
if err != nil {
|
||||
server.log.Error(
|
||||
"Cannot authentificate admin user in LDAP",
|
||||
@@ -410,8 +428,8 @@ func (server *Server) AuthAdmin() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// auth is helper for several types of LDAP authentification
|
||||
func (server *Server) auth(path, password string) error {
|
||||
// userBind authenticates the connection with the LDAP server
|
||||
func (server *Server) userBind(path, password string) error {
|
||||
err := server.Connection.Bind(path, password)
|
||||
if err != nil {
|
||||
if ldapErr, ok := err.(*ldap.Error); ok {
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestLDAPLogin(t *testing.T) {
|
||||
}
|
||||
|
||||
Convey("Login()", t, func() {
|
||||
Convey("Should get invalid credentials when auth fails", func() {
|
||||
Convey("Should get invalid credentials when userBind fails", func() {
|
||||
connection := &MockConnection{}
|
||||
entry := ldap.Entry{}
|
||||
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
||||
|
||||
@@ -145,7 +145,7 @@ func TestLDAPPrivateMethods(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("shouldAuthAdmin()", t, func() {
|
||||
Convey("it should require admin auth", func() {
|
||||
Convey("it should require admin userBind", func() {
|
||||
server := &Server{
|
||||
Config: &ServerConfig{
|
||||
BindPassword: "test",
|
||||
@@ -156,7 +156,7 @@ func TestLDAPPrivateMethods(t *testing.T) {
|
||||
So(result, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("it should not require admin auth", func() {
|
||||
Convey("it should not require admin userBind", func() {
|
||||
server := &Server{
|
||||
Config: &ServerConfig{
|
||||
BindPassword: "",
|
||||
|
||||
@@ -102,7 +102,7 @@ func TestPublicAPI(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Auth()", t, func() {
|
||||
Convey("UserBind()", t, func() {
|
||||
Convey("Should use provided DN and password", func() {
|
||||
connection := &MockConnection{}
|
||||
var actualUsername, actualPassword string
|
||||
@@ -119,7 +119,7 @@ func TestPublicAPI(t *testing.T) {
|
||||
}
|
||||
|
||||
dn := "cn=user,ou=users,dc=grafana,dc=org"
|
||||
err := server.Auth(dn, "pwd")
|
||||
err := server.UserBind(dn, "pwd")
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(actualUsername, ShouldEqual, dn)
|
||||
@@ -141,7 +141,7 @@ func TestPublicAPI(t *testing.T) {
|
||||
},
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
err := server.Auth("user", "pwd")
|
||||
err := server.UserBind("user", "pwd")
|
||||
So(err, ShouldEqual, expected)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -109,6 +109,10 @@ func (multiples *MultiLDAP) User(login string) (
|
||||
|
||||
defer server.Close()
|
||||
|
||||
if err := server.Bind(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users, err := server.Users(search)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -142,6 +146,10 @@ func (multiples *MultiLDAP) Users(logins []string) (
|
||||
|
||||
defer server.Close()
|
||||
|
||||
if err := server.Bind(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users, err := server.Users(logins)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -11,12 +11,15 @@ type MockLDAP struct {
|
||||
loginCalledTimes int
|
||||
closeCalledTimes int
|
||||
usersCalledTimes int
|
||||
bindCalledTimes int
|
||||
|
||||
dialErrReturn error
|
||||
|
||||
loginErrReturn error
|
||||
loginReturn *models.ExternalUserInfo
|
||||
|
||||
bindErrReturn error
|
||||
|
||||
usersErrReturn error
|
||||
usersFirstReturn []*models.ExternalUserInfo
|
||||
usersRestReturn []*models.ExternalUserInfo
|
||||
@@ -40,8 +43,8 @@ func (mock *MockLDAP) Users([]string) ([]*models.ExternalUserInfo, error) {
|
||||
return mock.usersRestReturn, mock.usersErrReturn
|
||||
}
|
||||
|
||||
// Auth test fn
|
||||
func (mock *MockLDAP) Auth(string, string) error {
|
||||
// UserBind test fn
|
||||
func (mock *MockLDAP) UserBind(string, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -56,6 +59,11 @@ func (mock *MockLDAP) Close() {
|
||||
mock.closeCalledTimes = mock.closeCalledTimes + 1
|
||||
}
|
||||
|
||||
func (mock *MockLDAP) Bind() error {
|
||||
mock.bindCalledTimes++
|
||||
return mock.bindErrReturn
|
||||
}
|
||||
|
||||
// MockMultiLDAP represents testing struct for multildap testing
|
||||
type MockMultiLDAP struct {
|
||||
LoginCalledTimes int
|
||||
|
||||
@@ -179,7 +179,7 @@ export default class AdminEditUserCtrl {
|
||||
const user = $scope.user;
|
||||
|
||||
// External user can not be disabled
|
||||
if (user.authModule) {
|
||||
if (user.isExternal) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
import { NavModelSrv } from 'app/core/core';
|
||||
import tags from 'app/core/utils/tags';
|
||||
|
||||
export default class AdminListUsersCtrl {
|
||||
users: any;
|
||||
@@ -32,6 +33,8 @@ export default class AdminListUsersCtrl {
|
||||
for (let i = 1; i < this.totalPages + 1; i++) {
|
||||
this.pages.push({ page: i, current: i === this.page });
|
||||
}
|
||||
|
||||
this.addUsersAuthLabels();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,10 +43,29 @@ export default class AdminListUsersCtrl {
|
||||
this.getUsers();
|
||||
}
|
||||
|
||||
getAuthModule(user: any) {
|
||||
if (user.authModule && user.authModule.length) {
|
||||
return user.authModule[0];
|
||||
addUsersAuthLabels() {
|
||||
for (const user of this.users) {
|
||||
user.authLabel = getAuthLabel(user);
|
||||
user.authLabelStyle = getAuthLabelStyle(user.authLabel);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getAuthLabel(user: any) {
|
||||
if (user.authLabels && user.authLabels.length) {
|
||||
return user.authLabels[0];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getAuthLabelStyle(label: string) {
|
||||
if (label === 'LDAP' || !label) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { color, borderColor } = tags.getTagColorsFromName(label);
|
||||
return {
|
||||
'background-color': color,
|
||||
'border-color': borderColor,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,48 +118,52 @@
|
||||
<h3 class="page-heading">Sessions</h3>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<table class="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Last seen</th>
|
||||
<th>Logged on</th>
|
||||
<th>IP address</th>
|
||||
<th>Browser & OS</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="session in sessions">
|
||||
<td ng-if="session.isActive">Now</td>
|
||||
<td ng-if="!session.isActive">{{session.seenAt}}</td>
|
||||
<td>{{session.createdAt}}</td>
|
||||
<td>{{session.clientIp}}</td>
|
||||
<td>{{session.browser}} on {{session.os}} {{session.osVersion}}</td>
|
||||
<td>
|
||||
<button class="btn btn-danger btn-small" ng-click="revokeUserSession(session.id)">
|
||||
<i class="fa fa-power-off"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="gf-form">
|
||||
<table class="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Last seen</th>
|
||||
<th>Logged on</th>
|
||||
<th>IP address</th>
|
||||
<th>Browser & OS</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="session in sessions">
|
||||
<td ng-if="session.isActive">Now</td>
|
||||
<td ng-if="!session.isActive">{{session.seenAt}}</td>
|
||||
<td>{{session.createdAt}}</td>
|
||||
<td>{{session.clientIp}}</td>
|
||||
<td>{{session.browser}} on {{session.os}} {{session.osVersion}}</td>
|
||||
<td>
|
||||
<button class="btn btn-danger btn-small" ng-click="revokeUserSession(session.id)">
|
||||
<i class="fa fa-power-off"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="gf-form-button-row">
|
||||
<button ng-if="sessions.length" class="btn btn-danger" ng-click="revokeAllUserSessions()">
|
||||
Logout user from all devices
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button ng-if="sessions.length" class="btn btn-danger" ng-click="revokeAllUserSessions()">
|
||||
Logout user from all devices
|
||||
</button>
|
||||
|
||||
<h3 class="page-heading">User status</h3>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h3 class="page-heading">User status</h3>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-danger"
|
||||
ng-if="!user.isDisabled"
|
||||
ng-click="disableUser($event)"
|
||||
bs-tooltip="user.authModule ? 'External user cannot be activated or deactivated' : ''"
|
||||
ng-class="{'disabled': user.authModule}"
|
||||
bs-tooltip="user.isExternal ? 'External user cannot be enabled or disabled' : ''"
|
||||
ng-class="{'disabled': user.isExternal}"
|
||||
>
|
||||
Disable
|
||||
</button>
|
||||
@@ -168,8 +172,8 @@
|
||||
class="btn btn-primary"
|
||||
ng-if="user.isDisabled"
|
||||
ng-click="disableUser($event)"
|
||||
bs-tooltip="user.authModule ? 'External user cannot be activated or deactivated' : ''"
|
||||
ng-class="{'disabled': user.authModule}"
|
||||
bs-tooltip="user.isExternal ? 'External user cannot be enabled or disabled' : ''"
|
||||
ng-class="{'disabled': user.isExternal}"
|
||||
>
|
||||
Enable
|
||||
</button>
|
||||
|
||||
@@ -55,7 +55,9 @@
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span class="label label-tag" ng-class="{'muted': user.isDisabled}" ng-if="ctrl.getAuthModule(user) === 'ldap'">LDAP</span>
|
||||
<span class="label label-tag" ng-style="user.authLabelStyle" ng-if="user.authLabel">
|
||||
{{user.authLabel}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span class="label label-tag label-tag--gray" ng-if="user.isDisabled">Disabled</span>
|
||||
|
||||
@@ -22,7 +22,7 @@ const DEFAULT_KEYS = ['job', 'namespace'];
|
||||
const EMPTY_SELECTOR = '{}';
|
||||
const HISTORY_ITEM_COUNT = 10;
|
||||
const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
|
||||
const NS_IN_MS = 1_000_000;
|
||||
const NS_IN_MS = 1000000;
|
||||
export const LABEL_REFRESH_INTERVAL = 1000 * 30; // 30sec
|
||||
|
||||
const wrapLabel = (label: string) => ({ label });
|
||||
|
||||
Reference in New Issue
Block a user