init alerting

This commit is contained in:
silenceqi 2021-09-04 10:11:51 +08:00
parent 5a7f978fea
commit 05300a6a01
418 changed files with 40303 additions and 72 deletions

View File

@ -8,6 +8,7 @@ import (
"infini.sh/search-center/api/index_management"
"infini.sh/search-center/config"
"path"
"infini.sh/search-center/service/alerting"
)
func Init(cfg *config.AppConfig) {
@ -36,6 +37,13 @@ func Init(cfg *config.AppConfig) {
ui.HandleUIMethod(api.DELETE, path.Join(pathPrefix, "index/:index"), handler.HandleDeleteIndexAction)
ui.HandleUIMethod(api.POST, path.Join(pathPrefix, "index/:index"), handler.HandleCreateIndexAction)
ui.HandleUIMethod(api.GET, "/elasticsearch/:id/alerting/alerts", alerting.GetAlerts)
ui.HandleUIMethod(api.POST, "/elasticsearch/:id/alerting/monitors/_search", alerting.Search)
ui.HandleUIMethod(api.POST, "/elasticsearch/:id/alerting/_indices", alerting.GetIndices)
ui.HandleUIMethod(api.POST, "/elasticsearch/:id/alerting/_aliases", alerting.GetAliases)
ui.HandleUIMethod(api.POST, "/elasticsearch/:id/alerting/_mappings", alerting.GetMappings)
ui.HandleUIMethod(api.GET, "/elasticsearch/:id/alerting/_plugins", alerting.GetPlugins)
task.RegisterScheduleTask(task.ScheduleTask{
Description: "sync reindex task result",

172
service/alerting/alert.go Normal file
View File

@ -0,0 +1,172 @@
package alerting
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
httprouter "infini.sh/framework/core/api/router"
"infini.sh/framework/core/elastic"
"io"
"net/http"
"net/url"
"strings"
)
func getQueryParam(req *http.Request, key string, or ...string) string {
query := req.URL.Query()
val := query.Get(key)
if val == "" && len(or)>0 {
return or[0]
}
return val
}
func GetAlerts (w http.ResponseWriter, req *http.Request, ps httprouter.Params){
id := ps.ByName("id")
conf := elastic.GetConfig(id)
if conf == nil {
writeError(w, errors.New("cluster not found"))
return
}
var (
from = getQueryParam(req, "from", "0")
size = getQueryParam(req, "size", "20")
search = getQueryParam(req, "search")
sortDirection = getQueryParam(req, "sortDirection", "desc")
sortField = getQueryParam(req, "sortField", "start_time")
severityLevel = getQueryParam(req, "severityLevel", "ALL")
alertState = getQueryParam(req, "alertState", "ALL")
//monitorIds = getQueryParam(req, "monitorIds")
params = map[string]string{
"startIndex": from,
"size": size,
"severityLevel": severityLevel,
"alertState": alertState,
"searchString": search,
}
)
switch sortField {
case "monitor_name", "trigger_name":
params["sortString"] = fmt.Sprintf(`%s.keyword`, sortField)
params["sortOrder"] = sortDirection
case "start_time":
params["sortString"] = sortField
params["sortOrder"] = sortDirection
case "end_time":
params["sortString"] = sortField
params["sortOrder"] = sortDirection
params["missing"] = "_first"
if sortDirection == "asc" {
params["missing"] = "_last"
}
case "acknowledged_time":
params["sortString"] = sortField
params["sortOrder"] = sortDirection
params["missing"] = "_last"
}
if clearSearch := strings.TrimSpace(search); clearSearch != ""{
searches := strings.Split(clearSearch, " ")
clearSearch = strings.Join(searches, "* *")
params["searchString"] = fmt.Sprintf("*%s*", clearSearch)
}
reqUrl := conf.Endpoint + "/_opendistro/_alerting/monitors/alerts"
res, err := doRequest(reqUrl, http.MethodGet, params, nil)
if err != nil {
writeError(w, err)
return
}
var alertRes = AlertResponse{}
err = decodeJSON(res.Body, &alertRes)
defer res.Body.Close()
if err != nil {
writeError(w, err)
return
}
var alerts = []IfaceMap{}
for _, hit := range alertRes.Alerts {
alert := IfaceMap{
"id": hit["alert_id"],
}
for k, v := range hit {
alert[k] = v
}
alert["version"] = hit["alert_version"]
}
writeJSON(w, IfaceMap{
"ok": true,
"alerts": alerts,
"totalAlerts": alertRes.TotalAlerts,
}, http.StatusOK)
}
func writeError(w http.ResponseWriter, err error) {
writeJSON(w, map[string]interface{}{
"body": map[string]interface{}{
"ok": false,
"err": err.Error(),
},
}, http.StatusOK)
}
type IfaceMap map[string]interface{}
type AlertResponse struct {
Alerts []IfaceMap `json:"alerts"`
TotalAlerts int `json:"totalAlerts"`
}
func decodeJSON(reader io.Reader, obj interface{}) error{
dec := json.NewDecoder(reader)
return dec.Decode(obj)
}
func writeJSON(w http.ResponseWriter, data interface{}, statusCode int){
w.WriteHeader(statusCode)
w.Header().Set("content-type", "application/json")
buf, _ := json.Marshal(data)
w.Write(buf)
}
var alertClient = http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
func doRequest(requestUrl string, method string, params map[string]string, body interface{}) (*http.Response, error){
var req *http.Request
if params != nil && len(params) > 0 {
var queryValues = url.Values{}
for k, v := range params {
queryValues.Set(k, v)
}
requestUrl += "?"+ queryValues.Encode()
}
var reader io.Reader
if body != nil {
switch body.(type) {
case string:
reader = bytes.NewBufferString(body.(string))
case io.Reader:
reader = body.(io.Reader)
default:
rw := &bytes.Buffer{}
enc := json.NewEncoder(rw)
err := enc.Encode(body)
if err != nil {
return nil, err
}
reader = rw
}
}
req, _ = http.NewRequest(method, requestUrl, reader)
req.Header.Set("content-type", "application/json")
return alertClient.Do(req)
}

View File

@ -0,0 +1,217 @@
package alerting
import (
"errors"
"fmt"
httprouter "infini.sh/framework/core/api/router"
"infini.sh/framework/core/elastic"
"net/http"
"runtime/debug"
)
type SearchBody struct {
Query IfaceMap `json:"query"`
Index string `json:"index"`
Size int `json:""size`
}
func Search(w http.ResponseWriter, req *http.Request, ps httprouter.Params){
id := ps.ByName("id")
conf := elastic.GetConfig(id)
if conf == nil {
writeError(w, errors.New("cluster not found"))
return
}
var body = SearchBody{}
err := decodeJSON(req.Body, &body)
if err != nil {
writeError(w, err)
return
}
reqUrl := conf.Endpoint +"/_opendistro/_alerting/monitors/_search"
params := map[string]string{
"index": body.Index,
}
body.Query["size"] = body.Size
res, err := doRequest(reqUrl, http.MethodPost, params, body.Query)
if err != nil {
writeError(w, err)
return
}
defer res.Body.Close()
var resBody = IfaceMap{}
err = decodeJSON(res.Body, &resBody)
if err != nil {
writeError(w, err)
return
}
writeJSON(w, IfaceMap{
"body": IfaceMap{
"ok": true,
"resp": resBody,
},
}, http.StatusOK)
}
func GetIndices(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
id := ps.ByName("id")
conf := elastic.GetConfig(id)
if conf == nil {
writeError(w, errors.New("cluster not found"))
return
}
var body = struct{
Index string `json:"index"`
}{}
err := decodeJSON(req.Body, &body)
if err != nil {
writeError(w, err)
return
}
reqUrl := fmt.Sprintf("%s/_cat/indices/%s", conf.Endpoint, body.Index)
params := map[string]string{
"format": "json",
"h": "health,index,status",
}
res, err := doRequest(reqUrl, http.MethodGet, params, nil)
if err != nil {
writeError(w, err)
return
}
defer res.Body.Close()
var resBody = []IfaceMap{}
err = decodeJSON(res.Body, &resBody)
if err != nil {
writeError(w, err)
return
}
writeJSON(w, IfaceMap{
"body": IfaceMap{
"ok": true,
"resp": resBody,
},
}, http.StatusOK)
}
func GetAliases(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
defer func() {
if err := recover(); err != nil {
debug.PrintStack()
}
}()
id := ps.ByName("id")
conf := elastic.GetConfig(id)
if conf == nil {
writeError(w, errors.New("cluster not found"))
return
}
var body = struct{
Alias string `json:"alias"`
}{}
err := decodeJSON(req.Body, &body)
if err != nil {
writeError(w, err)
return
}
reqUrl := fmt.Sprintf("%s/_cat/aliases/%s", conf.Endpoint, body.Alias)
params := map[string]string{
"format": "json",
"h": "alias,index",
}
res, err := doRequest(reqUrl, http.MethodGet, params, nil)
if err != nil {
writeError(w, err)
return
}
defer res.Body.Close()
var resBody = []IfaceMap{}
err = decodeJSON(res.Body, &resBody)
if err != nil {
writeError(w, err)
return
}
writeJSON(w, IfaceMap{
"body": IfaceMap{
"ok": true,
"resp": resBody,
},
}, http.StatusOK)
}
func GetMappings(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
id := ps.ByName("id")
conf := elastic.GetConfig(id)
if conf == nil {
writeError(w, errors.New("cluster not found"))
return
}
var body = struct{
Index string `json:"index"`
}{}
err := decodeJSON(req.Body, &body)
if err != nil {
writeError(w, err)
return
}
reqUrl := fmt.Sprintf("%s/%s/_mapping", conf.Endpoint, body.Index)
res, err := doRequest(reqUrl, http.MethodGet, nil, nil)
if err != nil {
writeError(w, err)
return
}
defer res.Body.Close()
var resBody = IfaceMap{}
err = decodeJSON(res.Body, &resBody)
if err != nil {
writeError(w, err)
return
}
writeJSON(w, IfaceMap{
"body": IfaceMap{
"ok": true,
"resp": resBody,
},
}, http.StatusOK)
}
func GetPlugins(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
id := ps.ByName("id")
conf := elastic.GetConfig(id)
if conf == nil {
writeError(w, errors.New("cluster not found"))
return
}
reqUrl := fmt.Sprintf("%s/_cat/plugins", conf.Endpoint)
res, err := doRequest(reqUrl, http.MethodGet, map[string]string{
"format": "json",
"h": "component",
}, nil)
if err != nil {
writeError(w, err)
return
}
defer res.Body.Close()
var resBody = []IfaceMap{}
err = decodeJSON(res.Body, &resBody)
if err != nil {
writeError(w, err)
return
}
writeJSON(w, IfaceMap{
"body": IfaceMap{
"ok": true,
"resp": resBody,
},
}, http.StatusOK)
}

118
service/alerting/monitor.go Normal file
View File

@ -0,0 +1,118 @@
package alerting
import (
"errors"
"fmt"
httprouter "infini.sh/framework/core/api/router"
"infini.sh/framework/core/elastic"
"net/http"
"strings"
)
func GetMonitor(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
id := ps.ByName("id")
conf := elastic.GetConfig(id)
if conf == nil {
writeError(w, errors.New("cluster not found"))
return
}
mid := ps.ByName("monitorID")
// /_opendistro/_alerting/monitors/uiSjqXsBHT9Hsiy5Dq6g
reqUrl := fmt.Sprintf("%s/_opendistro/_alerting/monitors/%s", conf.Endpoint, mid)
res, err := doRequest(reqUrl, http.MethodGet, nil, nil)
if err != nil {
writeError(w, err)
return
}
var resBody = IfaceMap{}
err = decodeJSON(res.Body, &resBody)
if err != nil {
writeError(w, err)
return
}
res.Body.Close()
if _, ok := resBody["monitor"]; !ok {
writeJSON(w, IfaceMap{
"body": IfaceMap{
"ok": false,
},
}, http.StatusOK)
return
}
///_opendistro/_alerting/monitors/_search?index=.opendistro-alerting-alert*
queryDSL := ` {
"size": 0,
"query"": {
"bool": {
"must": {
"term"": {
"monitor_id": "%s",
},
},
},
},
"aggs": {
"active_count": {
"terms": {
"field": "state",
}
},
"24_hour_count": {
"date_range": {
"field": "start_time",
"ranges": [{ "from": "now-24h/h" }]
}
}
}
}`
queryDSL = fmt.Sprintf(queryDSL, id)
reqUrl = fmt.Sprintf("%s/_opendistro/_alerting/monitors/_search", conf.Endpoint)
res, err = doRequest(reqUrl, http.MethodPost, map[string]string{
"index": ".opendistro-alerting-alert*",
}, queryDSL)
if err != nil {
writeError(w, err)
return
}
var searchResBody = IfaceMap{}
err = decodeJSON(res.Body, &searchResBody)
if err != nil {
writeError(w, err)
return
}
//dayCount := queryValue(searchResBody, "aggregations.24_hour_count.buckets.0.doc_count", 0)
//activeBuckets := queryValue(searchResBody, "aggregations.active_count.buckets",[]interface{}{})
writeJSON(w, IfaceMap{
"body": IfaceMap{
"ok": true,
"resp": resBody,
},
}, http.StatusOK)
}
func queryValue(obj map[string]interface{}, key string, defaultValue interface{}) interface{} {
if key == "" {
return obj
}
idx := strings.Index(key, ".")
if idx == -1 {
if v, ok := obj[key]; ok {
return v
}
return defaultValue
}
ckey := key[0:idx]
if v, ok := obj[ckey]; ok {
if vmap, ok := v.(map[string]interface{}); ok {
return queryValue(vmap, key[idx+1:], defaultValue)
}
}
return defaultValue
}

View File

@ -104,6 +104,17 @@ export default [
component: './DevTool/Console',
},
//alerting
{
routes:[
{ path: '/', redirect: '/' },
],
path: '/alerting',
name: 'alerting',
icon: 'alert',
component: './Alerting/index',
},
//data
{
path: '/data',

View File

@ -27,6 +27,7 @@
"dns": "^0.2.2",
"dva": "^2.4.0",
"enquire-js": "^0.2.1",
"formik": "^2.2.9",
"fp-ts": "^2.10.5",
"hash.js": "^1.1.5",
"honeycomb-grid": "^3.1.7",
@ -63,13 +64,15 @@
"react-json-view": "^1.19.1",
"react-router-dom": "^4.3.1",
"react-use": "^17.2.4",
"react-vis": "^1.11.7",
"readline": "^1.3.0",
"repl": "^0.1.3",
"reqwest": "^2.0.5",
"rison-node": "^2.1.1",
"rxjs": "^7.2.0",
"sass-loader": "^8.0.2",
"use-query-params": "^1.2.3"
"use-query-params": "^1.2.3",
"uuidv4": "^6.2.12"
},
"devDependencies": {
"antd-pro-merge-less": "^0.1.0",

View File

@ -32,6 +32,7 @@ import {
EuiSwitch,
} from '@elastic/eui';
import {useState} from 'react';
import {Button, Icon} from 'antd';
interface HeaderProps {
@ -140,18 +141,15 @@ export const Header: React.FC<HeaderProps> = ({
</EuiFlexItem>
<EuiFlexItem grow={false} >
<EuiFormRow hasEmptyLabelSpace style={{marginBottom:60,marginTop:'auto'}}>
<EuiButton
fill
iconSide="right"
iconType="arrowRight"
<Button
type="primary"
onClick={() => {
goToNextStep(query, viewName)
}}
isDisabled={isNextStepDisabled || viewName.replace(' ', '').length <= 0}
data-test-subj="createIndexPatternGoToStep2Button"
disabled={isNextStepDisabled || viewName.replace(' ', '').length <= 0}
>
</EuiButton>
<Icon type="right"/>
</Button>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>

View File

@ -105,7 +105,7 @@ export class IndicesList extends React.Component<IndicesListProps, IndicesListSt
iconSide="right"
onClick={this.openPerPageControl}
>
{`Rows per page: ${perPage}`}
{`每页行数: ${perPage}`}
</EuiButtonEmpty>
);

View File

@ -20,6 +20,7 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty } from '@elastic/eui';
import {Button} from 'antd';
export const ActionButtons = ({
goToPreviousStep,
@ -32,19 +33,18 @@ export const ActionButtons = ({
}) => (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="arrowLeft" onClick={goToPreviousStep}>
<Button icon="left" onClick={goToPreviousStep}>
</EuiButtonEmpty>
</Button>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
isDisabled={!submittable}
data-test-subj="createIndexPatternButton"
fill
<Button
disabled={!submittable}
type="primary"
onClick={createIndexPattern}
>
</EuiButton>
</Button>
</EuiFlexItem>
</EuiFlexGroup>
);

View File

@ -39,6 +39,10 @@ import { IndexPatternTableItem } from '../types';
import { getIndexPatterns } from '../utils';
import {useGlobalContext} from '../../context';
import { IndexPattern, IndexPatternField } from '../../import';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import styles from '@/pages/System/Cluster/step.less';
import clusterBg from '@/assets/cluster_bg.png';
export interface EditIndexPatternProps extends RouteComponentProps {
indexPattern: IndexPattern;
@ -152,16 +156,43 @@ export const EditIndexPattern = withRouter(
const showTagsSection = Boolean(indexPattern.timeFieldName || (tags && tags.length > 0));
const content = (
<div className={styles.pageHeaderContent}>
<EuiText>
<p>
<strong>{indexPattern.title}</strong> Elasticsearch 使 Elasticsearch{' '}
<EuiLink
href="http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html"
target="_blank"
external
>
{mappingAPILink}
</EuiLink>
</p>
</EuiText>
</div>
);
const extraContent = (
<div className={styles.extraImg}>
<img
alt="数据视图"
src={clusterBg}
/>
</div>
);
return (
<PageHeaderWrapper title={indexPattern.viewName} content={content} extraContent={extraContent}>
<EuiPanel paddingSize={'l'}>
<div data-test-subj="editIndexPattern" role="region" aria-label={headingAriaLabel}>
<IndexHeader
{/* <IndexHeader
indexPattern={indexPattern}
setDefault={setDefaultPattern}
refreshFields={refreshFields}
deleteIndexPatternClick={removePattern}
defaultIndex={defaultIndex}
/>
/> */}
<EuiSpacer size="s" />
{showTagsSection && (
<EuiFlexGroup wrap>
@ -208,6 +239,7 @@ export const EditIndexPattern = withRouter(
/>
</div>
</EuiPanel>
</PageHeaderWrapper>
);
}
);

View File

@ -24,6 +24,7 @@ import { Table } from './components/table';
import { getFieldFormat } from './lib';
import { IndexedFieldItem } from './types';
import { IndexPatternField, IndexPattern, IFieldType } from '../../../import';
import {EuiContext} from '@elastic/eui';
interface IndexedFieldsTableProps {
fields: IndexPatternField[];
@ -108,11 +109,18 @@ export class IndexedFieldsTable extends Component<
return (
<div>
<EuiContext i18n={{
mapping: {
'euiTablePagination.rowsPerPage': '每页行数',
'euiTablePagination.rowsPerPageOption': '{rowsPerPage} 行'
}
}}>
<Table
indexPattern={indexPattern}
items={fields}
editField={(field) => this.props.helpers.redirectToRoute(field)}
/>
</EuiContext>
</div>
);
}

View File

@ -85,6 +85,7 @@ export default {
'component.noticeIcon.empty': 'No notifications',
'menu.home': 'Home',
'menu.devtool': 'CONSOLE',
'menu.alerting': 'AERTING',
'menu.cluster': 'CLUSTER',
'menu.cluster.overview': 'OVERVIEW',

View File

@ -92,6 +92,7 @@ export default {
'menu.home': '首页',
'menu.devtool': '开发工具',
'menu.alerting': '告警管理',
'menu.cluster': '集群管理',
'menu.cluster.overview': '概览',

View File

@ -0,0 +1,49 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Route } from 'react-router-dom';
import 'react-vis/dist/style.css';
// TODO: review the CSS style and migrate the necessary style to SASS, as Less is not supported in Kibana "new platform" anymore
// import './less/main.less';
import Main from './pages/Main';
import { CoreContext } from './utils/CoreContext';
export function renderApp(coreStart, params) {
const isDarkMode = coreStart.uiSettings.get('theme:darkMode') || false;
coreStart.chrome.setBreadcrumbs([{ text: 'Alerting' }]); // Set Breadcrumbs for the plugin
// Load Chart's dark mode CSS
if (isDarkMode) {
require('@elastic/charts/dist/theme_only_dark.css');
} else {
require('@elastic/charts/dist/theme_only_light.css');
}
// render react to DOM
ReactDOM.render(
<Router>
<CoreContext.Provider
value={{ http: coreStart.http, isDarkMode, notifications: coreStart.notifications }}
>
<Route render={(props) => <Main title="Alerting" {...props} />} />
</CoreContext.Provider>
</Router>,
params.element
);
return () => ReactDOM.unmountComponentAtNode(params.element);
}

View File

@ -0,0 +1,102 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import _ from 'lodash';
import PropTypes from 'prop-types';
import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiText } from '@elastic/eui';
const propTypes = {
titleText: PropTypes.string,
emptyText: PropTypes.string,
name: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
onAdd: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired,
onRenderKeyField: PropTypes.func.isRequired,
onRenderValueField: PropTypes.func.isRequired,
addButtonText: PropTypes.string,
removeButtonText: PropTypes.string,
isEnabled: PropTypes.bool,
};
const defaultProps = {
titleText: '',
emptyText: 'No attributes found.',
addButtonText: 'Add',
removeButtonText: 'Remove',
isEnabled: true,
};
const AttributeEditor = ({
titleText,
emptyText,
name,
items,
onAdd,
onRemove,
onRenderKeyField,
onRenderValueField,
addButtonText,
removeButtonText,
isEnabled,
}) => {
return (
<EuiFlexGroup direction="column" alignItems="flexStart" style={{ paddingLeft: '10px' }}>
{!_.isEmpty(titleText) ? (
<EuiFlexItem>
<EuiText size="xs">{titleText}</EuiText>
</EuiFlexItem>
) : null}
{!_.isEmpty(items) ? (
items.map((item, index) => (
<EuiFlexItem style={{ marginBottom: 0 }} key={`${name}.${index}.key`}>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
{onRenderKeyField(`${name}.${index}.key`, index, isEnabled)}
</EuiFlexItem>
<EuiFlexItem>
{onRenderValueField(`${name}.${index}.value`, index, isEnabled)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
style={{ marginTop: 10 }}
size="s"
onClick={e => onRemove(index)}
disabled={!isEnabled}
>
{removeButtonText}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
))
) : (
<EuiFlexItem style={{ marginBottom: 0 }}>
<EuiText size="xs"> {emptyText} </EuiText>
</EuiFlexItem>
)}
<EuiFlexItem>
<EuiButton size="s" onClick={onAdd} disabled={!isEnabled}>
{addButtonText}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
};
AttributeEditor.propTypes = propTypes;
AttributeEditor.defaultProps = defaultProps;
export default AttributeEditor;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import AttributeEditor from './AttributeEditor';
export default AttributeEditor;

View File

@ -0,0 +1,164 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import queryString from 'query-string';
import { EuiBreadcrumbs } from '@elastic/eui';
import {
APP_PATH,
DESTINATION_ACTIONS,
MONITOR_ACTIONS,
TRIGGER_ACTIONS,
} from '../../utils/constants';
const propTypes = {
history: PropTypes.object.isRequired,
httpClient: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
title: PropTypes.string.isRequired,
};
export default class Breadcrumbs extends Component {
constructor(props) {
super(props);
this.state = { breadcrumbs: [] };
this.getBreadcrumbs = this.getBreadcrumbs.bind(this);
}
componentDidMount() {
this.getBreadcrumbs();
}
componentDidUpdate(prevProps) {
const {
location: { pathname: prevPathname, search: prevSearch },
} = prevProps;
const {
location: { pathname, search },
} = this.props;
if (prevPathname + prevSearch !== pathname + search) {
this.getBreadcrumbs();
}
}
async getBreadcrumbs() {
const {
httpClient,
history,
location: { state: routeState },
} = this.props;
const rawBreadcrumbs = await getBreadcrumbs(window.location.hash, routeState, httpClient);
const breadcrumbs = rawBreadcrumbs.map((breadcrumb) =>
createEuiBreadcrumb(breadcrumb, history)
);
this.setState({ breadcrumbs });
}
render() {
const { breadcrumbs } = this.state;
return (
<EuiBreadcrumbs
breadcrumbs={breadcrumbs}
responsive={false}
truncate={true}
style={{ padding: '0px 15px' }}
/>
);
}
}
Breadcrumbs.propTypes = propTypes;
export function createEuiBreadcrumb(breadcrumb, history) {
const { text, href } = breadcrumb;
return {
text,
href: `#${href}`,
onClick: (e) => {
e.preventDefault();
history.push(href);
},
};
}
export async function getBreadcrumbs(hash, routeState, httpClient) {
const routes = parseLocationHash(hash);
const asyncBreadcrumbs = await Promise.all(
routes.map((route) => getBreadcrumb(route, routeState, httpClient))
);
const breadcrumbs = _.flatten(asyncBreadcrumbs).filter((breadcrumb) => !!breadcrumb);
return breadcrumbs;
}
export function parseLocationHash(hash) {
return hash.split('/').filter((route) => !!route);
}
export async function getBreadcrumb(route, routeState, httpClient) {
const [base, queryParams] = route.split('?');
if (!base) return null;
// This condition is true for any auto generated 20 character long,
// URL-safe, base64-encoded document ID by elasticsearch
if (RegExp(/^[0-9a-z_-]{20}$/i).test(base)) {
const { action } = queryString.parse(`?${queryParams}`);
switch (action) {
case DESTINATION_ACTIONS.UPDATE_DESTINATION:
const destinationName = _.get(routeState, 'destinationToEdit.name', base);
const destinationBreadcrumbs = [{ text: destinationName, href: `/destinations/${base}` }];
if (action === DESTINATION_ACTIONS.UPDATE_DESTINATION) {
destinationBreadcrumbs.push({ text: 'Update destination', href: '/' });
}
return destinationBreadcrumbs;
default:
// TODO::Everything else is considered as monitor, we should break this.
let monitorName = base;
try {
const response = await httpClient.get(`../api/alerting/monitors/${base}`);
if (response.ok) {
monitorName = response.resp.name;
}
} catch (err) {
console.error(err);
}
const breadcrumbs = [{ text: monitorName, href: `/monitors/${base}` }];
if (action === MONITOR_ACTIONS.UPDATE_MONITOR)
breadcrumbs.push({ text: 'Update monitor', href: '/' });
if (action === TRIGGER_ACTIONS.CREATE_TRIGGER)
breadcrumbs.push({ text: 'Create trigger', href: '/' });
if (action === TRIGGER_ACTIONS.UPDATE_TRIGGER)
breadcrumbs.push({ text: 'Update trigger', href: '/' });
return breadcrumbs;
}
}
return {
'#': { text: 'Alerting', href: '/' },
monitors: { text: 'Monitors', href: '/monitors' },
dashboard: { text: 'Dashboard', href: '/dashboard' },
destinations: { text: 'Destinations', href: '/destinations' },
'create-monitor': [
{ text: 'Monitors', href: '/monitors' },
{ text: 'Create monitor', href: APP_PATH.CREATE_MONITOR },
],
'create-destination': [
{ text: 'Destinations', href: '/destinations' },
{ text: 'Create destination', href: APP_PATH.CREATE_DESTINATION },
],
}[base];
}

View File

@ -0,0 +1,162 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { mount, shallow } from 'enzyme';
import { EuiBreadcrumbs } from '@elastic/eui';
import Breadcrumbs, {
createEuiBreadcrumb,
getBreadcrumbs,
parseLocationHash,
getBreadcrumb,
} from './Breadcrumbs';
import { historyMock, httpClientMock } from '../../../test/mocks';
import { MONITOR_ACTIONS, TRIGGER_ACTIONS } from '../../utils/constants';
const monitorId = 'soDk30SjdsekoaSoMcj1';
const location = {
hash: '',
pathname: '/monitors/random_id_20_chars__',
search: '',
state: undefined,
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('Breadcrumbs', () => {
const title = 'Alerting';
httpClientMock.get = jest.fn().mockResolvedValue({ ok: true, resp: { name: 'random monitor' } });
delete global.window.location;
global.window.location = { hash: '' };
test('renders', () => {
const wrapper = shallow(
<Breadcrumbs
title={title}
location={location}
httpClient={httpClientMock}
history={historyMock}
/>
);
expect(wrapper).toMatchSnapshot();
});
test('calls getBreadcrumbs on mount and when pathname+search are updated', () => {
const getBreadcrumbs = jest.spyOn(Breadcrumbs.prototype, 'getBreadcrumbs');
const wrapper = mount(
<Breadcrumbs
title={title}
location={location}
httpClient={httpClientMock}
history={historyMock}
/>
);
expect(getBreadcrumbs).toHaveBeenCalledTimes(1);
wrapper.setProps({ location: { ...location, search: '?search=new' } });
expect(getBreadcrumbs).toHaveBeenCalledTimes(2);
});
});
describe('createEuiBreadcrumbs', () => {
test('creates breadcrumbs for EuiBreadcrumbs', () => {
const breadcrumb = { text: 'This is a breadcrumb', href: '/this-is-the-href' };
expect(createEuiBreadcrumb(breadcrumb, historyMock)).toMatchSnapshot();
});
});
describe('parseLocationHash', () => {
test('correctly parses location hash', () => {
const hash = `#/monitors/${monitorId}?action=${TRIGGER_ACTIONS.CREATE_TRIGGER}`;
expect(parseLocationHash(hash)).toMatchSnapshot();
});
test('filters out falsy string values', () => {
const hash = '#/monitors/';
expect(parseLocationHash(hash)).toMatchSnapshot();
});
});
describe('getBreadcrumb', () => {
const routeState = { destinationToEdit: { name: 'unique_name' } };
test('returns null if falsy base value', async () => {
expect(await getBreadcrumb('', {}, httpClientMock)).toBe(null);
expect(
await getBreadcrumb(`?action=${TRIGGER_ACTIONS.CREATE_TRIGGER}`, {}, httpClientMock)
).toBe(null);
});
test('returns correct constant breadcrumbs', async () => {
expect(await getBreadcrumb('#', {}, httpClientMock)).toMatchSnapshot();
expect(await getBreadcrumb('monitors', {}, httpClientMock)).toMatchSnapshot();
expect(await getBreadcrumb('dashboard', {}, httpClientMock)).toMatchSnapshot();
expect(await getBreadcrumb('destinations', {}, httpClientMock)).toMatchSnapshot();
expect(await getBreadcrumb('create-monitor', {}, httpClientMock)).toMatchSnapshot();
expect(await getBreadcrumb('create-destination', {}, httpClientMock)).toMatchSnapshot();
});
describe('when matching document IDs', () => {
test('calls get monitor route', async () => {
httpClientMock.get.mockResolvedValue({ ok: true, resp: { name: 'random_name' } });
await getBreadcrumb(monitorId, {}, httpClientMock);
expect(httpClientMock.get).toHaveBeenCalled();
expect(httpClientMock.get).toHaveBeenCalledWith(`../api/alerting/monitors/${monitorId}`);
});
test('returns monitor name', async () => {
httpClientMock.get.mockResolvedValue({ ok: true, resp: { name: 'random_name' } });
expect(await getBreadcrumb(monitorId, {}, httpClientMock)).toMatchSnapshot();
});
test('uses monitor id as name if request fails', async () => {
httpClientMock.get.mockRejectedValue({ ok: true, resp: { name: 'random_name' } });
expect(await getBreadcrumb(monitorId, {}, httpClientMock)).toMatchSnapshot();
});
test('uses monitor id as name if ok=false', async () => {
httpClientMock.get.mockResolvedValue({ ok: false, resp: { name: 'random_name' } });
expect(await getBreadcrumb(monitorId, {}, httpClientMock)).toMatchSnapshot();
});
test('adds appropriate action breadcrumb', async () => {
httpClientMock.get.mockResolvedValue({ ok: true, resp: { name: 'random_name' } });
expect(
await getBreadcrumb(
`${monitorId}?action=${MONITOR_ACTIONS.UPDATE_MONITOR}`,
{},
httpClientMock
)
).toMatchSnapshot();
expect(
await getBreadcrumb(
`${monitorId}?action=${TRIGGER_ACTIONS.CREATE_TRIGGER}`,
{},
httpClientMock
)
).toMatchSnapshot();
expect(
await getBreadcrumb(
`${monitorId}?action=${TRIGGER_ACTIONS.UPDATE_TRIGGER}`,
{},
httpClientMock
)
).toMatchSnapshot();
});
});
});

View File

@ -0,0 +1,157 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Breadcrumbs renders 1`] = `
<EuiBreadcrumbs
breadcrumbs={Array []}
responsive={false}
style={
Object {
"padding": "0px 15px",
}
}
truncate={true}
/>
`;
exports[`createEuiBreadcrumbs creates breadcrumbs for EuiBreadcrumbs 1`] = `
Object {
"href": "#/this-is-the-href",
"onClick": [Function],
"text": "This is a breadcrumb",
}
`;
exports[`getBreadcrumb returns correct constant breadcrumbs 1`] = `
Object {
"href": "/",
"text": "Alerting",
}
`;
exports[`getBreadcrumb returns correct constant breadcrumbs 2`] = `
Object {
"href": "/monitors",
"text": "Monitors",
}
`;
exports[`getBreadcrumb returns correct constant breadcrumbs 3`] = `
Object {
"href": "/dashboard",
"text": "Dashboard",
}
`;
exports[`getBreadcrumb returns correct constant breadcrumbs 4`] = `
Object {
"href": "/destinations",
"text": "Destinations",
}
`;
exports[`getBreadcrumb returns correct constant breadcrumbs 5`] = `
Array [
Object {
"href": "/monitors",
"text": "Monitors",
},
Object {
"href": "/create-monitor",
"text": "Create monitor",
},
]
`;
exports[`getBreadcrumb returns correct constant breadcrumbs 6`] = `
Array [
Object {
"href": "/destinations",
"text": "Destinations",
},
Object {
"href": "/create-destination",
"text": "Create destination",
},
]
`;
exports[`getBreadcrumb when matching document IDs adds appropriate action breadcrumb 1`] = `
Array [
Object {
"href": "/monitors/soDk30SjdsekoaSoMcj1",
"text": "random_name",
},
Object {
"href": "/",
"text": "Update monitor",
},
]
`;
exports[`getBreadcrumb when matching document IDs adds appropriate action breadcrumb 2`] = `
Array [
Object {
"href": "/monitors/soDk30SjdsekoaSoMcj1",
"text": "random_name",
},
Object {
"href": "/",
"text": "Create trigger",
},
]
`;
exports[`getBreadcrumb when matching document IDs adds appropriate action breadcrumb 3`] = `
Array [
Object {
"href": "/monitors/soDk30SjdsekoaSoMcj1",
"text": "random_name",
},
Object {
"href": "/",
"text": "Update trigger",
},
]
`;
exports[`getBreadcrumb when matching document IDs returns monitor name 1`] = `
Array [
Object {
"href": "/monitors/soDk30SjdsekoaSoMcj1",
"text": "random_name",
},
]
`;
exports[`getBreadcrumb when matching document IDs uses monitor id as name if ok=false 1`] = `
Array [
Object {
"href": "/monitors/soDk30SjdsekoaSoMcj1",
"text": "soDk30SjdsekoaSoMcj1",
},
]
`;
exports[`getBreadcrumb when matching document IDs uses monitor id as name if request fails 1`] = `
Array [
Object {
"href": "/monitors/soDk30SjdsekoaSoMcj1",
"text": "soDk30SjdsekoaSoMcj1",
},
]
`;
exports[`parseLocationHash correctly parses location hash 1`] = `
Array [
"#",
"monitors",
"soDk30SjdsekoaSoMcj1?action=create-trigger",
]
`;
exports[`parseLocationHash filters out falsy string values 1`] = `
Array [
"#",
"monitors",
]
`;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import Breadcrumbs from './Breadcrumbs';
export default Breadcrumbs;

View File

@ -0,0 +1,41 @@
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
const ChartContainer = ({ children, style }) => (
<div
style={{
borderRadius: '5px',
padding: '10px',
border: '1px solid #D9D9D9',
height: '250px',
width: '100%',
...style,
}}
>
{children}
</div>
);
ChartContainer.propTypes = {
style: PropTypes.object,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf([PropTypes.node])]).isRequired,
};
ChartContainer.defaultPropTypes = {
style: {},
};
export { ChartContainer };

View File

@ -0,0 +1,470 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
/*
This file has been cloned and modified from the original source(https://github.com/uber/react-vis/blob/35c8950722e9ad60214399291eda731ac31af267/src/plot/highlight.js) due to its limitations.
We should revert this to an Actual Highlight Component from react-vis when it is ready for use with
Controlled Highlight, allow only Drag and not to rebrush.
* Removes capability for brushing on Area
* Adds capability for dragging on chart
* Styles Component as per the our mocks
*/
import React from 'react';
import { ScaleUtils, AbstractSeries } from 'react-vis';
import { isEqual } from 'lodash';
import PropTypes from 'prop-types';
const getLocs = evt => {
const targetBounding = evt.currentTarget.getBoundingClientRect();
const offsetX = evt.clientX - targetBounding.left;
const offsetY = evt.clientY - targetBounding.top;
return {
xLoc: offsetX,
yLoc: offsetY,
};
};
class Highlight extends AbstractSeries {
constructor(props) {
super(props);
this.state = {
dragging: false,
brushArea: { bottom: 0, right: 0, left: 0, top: 0 },
brushing: false,
startLocX: 0,
startLocY: 0,
dragArea: null,
};
}
componentDidMount() {
const { highlightRef } = this.props;
if (highlightRef) {
highlightRef({
setHighlightedArea: this.setHighlightedArea,
});
}
}
componentDidUpdate(prevProps) {
if (!isEqual(this.props, prevProps)) {
this.setHighlightedArea({
...this.props.highlightedArea,
});
}
}
_getDrawArea(xLoc, yLoc) {
const { startLocX, startLocY } = this.state;
const {
enableX,
enableY,
highlightWidth,
highlightHeight,
innerWidth,
innerHeight,
marginLeft,
marginRight,
marginBottom,
marginTop,
} = this.props;
const plotHeight = innerHeight + marginTop + marginBottom;
const plotWidth = innerWidth + marginLeft + marginRight;
const touchWidth = highlightWidth || plotWidth;
const touchHeight = highlightHeight || plotHeight;
return {
bottom: enableY ? Math.max(startLocY, yLoc) : touchHeight,
right: enableX ? Math.max(startLocX, xLoc) : touchWidth,
left: enableX ? Math.min(xLoc, startLocX) : 0,
top: enableY ? Math.min(yLoc, startLocY) : 0,
};
}
_getDragArea(xLoc, yLoc) {
const { enableX, enableY } = this.props;
const { startLocX, startLocY, dragArea } = this.state;
return {
bottom: dragArea.bottom + (enableY ? yLoc - startLocY : 0),
left: dragArea.left + (enableX ? xLoc - startLocX : 0),
right: dragArea.right + (enableX ? xLoc - startLocX : 0),
top: dragArea.top + (enableY ? yLoc - startLocY : 0),
};
}
_clickedOutsideDrag(xLoc, yLoc) {
const { enableX, enableY, marginLeft, marginTop } = this.props;
const {
dragArea,
brushArea: { left, right, top, bottom },
} = this.state;
const actualXLoc = xLoc + marginLeft;
const actualYLoc = yLoc + marginTop;
const clickedOutsideDragX = dragArea && (actualXLoc < left || actualXLoc > right);
const clickedOutsideDragY = dragArea && (actualYLoc < top || actualYLoc > bottom);
if (enableX && enableY) {
return clickedOutsideDragX || clickedOutsideDragY;
}
if (enableX) {
return clickedOutsideDragX;
}
if (enableY) {
return clickedOutsideDragY;
}
return true;
}
_convertAreaToCoordinates(brushArea) {
// NOTE only continuous scales are supported for brushing/getting coordinates back
const { enableX, enableY, marginLeft, marginTop } = this.props;
const xScale = ScaleUtils.getAttributeScale(this.props, 'x');
const yScale = ScaleUtils.getAttributeScale(this.props, 'y');
// Ensure that users wishes are being respected about which scales are evaluated
// this is specifically enabled to ensure brushing on mixed categorical and linear
// charts will run as expected
if (enableX && enableY) {
return {
bottom: yScale.invert(brushArea.bottom),
left: xScale.invert(brushArea.left - marginLeft),
right: xScale.invert(brushArea.right - marginLeft),
top: yScale.invert(brushArea.top),
};
}
if (enableY) {
return {
bottom: yScale.invert(brushArea.bottom - marginTop),
top: yScale.invert(brushArea.top - marginTop),
};
}
if (enableX) {
return {
left: xScale.invert(brushArea.left - marginLeft),
right: xScale.invert(brushArea.right - marginLeft),
};
}
return {};
}
/*
This is a public method which can be accessed via ref which will allow
consumer to control Highlight Window.
*/
setHighlightedArea = highlightArea => {
const {
highlightHeight,
highlightWidth,
innerWidth,
innerHeight,
marginLeft,
marginBottom,
marginTop,
marginRight,
} = this.props;
const xScale = ScaleUtils.getAttributeScale(this.props, 'x');
const yScale = ScaleUtils.getAttributeScale(this.props, 'y');
const plotHeight = innerHeight + marginTop + marginBottom;
const plotWidth = innerWidth + marginLeft + marginRight;
let bottomPos = highlightHeight || plotHeight;
if (highlightArea.bottom) {
bottomPos = yScale(highlightArea.bottom) + marginTop;
}
let topPos = 0;
if (highlightArea.top) {
topPos = yScale(highlightArea.top) + marginTop;
}
let leftPos = 0 + marginLeft;
if (highlightArea.left) {
leftPos = xScale(highlightArea.left) + marginLeft;
}
let rightPos = highlightWidth || plotWidth;
if (highlightArea.right) {
rightPos = xScale(highlightArea.right) + marginLeft;
}
const brushArea = {
bottom: bottomPos,
right: rightPos,
left: leftPos,
top: topPos,
};
this.setState({ brushArea, dragArea: brushArea });
};
startBrushing(e) {
e.preventDefault();
const { onDragStart, drag } = this.props;
const { dragArea } = this.state;
const { xLoc, yLoc } = getLocs(e);
const clickedOutsideDrag = this._clickedOutsideDrag(xLoc, yLoc);
if (!clickedOutsideDrag && drag) {
this.setState({
dragging: true,
brushArea: dragArea,
brushing: false,
startLocX: xLoc,
startLocY: yLoc,
});
if (onDragStart) {
onDragStart(e);
}
}
}
stopBrushing(e) {
const { brushing, dragging, brushArea } = this.state;
// Quickly short-circuit if the user isn't brushing in our component
if (!brushing && !dragging) {
return;
}
const { onBrushEnd, onDragEnd, drag } = this.props;
const noHorizontal = Math.abs(brushArea.right - brushArea.left) < 5;
const noVertical = Math.abs(brushArea.top - brushArea.bottom) < 5;
// Invoke the callback with null if the selected area was < 5px
const isNulled = noVertical || noHorizontal;
// Clear the draw area
this.setState({
brushing: false,
dragging: false,
brushArea: drag ? brushArea : { top: 0, right: 0, bottom: 0, left: 0 },
startLocX: 0,
startLocY: 0,
dragArea: drag && !isNulled && brushArea,
});
if (brushing && onBrushEnd) {
onBrushEnd(!isNulled ? this._convertAreaToCoordinates(brushArea) : null);
}
if (drag && onDragEnd) {
onDragEnd(!isNulled ? this._convertAreaToCoordinates(brushArea) : null);
}
}
onBrush(e) {
const { marginLeft, marginRight, innerWidth, onBrush, onDrag, drag } = this.props;
const { brushing, dragging } = this.state;
const { xLoc, yLoc } = getLocs(e);
if (brushing) {
const brushArea = this._getDrawArea(xLoc, yLoc);
this.setState({ brushArea });
if (onBrush) {
onBrush(this._convertAreaToCoordinates(brushArea));
}
}
if (drag && dragging) {
const brushArea = this._getDragArea(xLoc, yLoc);
const rightBoundary = innerWidth + marginRight;
const leftBoundary = marginLeft;
if (brushArea.right <= rightBoundary && brushArea.left >= leftBoundary) {
this.setState({ brushArea });
if (onDrag) {
onDrag(this._convertAreaToCoordinates(brushArea));
}
}
}
}
getHighlighterStyles() {
const { isDarkMode } = this.props;
if (isDarkMode) {
return {
fill: 'black',
opacity: 0.1,
};
}
return {
fill: 'white',
opacity: 0.8,
};
}
render() {
const {
color,
className,
highlightHeight,
highlightWidth,
highlightX,
highlightY,
innerWidth,
innerHeight,
marginLeft,
marginRight,
marginTop,
marginBottom,
opacity,
} = this.props;
const {
brushArea: { left, right, top, bottom },
} = this.state;
let leftPos = 0;
if (highlightX) {
const xScale = ScaleUtils.getAttributeScale(this.props, 'x');
leftPos = xScale(highlightX);
}
let topPos = 0;
if (highlightY) {
const yScale = ScaleUtils.getAttributeScale(this.props, 'y');
topPos = yScale(highlightY);
}
const plotWidth = marginLeft + marginRight + innerWidth;
const plotHeight = marginTop + marginBottom + innerHeight;
const touchWidth = highlightWidth || plotWidth;
const touchHeight = highlightHeight || plotHeight;
return (
<g
transform={`translate(${leftPos}, ${topPos})`}
className={`${className} rv-highlight-container`}
>
{/* An overlay on a graph which allows dragging */}
<rect
className="rv-mouse-target"
opacity="0"
cursor="move"
x={marginLeft}
y={0}
width={Math.max(touchWidth, 0)}
height={Math.max(touchHeight, 0)}
onMouseDown={e => this.startBrushing(e)}
onMouseMove={e => this.onBrush(e)}
onMouseUp={e => this.stopBrushing(e)}
onMouseLeave={e => this.stopBrushing(e)}
// preventDefault() so that mouse event emulation does not happen
onTouchEnd={e => {
e.preventDefault();
this.stopBrushing(e);
}}
onTouchCancel={e => {
e.preventDefault();
this.stopBrushing(e);
}}
onContextMenu={e => e.preventDefault()}
onContextMenuCapture={e => e.preventDefault()}
/>
{/* Left side of overlay which allows us to give an opacity to cover */}
<rect
pointerEvents="none"
className="rv-mouse-target"
x={marginLeft}
y="0"
width={Math.max(left - marginLeft, 0)}
height={Math.max(touchHeight, 0)}
onMouseLeave={e => e.preventDefault()}
{...this.getHighlighterStyles()}
/>
{/* A Center Highlighter */}
<rect
pointerEvents="none"
opacity={opacity}
fill={color}
x={left}
y={top}
width={Math.max(0, right - left)}
height={Math.max(0, bottom - top)}
onMouseLeave={e => e.preventDefault()}
/>
{/* Right side of overlay which allows us to give an opacity to cover */}
<rect
pointerEvents="none"
className="rv-mouse-target"
x={right}
y="0"
width={Math.max(touchWidth - right, 0)}
height={Math.max(touchHeight, 0)}
onMouseLeave={e => e.preventDefault()}
{...this.getHighlighterStyles()}
/>
{/* Draws border lines on Highlighted area */}
<g>
<rect
pointerEvents="none"
x={left}
y={top}
width="1"
height={Math.max(0, bottom - top)}
fill="rgb(151,151,151)"
stroke="none"
onMouseLeave={e => e.preventDefault()}
/>
<rect
pointerEvents="none"
x={right}
y={top}
width="1"
height={Math.max(0, bottom - top)}
fill="rgb(151,151,151)"
stroke="none"
onMouseLeave={e => e.preventDefault()}
/>
<line
pointerEvents="none"
x1={left}
x2={right}
stroke="rgb(151,151,151)"
strokeWidth={2}
onMouseLeave={e => e.preventDefault()}
/>
</g>
</g>
);
}
}
Highlight.displayName = 'HighlightOverlay';
Highlight.defaultProps = {
color: 'rgb(77, 182, 172)',
className: '',
enableX: true,
enableY: true,
highlightRef: null,
opacity: 0.3,
};
Highlight.propTypes = {
...AbstractSeries.propTypes,
enableX: PropTypes.bool,
enableY: PropTypes.bool,
highlightHeight: PropTypes.number,
highlightWidth: PropTypes.number,
highlightX: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
highlightY: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
highlightRef: PropTypes.func,
onBrushStart: PropTypes.func,
onDragStart: PropTypes.func,
onBrush: PropTypes.func,
onDrag: PropTypes.func,
onBrushEnd: PropTypes.func,
onDragEnd: PropTypes.func,
};
export default Highlight;

View File

@ -0,0 +1,63 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiPanel, EuiTitle } from '@elastic/eui';
const ContentPanel = ({
title = '',
titleSize = 'l',
bodyStyles = {},
panelStyles = {},
horizontalRuleClassName = '',
actions,
children,
}) => (
<EuiPanel style={{ paddingLeft: '0px', paddingRight: '0px', ...panelStyles }}>
<EuiFlexGroup style={{ padding: '0px 10px' }} justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem>
<EuiTitle size={titleSize}>
<h3>{title}</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
{Array.isArray(actions) ? (
actions.map((action, idx) => <EuiFlexItem key={idx}>{action}</EuiFlexItem>)
) : (
<EuiFlexItem>{actions}</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="xs" className={horizontalRuleClassName} />
<div style={{ padding: '0px 10px', ...bodyStyles }}>{children}</div>
</EuiPanel>
);
ContentPanel.propTypes = {
title: PropTypes.string,
titleSize: PropTypes.string,
bodyStyles: PropTypes.object,
panelStyles: PropTypes.object,
horizontalRuleClassName: PropTypes.string,
actions: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf([PropTypes.node])]),
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf([PropTypes.node])]).isRequired,
};
export default ContentPanel;

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import ContentPanel from './ContentPanel';
describe('ContentPanel', () => {
test('renders', () => {
const component = (
<ContentPanel title="Test Content Panel">
<div>Test</div>
</ContentPanel>
);
expect(render(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ContentPanel renders 1`] = `
<div
class="euiPanel euiPanel--paddingMedium"
style="padding-left:0px;padding-right:0px"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive"
style="padding:0px 10px"
>
<div
class="euiFlexItem"
>
<h3
class="euiTitle euiTitle--large"
>
Test Content Panel
</h3>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem"
/>
</div>
</div>
</div>
<hr
class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginXSmall"
/>
<div
style="padding:0px 10px"
>
<div>
Test
</div>
</div>
</div>
`;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import ContentPanel from './ContentPanel';
export default ContentPanel;

View File

@ -0,0 +1,75 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
class DelayedLoader extends React.Component {
constructor(props) {
super(props);
this.state = {
displayLoader: false,
};
if (typeof props.children !== 'function') {
throw new Error('Children should be function');
}
}
componentDidMount() {
if (this.props.isLoading) {
this.setTimer();
}
}
componentDidUpdate(prevProps) {
const { isLoading } = this.props;
// Setting up the loader to be visible only when network is too slow
if (isLoading !== prevProps.isLoading) {
if (isLoading) {
this.setTimer();
} else {
this.clearTimer();
this.setState({ displayLoader: false });
}
}
}
componentWillUnmount() {
this.clearTimer();
}
clearTimer = () => {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
};
setTimer = () => {
this.timer = setTimeout(this.handleDisplayLoader, 1000);
};
handleDisplayLoader = () => this.setState({ displayLoader: true });
render() {
const { displayLoader } = this.state;
return this.props.children(displayLoader);
}
}
DelayedLoader.propTypes = {
isLoading: PropTypes.bool.isRequired,
children: PropTypes.func.isRequired,
};
export default DelayedLoader;

View File

@ -0,0 +1,89 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render, mount } from 'enzyme';
import DelayedLoader from '../DelayedLoader';
describe('<DelayedLoader/>', () => {
beforeEach(() => {
jest.useFakeTimers();
});
test('renders', () => {
expect(
render(
<DelayedLoader isLoading={false}>
{showLoader => <div style={{ opacity: showLoader ? '0.2' : '1' }} />}
</DelayedLoader>
)
).toMatchSnapshot();
});
test('should set Timer for 1 seconds if initial loading is true', () => {
const wrapper = mount(
<DelayedLoader isLoading={true}>
{showLoader => <div style={{ opacity: showLoader ? '0.2' : '1' }} />}
</DelayedLoader>
);
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
expect(wrapper).toMatchSnapshot();
});
test('should clear Timer on componentWillUnmount if exists', () => {
const wrapper = mount(
<DelayedLoader isLoading={true}>
{showLoader => <div style={{ opacity: showLoader ? '0.2' : '1' }} />}
</DelayedLoader>
);
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
wrapper.unmount();
expect(clearTimeout).toHaveBeenCalledTimes(1);
});
test('should not show loader if data fetching is finished before threshold', () => {
const wrapper = mount(
<DelayedLoader isLoading={true}>
{showLoader => <div style={{ opacity: showLoader ? '0.2' : '1' }} />}
</DelayedLoader>
);
wrapper.setProps({ isLoading: false });
expect(clearTimeout).toHaveBeenCalledTimes(1);
expect(wrapper).toMatchSnapshot();
});
test('should show loader if data fetching takes more than threshold', () => {
const wrapper = mount(
<DelayedLoader isLoading={false}>
{showLoader => <div style={{ opacity: showLoader ? '0.2' : '1' }} />}
</DelayedLoader>
);
wrapper.setProps({ isLoading: true });
jest.runAllTimers();
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
test('should throw an error if children is not function', () => {
expect(() => {
render(
<DelayedLoader isLoading={false}>
<div />
</DelayedLoader>
);
}).toThrow('Children should be function');
});
});

View File

@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<DelayedLoader/> renders 1`] = `
<div
style="opacity:1"
/>
`;
exports[`<DelayedLoader/> should not show loader if data fetching is finished before threshold 1`] = `
<DelayedLoader
isLoading={false}
>
<div
style={
Object {
"opacity": "1",
}
}
/>
</DelayedLoader>
`;
exports[`<DelayedLoader/> should set Timer for 1 seconds if initial loading is true 1`] = `
<DelayedLoader
isLoading={true}
>
<div
style={
Object {
"opacity": "1",
}
}
/>
</DelayedLoader>
`;
exports[`<DelayedLoader/> should show loader if data fetching takes more than threshold 1`] = `
<DelayedLoader
isLoading={true}
>
<div
style={
Object {
"opacity": "0.2",
}
}
/>
</DelayedLoader>
`;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import DelayedLoader from './DelayedLoader';
export default DelayedLoader;

View File

@ -0,0 +1,62 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiFlyoutFooter } from '@elastic/eui';
import Flyouts from './flyouts';
const getFlyoutData = ({ type, payload }) => {
const flyout = Flyouts[type];
if (!flyout || typeof flyout !== 'function') return null;
return flyout(payload);
};
const propTypes = {
flyout: PropTypes.object,
onClose: PropTypes.func.isRequired,
};
const Flyout = ({ flyout, onClose }) => {
if (!flyout) return null;
const flyoutData = getFlyoutData(flyout);
if (!flyoutData) return null;
const {
flyoutProps = {},
headerProps = {},
bodyProps = {},
footerProps = {},
header = null,
body = null,
footer = null,
} = flyoutData;
const flyoutHeader = header && <EuiFlyoutHeader {...headerProps}>{header}</EuiFlyoutHeader>;
const flyoutBody = body && <EuiFlyoutBody {...bodyProps}>{body}</EuiFlyoutBody>;
const flyoutFooter = footer && <EuiFlyoutFooter {...footerProps}>{footer}</EuiFlyoutFooter>;
return (
<EuiFlyout onClose={onClose} {...flyoutProps}>
{flyoutHeader}
{flyoutBody}
{flyoutFooter}
</EuiFlyout>
);
};
Flyout.propTypes = propTypes;
export default Flyout;

View File

@ -0,0 +1,45 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import Flyout from './Flyout';
import Flyouts from './flyouts';
jest.unmock('./flyouts');
describe('Flyout', () => {
test('renders', () => {
const wrapper = shallow(
<Flyout flyout={{ type: 'message', payload: null }} onClose={jest.fn()} />
);
expect(wrapper).toMatchSnapshot();
});
test('renders null if no flyout', () => {
const wrapper = shallow(
<Flyout flyout={{ type: 'definitely no flyout', payload: null }} onClose={jest.fn()} />
);
expect(wrapper).toMatchSnapshot();
});
test('defaults if bad flyout data', () => {
Flyouts.message = jest.fn(() => ({}));
const wrapper = shallow(
<Flyout flyout={{ type: 'message', payload: null }} onClose={jest.fn()} />
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -0,0 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Flyout defaults if bad flyout data 1`] = `
<EuiFlyout
onClose={[MockFunction]}
/>
`;
exports[`Flyout renders 1`] = `
<EuiFlyout
aria-labelledby="messageFlyout"
maxWidth={500}
onClose={[MockFunction]}
size="m"
>
<EuiFlyoutHeader
hasBorder={true}
>
<EuiTitle
size="m"
style={
Object {
"fontSize": "25px",
}
}
>
<h2>
<strong>
Message
</strong>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiText
style={
Object {
"fontSize": "14px",
}
}
>
<p>
You have access to a "ctx" variable in your painless scripts and action mustache templates.
</p>
<h3>
Learn More
</h3>
<ul>
<li>
<EuiLink
href="https://mustache.github.io/mustache.5.html"
target="_blank"
>
HTML Templates with Mustache.js
</EuiLink>
</li>
</ul>
</EuiText>
</EuiFlyoutBody>
</EuiFlyout>
`;
exports[`Flyout renders null if no flyout 1`] = `""`;

View File

@ -0,0 +1,182 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Flyouts.message generates message JSON 1`] = `
Object {
"body": <EuiText
style={
Object {
"fontSize": "14px",
}
}
>
<p>
You have access to a "ctx" variable in your painless scripts and action mustache templates.
</p>
<h3>
Learn More
</h3>
<ul>
<li>
<EuiLink
href="https://mustache.github.io/mustache.5.html"
target="_blank"
>
HTML Templates with Mustache.js
</EuiLink>
</li>
</ul>
</EuiText>,
"flyoutProps": Object {
"aria-labelledby": "messageFlyout",
"maxWidth": 500,
"size": "m",
},
"header": <EuiTitle
size="m"
style={
Object {
"fontSize": "25px",
}
}
>
<h2>
<strong>
Message
</strong>
</h2>
</EuiTitle>,
"headerProps": Object {
"hasBorder": true,
},
}
`;
exports[`Flyouts.messageFrequency generates message JSON 1`] = `
Object {
"body": <EuiText
style={
Object {
"fontSize": "14px",
}
}
>
<p>
Specify message frequency to limit the number of notifications you receive within a given span of time. This setting is especially useful for low severity trigger conditions.
</p>
<p>
Consider the following example:
</p>
<ul>
<li>
A trigger condition is met.
</li>
<li>
The monitor sends a message
</li>
<li>
Message frequency is set to 10 minutes.
</li>
</ul>
<p>
For the next 10 minutes, even if a trigger condition is met dozens of times, the monitor sends no additional messages. If the trigger condition is met 11 minutes later, the monitor sends another message.
</p>
</EuiText>,
"flyoutProps": Object {
"aria-labelledby": "messageFrequencyFlyout",
"maxWidth": 500,
"size": "m",
},
"header": <EuiTitle
size="m"
style={
Object {
"fontSize": "25px",
}
}
>
<h2>
<strong>
Message frequency
</strong>
</h2>
</EuiTitle>,
"headerProps": Object {
"hasBorder": true,
},
}
`;
exports[`Flyouts.triggerCondition generates message JSON 1`] = `
Object {
"body": <div>
<EuiText
style={
Object {
"fontSize": "14px",
}
}
>
<p>
You have access to a "ctx" variable in your painless scripts
</p>
<p>
Below shows a quick JSON example of what's available under the "ctx" variable along with the actual results (where possible) for you to reference.
</p>
</EuiText>
<EuiSpacer
size="m"
/>
<EuiCodeBlock
language="json"
>
{
"monitor": "...",
"trigger": "...",
"results": "...",
"periodStart": "...",
"periodEnd": "...",
"alert": "...",
"error": "..."
}
</EuiCodeBlock>
<EuiSpacer
size="m"
/>
<EuiCodeEditor
height="700px"
mode="json"
readOnly={true}
setOptions={
Object {
"fontSize": "12px",
}
}
theme="github"
value="{}"
width="100%"
/>
</div>,
"flyoutProps": Object {
"aria-labelledby": "triggerConditionFlyout",
"maxWidth": 500,
"size": "m",
},
"header": <EuiTitle
size="m"
style={
Object {
"fontSize": "25px",
}
}
>
<h2>
<strong>
Trigger condition
</strong>
</h2>
</EuiTitle>,
"headerProps": Object {
"hasBorder": true,
},
}
`;

View File

@ -0,0 +1,38 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import Flyouts from './index';
describe('Flyouts.message', () => {
test('generates message JSON', () => {
const json = Flyouts.message();
expect(json).toMatchSnapshot();
});
});
describe('Flyouts.messageFrequency', () => {
test('generates message JSON', () => {
const json = Flyouts.messageFrequency();
expect(json).toMatchSnapshot();
});
});
describe('Flyouts.triggerCondition', () => {
test('generates message JSON', () => {
const json = Flyouts.triggerCondition({});
expect(json).toMatchSnapshot();
});
});

View File

@ -0,0 +1,26 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import message from './message';
import messageFrequency from './messageFrequency';
import triggerCondition from './triggerCondition';
const Flyouts = {
messageFrequency,
message,
triggerCondition,
};
export default Flyouts;

View File

@ -0,0 +1,51 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { EuiLink, EuiText, EuiTitle } from '@elastic/eui';
import { URL } from '../../../utils/constants';
const message = () => ({
flyoutProps: {
'aria-labelledby': 'messageFlyout',
maxWidth: 500,
size: 'm',
},
headerProps: { hasBorder: true },
header: (
<EuiTitle size="m" style={{ fontSize: '25px' }}>
<h2>
<strong>Message</strong>
</h2>
</EuiTitle>
),
body: (
<EuiText style={{ fontSize: '14px' }}>
<p>
{`You have access to a "ctx" variable in your painless scripts and action mustache templates.`}
</p>
<h3>Learn More</h3>
<ul>
<li>
<EuiLink target="_blank" href={URL.MUSTACHE}>
HTML Templates with Mustache.js
</EuiLink>
</li>
</ul>
</EuiText>
),
});
export default message;

View File

@ -0,0 +1,54 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { EuiText, EuiTitle } from '@elastic/eui';
const messageFrequency = () => ({
flyoutProps: {
'aria-labelledby': 'messageFrequencyFlyout',
maxWidth: 500,
size: 'm',
},
headerProps: { hasBorder: true },
header: (
<EuiTitle size="m" style={{ fontSize: '25px' }}>
<h2>
<strong>Message frequency</strong>
</h2>
</EuiTitle>
),
body: (
<EuiText style={{ fontSize: '14px' }}>
<p>
Specify message frequency to limit the number of notifications you receive within a given
span of time. This setting is especially useful for low severity trigger conditions.
</p>
<p>Consider the following example:</p>
<ul>
<li>A trigger condition is met.</li>
<li>The monitor sends a message</li>
<li>Message frequency is set to 10 minutes.</li>
</ul>
<p>
For the next 10 minutes, even if a trigger condition is met dozens of times, the monitor
sends no additional messages. If the trigger condition is met 11 minutes later, the monitor
sends another message.
</p>
</EuiText>
),
});
export default messageFrequency;

View File

@ -0,0 +1,76 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { EuiCodeBlock, EuiCodeEditor, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
const CONTEXT_VARIABLES = JSON.stringify(
{
monitor: '...',
trigger: '...',
results: '...',
periodStart: '...',
periodEnd: '...',
alert: '...',
error: '...',
},
null,
4
);
const triggerCondition = context => ({
flyoutProps: {
'aria-labelledby': 'triggerConditionFlyout',
maxWidth: 500,
size: 'm',
},
headerProps: { hasBorder: true },
header: (
<EuiTitle size="m" style={{ fontSize: '25px' }}>
<h2>
<strong>Trigger condition</strong>
</h2>
</EuiTitle>
),
body: (
<div>
<EuiText style={{ fontSize: '14px' }}>
<p>You have access to a "ctx" variable in your painless scripts</p>
<p>
Below shows a quick JSON example of what's available under the "ctx" variable along with
the actual results (where possible) for you to reference.
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiCodeBlock language="json">{CONTEXT_VARIABLES}</EuiCodeBlock>
<EuiSpacer size="m" />
<EuiCodeEditor
mode="json"
theme="github"
width="100%"
height="700px"
readOnly
value={JSON.stringify(context, null, 4)}
setOptions={{ fontSize: '12px' }}
/>
</div>
),
});
export default triggerCondition;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import Flyout from './Flyout';
export default Flyout;

View File

@ -0,0 +1,63 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiCheckbox } from '@elastic/eui';
import FormikInputWrapper from '../FormikInputWrapper';
import FormikFormRow from '../FormikFormRow';
const FormikCheckbox = ({
name,
formRow = false,
fieldProps = {},
rowProps = {},
inputProps = {},
}) => (
<FormikInputWrapper
name={name}
fieldProps={fieldProps}
render={({ field, form }) =>
formRow ? (
<FormikFormRow name={name} form={form} rowProps={rowProps}>
<FieldCheckbox name={name} form={form} field={field} inputProps={inputProps} />
</FormikFormRow>
) : (
<FieldCheckbox name={name} field={field} inputProps={inputProps} />
)
}
/>
);
const FieldCheckbox = ({ name, field: { value, ...rest }, inputProps }) => (
<EuiCheckbox name={name} id={name} checked={value} {...inputProps} {...rest} />
);
FormikCheckbox.propTypes = {
name: PropTypes.string.isRequired,
formRow: PropTypes.bool,
fieldProps: PropTypes.object,
rowProps: PropTypes.object,
inputProps: PropTypes.object,
};
FieldCheckbox.propTypes = {
name: PropTypes.string.isRequired,
inputProps: PropTypes.object.isRequired,
field: PropTypes.object.isRequired,
};
export default FormikCheckbox;

View File

@ -0,0 +1,42 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import { Formik } from 'formik';
import FormikCheckbox from './FormikCheckbox';
describe('FormikCheckbox', () => {
test('render formRow', () => {
const component = (
<Formik>
<FormikCheckbox name="testing" formRow />
</Formik>
);
expect(render(component)).toMatchSnapshot();
});
test('render', () => {
const component = (
<Formik>
<FormikCheckbox name="testing" />
</Formik>
);
expect(render(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormikCheckbox render 1`] = `
<div
class="euiCheckbox euiCheckbox--noLabel"
>
<input
class="euiCheckbox__input"
id="testing"
name="testing"
type="checkbox"
/>
<div
class="euiCheckbox__square"
/>
</div>
`;
exports[`FormikCheckbox render formRow 1`] = `
<div
class="euiFormRow"
id="testing-form-row-row"
>
<div
class="euiFormRow__fieldWrapper"
>
<div
class="euiCheckbox euiCheckbox--noLabel"
>
<input
class="euiCheckbox__input"
id="testing"
name="testing"
type="checkbox"
/>
<div
class="euiCheckbox__square"
/>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import FormikCheckbox from './FormikCheckbox';
export default FormikCheckbox;

View File

@ -0,0 +1,74 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiCodeEditor } from '@elastic/eui';
import FormikInputWrapper from '../FormikInputWrapper';
import FormikFormRow from '../FormikFormRow';
const FormikCodeEditor = ({
name,
formRow = false,
fieldProps = {},
rowProps = {},
inputProps = {},
}) => (
<FormikInputWrapper
name={name}
fieldProps={fieldProps}
render={({ field, form }) =>
formRow ? (
<FormikFormRow name={name} form={form} rowProps={rowProps}>
<CodeEditor name={name} form={form} field={field} inputProps={inputProps} />
</FormikFormRow>
) : (
<CodeEditor name={name} form={form} field={field} inputProps={inputProps} />
)
}
/>
);
const CodeEditor = ({ name, form, field, inputProps: { onBlur, onChange, ...rest } }) => (
<EuiCodeEditor
id={name}
onChange={string => {
onChange(string, field, form);
}}
onBlur={e => {
onBlur(e, field, form);
}}
value={field.value}
{...rest}
/>
);
FormikCodeEditor.propTypes = {
name: PropTypes.string.isRequired,
formRow: PropTypes.bool,
fieldProps: PropTypes.object,
rowProps: PropTypes.object,
inputProps: PropTypes.object,
};
CodeEditor.propTypes = {
name: PropTypes.string.isRequired,
inputProps: PropTypes.object.isRequired,
form: PropTypes.object.isRequired,
field: PropTypes.object.isRequired,
};
export default FormikCodeEditor;

View File

@ -0,0 +1,34 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import { Formik } from 'formik';
import FormikCodeEditor from './FormikCodeEditor';
// FIXME: This has an issue where EuiCodeEditor is generating a random HTML id and failing snapshot test
describe.skip('FormikCodeEditor', () => {
test('renders', () => {
const component = (
<Formik>
<FormikCodeEditor name="testing" />
</Formik>
);
expect(render(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import FormikCodeEditor from './FormikCodeEditor';
export default FormikCodeEditor;

View File

@ -0,0 +1,95 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiComboBox } from '@elastic/eui';
import FormikInputWrapper from '../FormikInputWrapper';
import FormikFormRow from '../FormikFormRow';
const FormikComboBox = ({
name,
formRow = false,
fieldProps = {},
rowProps = {},
inputProps = {},
}) => (
<FormikInputWrapper
name={name}
fieldProps={fieldProps}
render={({ field, form }) =>
formRow ? (
<FormikFormRow name={name} form={form} rowProps={rowProps}>
<ComboBox name={name} form={form} field={field} inputProps={inputProps} />
</FormikFormRow>
) : (
<ComboBox name={name} form={form} field={field} inputProps={inputProps} />
)
}
/>
);
const ComboBox = ({
name,
form,
field,
inputProps: { onBlur, onChange, onCreateOption, ...rest },
}) => (
<EuiComboBox
name={name}
id={name}
onChange={
typeof onChange === 'function'
? options => {
onChange(options, field, form);
}
: onChange
}
onCreateOption={
typeof onCreateOption === 'function'
? value => {
onCreateOption(value, field, form);
}
: onCreateOption
}
onBlur={
typeof onBlur === 'function'
? e => {
onBlur(e, field, form);
}
: onBlur
}
selectedOptions={field.value}
{...rest}
/>
);
FormikComboBox.propTypes = {
name: PropTypes.string.isRequired,
formRow: PropTypes.bool,
fieldProps: PropTypes.object,
rowProps: PropTypes.object,
inputProps: PropTypes.object,
};
ComboBox.propTypes = {
name: PropTypes.string.isRequired,
inputProps: PropTypes.object.isRequired,
form: PropTypes.object.isRequired,
field: PropTypes.object.isRequired,
};
export default FormikComboBox;

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import { Formik } from 'formik';
import FormikComboBox from './FormikComboBox';
describe.skip('FormikComboBox', () => {
test('renders', () => {
const component = (
<Formik>
<FormikComboBox name="testing" />
</Formik>
);
expect(render(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import FormikComboBox from './FormikComboBox';
export default FormikComboBox;

View File

@ -0,0 +1,69 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiFieldNumber } from '@elastic/eui';
import FormikInputWrapper from '../FormikInputWrapper';
import FormikFormRow from '../FormikFormRow';
const FormikFieldNumber = ({
name,
formRow = false,
fieldProps = {},
rowProps = {},
inputProps = {},
}) => (
<FormikInputWrapper
name={name}
fieldProps={fieldProps}
render={({ field, form }) =>
formRow ? (
<FormikFormRow name={name} form={form} rowProps={rowProps}>
<FieldNumber name={name} form={form} field={field} inputProps={inputProps} />
</FormikFormRow>
) : (
<FieldNumber name={name} form={form} field={field} inputProps={inputProps} />
)
}
/>
);
const FieldNumber = ({ name, form, field, inputProps: { onChange, isInvalid, ...rest } }) => (
<EuiFieldNumber
{...field}
{...rest}
onChange={e => (typeof onChange === 'function' ? onChange(e, field, form) : field.onChange(e))}
isInvalid={typeof isInvalid === 'function' ? isInvalid(name, form) : isInvalid}
/>
);
FormikFieldNumber.propTypes = {
name: PropTypes.string.isRequired,
formRow: PropTypes.bool,
fieldProps: PropTypes.object,
rowProps: PropTypes.object,
inputProps: PropTypes.object,
};
FieldNumber.propTypes = {
name: PropTypes.string.isRequired,
inputProps: PropTypes.object.isRequired,
form: PropTypes.object.isRequired,
field: PropTypes.object.isRequired,
};
export default FormikFieldNumber;

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import { Formik } from 'formik';
import FormikFieldNumber from './FormikFieldNumber';
describe('FormikFieldNumber', () => {
test('renders', () => {
const component = (
<Formik>
<FormikFieldNumber name="testing" />
</Formik>
);
expect(render(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormikFieldNumber renders 1`] = `
<div
class="euiFormControlLayout"
>
<div
class="euiFormControlLayout__childrenWrapper"
>
<input
class="euiFieldNumber"
name="testing"
type="number"
/>
</div>
</div>
`;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import FormikFieldNumber from './FormikFieldNumber';
export default FormikFieldNumber;

View File

@ -0,0 +1,75 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiFieldPassword } from '@elastic/eui';
import FormikInputWrapper from '../FormikInputWrapper';
import FormikFormRow from '../FormikFormRow';
const FormikFieldPassword = ({
name,
formRow = false,
fieldProps = {},
rowProps = {},
inputProps = {},
}) => (
<FormikInputWrapper
name={name}
fieldProps={fieldProps}
render={({ field, form }) =>
formRow ? (
<FormikFormRow name={name} form={form} rowProps={rowProps}>
<FieldPassword name={name} form={form} field={field} inputProps={inputProps} />
</FormikFormRow>
) : (
<FieldPassword name={name} form={form} field={field} inputProps={inputProps} />
)
}
/>
);
const FieldPassword = ({
name,
form,
field,
inputProps: { onChange, isInvalid, onFocus, ...rest },
}) => (
<EuiFieldPassword
{...field}
{...rest}
onChange={e => (typeof onChange === 'function' ? onChange(e, field, form) : field.onChange(e))}
onFocus={typeof onFocus === 'function' ? e => onFocus(e, field, form) : onFocus}
isInvalid={typeof isInvalid === 'function' ? isInvalid(name, form) : isInvalid}
/>
);
FormikFieldPassword.propTypes = {
name: PropTypes.string.isRequired,
formRow: PropTypes.bool,
fieldProps: PropTypes.object,
rowProps: PropTypes.object,
inputProps: PropTypes.object,
};
FieldPassword.propTypes = {
name: PropTypes.string.isRequired,
inputProps: PropTypes.object.isRequired,
form: PropTypes.object.isRequired,
field: PropTypes.object.isRequired,
};
export default FormikFieldPassword;

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import { Formik } from 'formik';
import FormikFieldPassword from './FormikFieldPassword';
describe('FormikFieldPassword', () => {
test('renders', () => {
const component = (
<Formik>
<FormikFieldPassword name="testing" />
</Formik>
);
expect(render(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormikFieldPassword renders 1`] = `
<div
class="euiFormControlLayout"
>
<div
class="euiFormControlLayout__childrenWrapper"
>
<input
class="euiFieldPassword"
name="testing"
type="password"
/>
<div
class="euiFormControlLayoutIcons"
>
<span
class="euiFormControlLayoutCustomIcon"
>
<div>
EuiIconMock
</div>
</span>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import FormikFieldPassword from './FormikFieldPassword';
export default FormikFieldPassword;

View File

@ -0,0 +1,68 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiRadio } from '@elastic/eui';
import FormikInputWrapper from '../FormikInputWrapper';
import FormikFormRow from '../FormikFormRow';
const FormikFieldRadio = ({
name,
formRow = false,
fieldProps = {},
rowProps = {},
inputProps = {},
}) => (
<FormikInputWrapper
name={name}
fieldProps={fieldProps}
render={({ field, form }) =>
formRow ? (
<FormikFormRow name={name} form={form} rowProps={rowProps}>
<FieldRadio name={name} form={form} field={field} inputProps={inputProps} />
</FormikFormRow>
) : (
<FieldRadio name={name} form={form} field={field} inputProps={inputProps} />
)
}
/>
);
const FieldRadio = ({ name, form, field, inputProps: { onChange, ...rest } }) => (
<EuiRadio
{...field}
{...rest}
onChange={e => (typeof onChange === 'function' ? onChange(e, field, form) : field.onChange(e))}
/>
);
FormikFieldRadio.propTypes = {
name: PropTypes.string.isRequired,
formRow: PropTypes.bool,
fieldProps: PropTypes.object,
rowProps: PropTypes.object,
inputProps: PropTypes.object,
};
FieldRadio.propTypes = {
name: PropTypes.string.isRequired,
inputProps: PropTypes.object.isRequired,
form: PropTypes.object.isRequired,
field: PropTypes.object.isRequired,
};
export default FormikFieldRadio;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import FormikFieldRadio from './FormikFieldRadio';
export default FormikFieldRadio;

View File

@ -0,0 +1,75 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiFieldText } from '@elastic/eui';
import FormikInputWrapper from '../FormikInputWrapper';
import FormikFormRow from '../FormikFormRow';
const FormikFieldText = ({
name,
formRow = false,
fieldProps = {},
rowProps = {},
inputProps = {},
}) => (
<FormikInputWrapper
name={name}
fieldProps={fieldProps}
render={({ field, form }) =>
formRow ? (
<FormikFormRow name={name} form={form} rowProps={rowProps}>
<FieldText name={name} form={form} field={field} inputProps={inputProps} />
</FormikFormRow>
) : (
<FieldText name={name} form={form} field={field} inputProps={inputProps} />
)
}
/>
);
const FieldText = ({
name,
form,
field,
inputProps: { onChange, isInvalid, onFocus, ...rest },
}) => (
<EuiFieldText
{...field}
{...rest}
onChange={e => (typeof onChange === 'function' ? onChange(e, field, form) : field.onChange(e))}
onFocus={typeof onFocus === 'function' ? e => onFocus(e, field, form) : onFocus}
isInvalid={typeof isInvalid === 'function' ? isInvalid(name, form) : isInvalid}
/>
);
FormikFieldText.propTypes = {
name: PropTypes.string.isRequired,
formRow: PropTypes.bool,
fieldProps: PropTypes.object,
rowProps: PropTypes.object,
inputProps: PropTypes.object,
};
FieldText.propTypes = {
name: PropTypes.string.isRequired,
inputProps: PropTypes.object.isRequired,
form: PropTypes.object.isRequired,
field: PropTypes.object.isRequired,
};
export default FormikFieldText;

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import { Formik } from 'formik';
import FormikFieldText from './FormikFieldText';
describe('FormikFieldText', () => {
test('renders', () => {
const component = (
<Formik>
<FormikFieldText name="testing" />
</Formik>
);
expect(render(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormikFieldText renders 1`] = `
<div
class="euiFormControlLayout"
>
<div
class="euiFormControlLayout__childrenWrapper"
>
<input
class="euiFieldText"
name="testing"
type="text"
/>
</div>
</div>
`;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import FormikFieldText from './FormikFieldText';
export default FormikFieldText;

View File

@ -0,0 +1,38 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiFormRow } from '@elastic/eui';
const FormikFormRow = ({ children, form, name, rowProps: { isInvalid, error, ...rest } }) => (
<EuiFormRow
id={`${name}-form-row`}
isInvalid={typeof isInvalid === 'function' ? isInvalid(name, form) : isInvalid}
error={typeof error === 'function' ? error(name, form) : error}
{...rest}
>
{children}
</EuiFormRow>
);
FormikFormRow.propTypes = {
name: PropTypes.string.isRequired,
rowProps: PropTypes.object.isRequired,
form: PropTypes.object.isRequired,
children: PropTypes.node.isRequired,
};
export default FormikFormRow;

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import FormikFormRow from './FormikFormRow';
describe('FormikFormRow', () => {
const Child = () => <div id="mine">child</div>;
test('renders', () => {
const component = (
<FormikFormRow name="testing" rowProps={{}} form={{}}>
<Child />
</FormikFormRow>
);
expect(render(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormikFormRow renders 1`] = `
<div
class="euiFormRow"
id="testing-form-row-row"
>
<div
class="euiFormRow__fieldWrapper"
>
<div
id="mine"
>
child
</div>
</div>
</div>
`;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import FormikFormRow from './FormikFormRow';
export default FormikFormRow;

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { Field } from 'formik';
const FormikInputWrapper = ({ name, fieldProps, render }) => (
<Field name={name} {...fieldProps}>
{({ field, form }) => render({ field, form })}
</Field>
);
FormikInputWrapper.propTypes = {
name: PropTypes.string.isRequired,
fieldProps: PropTypes.object.isRequired,
render: PropTypes.func.isRequired,
};
export default FormikInputWrapper;

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import { Formik } from 'formik';
import FormikInputWrapper from './FormikInputWrapper';
describe('FormikInputWrapper', () => {
test('renders', () => {
const component = (
<Formik>
<FormikInputWrapper name="testing" fieldProps={{}} render={() => <div>test</div>} />
</Formik>
);
expect(render(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormikInputWrapper renders 1`] = `
<div>
test
</div>
`;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import FormikInputWrapper from './FormikInputWrapper';
export default FormikInputWrapper;

View File

@ -0,0 +1,71 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiSelect } from '@elastic/eui';
import FormikInputWrapper from '../FormikInputWrapper';
import FormikFormRow from '../FormikFormRow';
const FormikSelect = ({
name,
formRow = false,
fieldProps = {},
rowProps = {},
inputProps = {},
}) => (
<FormikInputWrapper
name={name}
fieldProps={fieldProps}
render={({ field, form }) =>
formRow ? (
<FormikFormRow name={name} form={form} rowProps={rowProps}>
<FieldSelect name={name} form={form} field={field} inputProps={inputProps} />
</FormikFormRow>
) : (
<FieldSelect name={name} form={form} field={field} inputProps={inputProps} />
)
}
/>
);
const FieldSelect = ({ name, field, form, inputProps: { onChange, isInvalid, ...rest } }) => (
<EuiSelect
name={name}
id={name}
{...field}
{...rest}
onChange={e => (typeof onChange === 'function' ? onChange(e, field, form) : field.onChange(e))}
isInvalid={typeof isInvalid === 'function' ? isInvalid(name, form) : isInvalid}
/>
);
FormikSelect.propTypes = {
name: PropTypes.string.isRequired,
formRow: PropTypes.bool,
fieldProps: PropTypes.object,
rowProps: PropTypes.object,
inputProps: PropTypes.object,
};
FieldSelect.propTypes = {
name: PropTypes.string.isRequired,
form: PropTypes.object.isRequired,
inputProps: PropTypes.object.isRequired,
field: PropTypes.object.isRequired,
};
export default FormikSelect;

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import { Formik } from 'formik';
import FormikSelect from './FormikSelect';
describe('FormikSelect', () => {
test('renders', () => {
const component = (
<Formik>
<FormikSelect name="testing" inputProps={{ options: [{ value: 'test', text: 'test' }] }} />
</Formik>
);
expect(render(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormikSelect renders 1`] = `
<div
class="euiFormControlLayout"
>
<div
class="euiFormControlLayout__childrenWrapper"
>
<select
class="euiSelect"
id="testing"
name="testing"
>
<option
value="test"
>
test
</option>
</select>
<div
class="euiFormControlLayoutIcons euiFormControlLayoutIcons--right"
>
<span
class="euiFormControlLayoutCustomIcon"
>
<div>
EuiIconMock
</div>
</span>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import FormikSelect from './FormikSelect';
export default FormikSelect;

View File

@ -0,0 +1,63 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiSwitch } from '@elastic/eui';
import FormikInputWrapper from '../FormikInputWrapper';
import FormikFormRow from '../FormikFormRow';
const FormikSwitch = ({
name,
formRow = false,
fieldProps = {},
rowProps = {},
inputProps = {},
}) => (
<FormikInputWrapper
name={name}
fieldProps={fieldProps}
render={({ field, form }) =>
formRow ? (
<FormikFormRow name={name} form={form} rowProps={rowProps}>
<FieldSwitch name={name} form={form} field={field} inputProps={inputProps} />
</FormikFormRow>
) : (
<FieldSwitch name={name} field={field} inputProps={inputProps} />
)
}
/>
);
const FieldSwitch = ({ name, field: { value, ...rest }, inputProps }) => (
<EuiSwitch name={name} id={name} checked={value} {...inputProps} {...rest} />
);
FormikSwitch.propTypes = {
name: PropTypes.string.isRequired,
formRow: PropTypes.bool,
fieldProps: PropTypes.object,
rowProps: PropTypes.object,
inputProps: PropTypes.object,
};
FieldSwitch.propTypes = {
name: PropTypes.string.isRequired,
inputProps: PropTypes.object.isRequired,
field: PropTypes.object.isRequired,
};
export default FormikSwitch;

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import { Formik } from 'formik';
import FormikSwitch from './FormikSwitch';
describe('FormikSwitch', () => {
test('renders', () => {
const component = (
<Formik>
<FormikSwitch name="testing" />
</Formik>
);
expect(render(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormikSwitch renders 1`] = `
<div
class="euiSwitch"
>
<button
aria-checked="false"
aria-labelledby="generated-id"
class="euiSwitch__button"
id="testing"
name="testing"
role="switch"
type="button"
>
<span
class="euiSwitch__body"
>
<span
class="euiSwitch__thumb"
/>
<span
class="euiSwitch__track"
>
<div>
EuiIconMock
</div>
<div>
EuiIconMock
</div>
</span>
</span>
</button>
<span
class="euiSwitch__label"
id="generated-id"
/>
</div>
`;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import FormikSwitch from './FormikSwitch';
export default FormikSwitch;

View File

@ -0,0 +1,67 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiTextArea } from '@elastic/eui';
import FormikInputWrapper from '../FormikInputWrapper';
import FormikFormRow from '../FormikFormRow';
const FormikTextArea = ({
name,
formRow = false,
fieldProps = {},
rowProps = {},
inputProps = {},
}) => (
<FormikInputWrapper
name={name}
fieldProps={fieldProps}
render={({ field, form }) =>
formRow ? (
<FormikFormRow name={name} form={form} rowProps={rowProps}>
<TextArea name={name} form={form} field={field} inputProps={inputProps} />
</FormikFormRow>
) : (
<TextArea name={name} form={form} field={field} inputProps={inputProps} />
)
}
/>
);
const TextArea = ({ name, form, field, inputProps: { isInvalid, ...rest } }) => (
<EuiTextArea
isInvalid={typeof isInvalid === 'function' ? isInvalid(name, form) : isInvalid}
{...field}
{...rest}
/>
);
FormikTextArea.propTypes = {
name: PropTypes.string.isRequired,
formRow: PropTypes.bool,
fieldProps: PropTypes.object,
rowProps: PropTypes.object,
inputProps: PropTypes.object,
};
TextArea.propTypes = {
name: PropTypes.string.isRequired,
inputProps: PropTypes.object.isRequired,
form: PropTypes.object.isRequired,
field: PropTypes.object.isRequired,
};
export default FormikTextArea;

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import { Formik } from 'formik';
import FormikTextArea from './FormikTextArea';
describe('FormikTextArea', () => {
test('renders', () => {
const component = (
<Formik>
<FormikTextArea name="testing" />
</Formik>
);
expect(render(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormikTextArea renders 1`] = `
<textarea
class="euiTextArea euiTextArea--resizeVertical"
name="testing"
rows="6"
/>
`;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import FormikTextArea from './FormikTextArea';
export default FormikTextArea;

View File

@ -0,0 +1,42 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import FormikFieldText from './FormikFieldText';
import FormikFormRow from './FormikFormRow';
import FormikInputWrapper from './FormikInputWrapper';
import FormikTextArea from './FormikTextArea';
import FormikSwitch from './FormikSwitch';
import FormikCheckbox from './FormikCheckbox';
import FormikSelect from './FormikSelect';
import FormikFieldNumber from './FormikFieldNumber';
import FormikCodeEditor from './FormikCodeEditor';
import FormikComboBox from './FormikComboBox';
import FormikFieldPassword from './FormikFieldPassword';
import FormikFieldRadio from './FormikFieldRadio';
export {
FormikComboBox,
FormikSwitch,
FormikCheckbox,
FormikSelect,
FormikFieldNumber,
FormikFieldText,
FormikFormRow,
FormikInputWrapper,
FormikTextArea,
FormikCodeEditor,
FormikFieldPassword,
FormikFieldRadio,
};

View File

@ -0,0 +1,43 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { EuiHorizontalRule, EuiText, EuiTitle } from '@elastic/eui';
const DEFAULT_PROPS = { size: 'xs', style: { paddingLeft: '10px' } };
const SubHeader = ({
description,
descriptionProps = DEFAULT_PROPS,
horizontalRuleMargin = 'xs',
title,
titleProps = DEFAULT_PROPS,
}) => (
<Fragment>
<EuiTitle {...titleProps}>{title}</EuiTitle>
<EuiHorizontalRule margin={horizontalRuleMargin} />
<EuiText {...descriptionProps}>{description}</EuiText>
</Fragment>
);
SubHeader.propTypes = {
description: PropTypes.node.isRequired,
descriptionProps: PropTypes.object,
horizontalRuleMargin: PropTypes.string,
title: PropTypes.node.isRequired,
titleProps: PropTypes.object,
};
export default SubHeader;

View File

@ -0,0 +1,26 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import SubHeader from './SubHeader';
describe('SubHeader', () => {
test('renders', () => {
const component = <SubHeader description={<div>description</div>} title={<div>title</div>} />;
expect(render(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SubHeader renders 1`] = `
Array [
<div
class="euiTitle euiTitle--xsmall"
style="padding-left:10px"
>
title
</div>,
<hr
class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginXSmall"
/>,
<div
class="euiText euiText--extraSmall"
style="padding-left:10px"
>
<div>
description
</div>
</div>,
]
`;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import SubHeader from './SubHeader';
export default SubHeader;

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 57.1 (83088) - https://sketch.com -->
<title>Alerting</title>
<desc>Created with Sketch.</desc>
<g id="Alerting" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-2" transform="translate(4.000000, 0.500000)">
<g id="Group-13">
<path d="M9.49121094,20.5 C9.49121094,19.1192881 8.37192281,18 6.99121094,18 C5.61049906,18 4.49121094,19.1192881 4.49121094,20.5" id="Oval-Copy-80" stroke="#007D73" transform="translate(6.991211, 19.250000) scale(1, -1) translate(-6.991211, -19.250000) "></path>
<path d="M12.9912109,9.1 C12.9912109,6.0072054 10.3049194,3.5 6.99121094,3.5 C3.67750244,3.5 0.991210938,6.0072054 0.991210938,9.1 C0.991210938,12.8807292 0.660807292,15.6807292 0,17.5 L13.9875,17.5 C12.9912109,14.7215104 12.9912109,12.1927946 12.9912109,9.1 Z" id="Oval" stroke="#333741"></path>
<circle id="Oval-Copy-67" stroke="#333741" cx="6.99121094" cy="2" r="1.5"></circle>
</g>
<path d="M2.5,14.5 L11.5,14.5" id="Line-9" stroke="#007D73" stroke-linecap="square"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,61 @@
import Main from './pages/Main';
import { CoreContext } from './utils/CoreContext';
import {Fetch} from '../../components/kibana/core/public/http/fetch';
import {Router} from 'react-router-dom';
import {useMemo} from 'react';
import {ScopedHistory} from '../../components/kibana/core/public/application/scoped_history';
import {notification} from 'antd';
import {connect} from 'dva'
const httpClient = new Fetch({
basePath:{
get: () => '',
prepend: (url) => url,
remove: (url) => url,
serverBasePath: '/elasticsearch',
}
});
const notifications = {
toasts: {
addDanger: ({title, text, toastLifeTimeMs})=>{
notification.warning({
message: title,
description: text,
duration: toastLifeTimeMs/1000,
})
}
}
}
const AlertingUI = (props)=>{
if(!props.selectedCluster.id){
return null;
}
useMemo(()=>{
httpClient.getServerBasePath = ()=>{
return '/api/elasticsearch/'+ props.selectedCluster.id;
}
}, [props.selectedCluster]);
const isDarkMode = false;
const history = useMemo(()=>{
return new ScopedHistory(props.history, '/alerting');
}, [props.history])
return (
<CoreContext.Provider
value={{ http: httpClient, isDarkMode, notifications: notifications }}
>
<Router history={history}>
<div style={{background:'#fff'}}>
<Main title="Alerting" {...props} />
</div>
</Router>
</CoreContext.Provider>
)
}
export default connect(({
global
})=>({
selectedCluster: global.selectedCluster,
}))(AlertingUI)

View File

@ -0,0 +1,86 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
.container {
margin-top: 30px;
}
.accordion-horizontal-rule {
margin-bottom: 0px;
}
.accordion-action {
padding: 10px 0px;
margin-bottom: 10px;
.euiAccordion__button {
padding-left: 10px;
}
}
.accordion-action:last-child {
margin-bottom: 0px;
}
.accordion-separator {
background-image: linear-gradient(#D3DAE6, #D3DAE6);
background-size: 100% 1px;
background-repeat: no-repeat;
background-position: center center;
}
.modal-manage-email {
min-width: 800px;
height: 75vh;
max-height: 1000px;
}
.euiTextArea.read-only-text-area {
padding-left: 12px;
border-color: initial;
box-shadow: 0 1px 1px -1px rgba(153, 153, 153, 0.2), 0 3px 2px -2px rgba(153, 153, 153, 0.2),
inset 0 0 0 1px rgba(0, 0, 0, 0.1);
}
/*
* Kibana itself has an old version of EUI stylesheets that is being applied to our upgraded EUI library.
* Our own EUI dependency is overwriting the Kibana EUI dependency stylesheets, but any CSS properties that aren't
* included in the local dependency aren't overwritten, so some extra styles are sometimes applied such as this case:
* .euiSwitch .euiSwitch__thumb has a width/height set at 24px in older version which breaks the styling of the newer version.
* */
.euiFormRow.euiFormRow--hasEmptyLabelSpace {
padding-top: initial;
}
.euiModal--confirmation {
width: initial;
}
.euiModal {
padding: initial;
}
.euiRangePicker--popper {
left: unset !important;
right: 0;
}
.euiDatePickerRange__icon {
flex: 0 !important;
}
.euiPaginationButton-isActive:focus {
color: #0079a5 !important;
}

View File

@ -0,0 +1,191 @@
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiText, EuiSpacer } from '@elastic/eui';
import {
Chart,
Axis,
LineSeries,
niceTimeFormatter,
Settings,
Position,
LineAnnotation,
} from '@elastic/charts';
import { ChartContainer } from '../../../../../components/ChartContainer/ChartContainer';
import DelayedLoader from '../../../../../components/DelayedLoader';
export const MAX_DATA_POINTS = 1000;
const getAxisTitle = (displayGrade, displayConfidence) => {
if (displayGrade && displayConfidence) {
return 'Anomaly grade / confidence';
}
return displayGrade ? 'Anomaly grade' : 'Anomaly confidence';
};
/**
* In case of too many anomalies (e.g., high-cardinality detectors), we only keep the max anomalies within
* allowed range. Otherwise, return data as they are.
* @param {any[]} data The original anomaly result from preview
* @returns {any[]} anomalies within allowed range
*/
export const prepareDataForChart = (data) => {
if (data && data.length > MAX_DATA_POINTS) {
return sampleMaxAnomalyGrade(data);
} else {
return data;
}
};
/**
* Caclulate the stride between each step
* @param {number} total Total number of preview results
* @returns {number} The stride
*/
const calculateStep = (total) => {
return Math.ceil(total / MAX_DATA_POINTS);
};
/**
* Pick the elememtn with the max anomaly grade within the input array
* @param {any[]} anomalies Input array with preview results
* @returns The elememtn with the max anomaly grade
*/
const findAnomalyWithMaxAnomalyGrade = (anomalies) => {
let anomalyWithMaxGrade = anomalies[0];
for (let i = 1; i < anomalies.length; i++) {
if (anomalies[i].anomalyGrade > anomalyWithMaxGrade.anomalyGrade) {
anomalyWithMaxGrade = anomalies[i];
}
}
return anomalyWithMaxGrade;
};
/**
* Sample max anomalies within allowed range
* @param {any[]} data The original results from preview
* @return {any[]} sampled anomalies
*/
const sampleMaxAnomalyGrade = (data) => {
let sortedData = data.sort((a, b) => (a.plotTime > b.plotTime ? 1 : -1));
const step = calculateStep(sortedData.length);
let index = 0;
const sampledAnomalies = [];
while (index < sortedData.length) {
const arr = sortedData.slice(index, index + step);
sampledAnomalies.push(findAnomalyWithMaxAnomalyGrade(arr));
index = index + step;
}
return sampledAnomalies;
};
const AnomaliesChart = (props) => {
const timeFormatter = niceTimeFormatter([props.startDateTime, props.endDateTime]);
const preparedAnomalies = prepareDataForChart(props.anomalies);
return (
<DelayedLoader isLoading={props.isLoading}>
{(showLoader) => (
<React.Fragment>
{props.showTitle ? (
<EuiText size="xs">
<strong>{props.title}</strong>
</EuiText>
) : null}
<EuiSpacer size="s" />
<div>
<ChartContainer
style={{ height: '300px', width: '100%', opacity: showLoader ? 0.2 : 1 }}
>
<Chart>
{props.showSettings ? (
<Settings
showLegend
legendPosition={Position.Bottom}
showLegendDisplayValue={false}
/>
) : null}
<Axis id="bottom" position="bottom" tickFormat={timeFormatter} />
<Axis
id="left"
title={getAxisTitle(props.displayGrade, props.displayConfidence)}
position="left"
domain={{ min: 0, max: 1 }}
/>
{props.annotationData ? (
<LineAnnotation
annotationId="anomalyAnnotation"
domainType="yDomain"
dataValues={props.annotationData}
style={{
line: {
strokeWidth: 1.5,
stroke: '#f00',
},
}}
/>
) : null}
{props.displayGrade ? (
<LineSeries
id="Anomaly grade"
xScaleType="time"
yScaleType="linear"
xAccessor={'plotTime'}
yAccessors={['anomalyGrade']}
data={preparedAnomalies}
/>
) : null}
{props.displayConfidence ? (
<LineSeries
id="Confidence"
xScaleType="time"
yScaleType="linear"
xAccessor={'plotTime'}
yAccessors={['confidence']}
data={preparedAnomalies}
/>
) : null}
</Chart>
</ChartContainer>
</div>
</React.Fragment>
)}
</DelayedLoader>
);
};
AnomaliesChart.propTypes = {
startDateTime: PropTypes.number,
endDateTime: PropTypes.number,
endDate: PropTypes.number,
isLoading: PropTypes.bool,
showTitle: PropTypes.bool,
showSettings: PropTypes.bool,
annotationData: PropTypes.array,
anomalies: PropTypes.array.isRequired,
title: PropTypes.string,
};
AnomaliesChart.defaultProps = {
isLoading: false,
showTitle: true,
showSettings: true,
anomalies: undefined,
title: 'Sample preview for anomaly score',
};
export { AnomaliesChart };

View File

@ -0,0 +1,148 @@
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import moment from 'moment';
import { AnomaliesChart, MAX_DATA_POINTS, prepareDataForChart } from './AnomaliesChart';
const startTime = moment('2018-10-25T09:30:00').valueOf();
const endTime = moment('2018-10-29T09:30:00').valueOf();
const getRandomArbitrary = (min, max) => {
return Math.floor(Math.random() * (max - min) + min);
};
/**
* Generate preview results
* @param {Number} startTime Preview start time in epoch milliseconds
* @param {Number} endTime Preview end time in epoch milliseconds
* @param {Number} count Number of results
* @returns {any[]} Generated results
*/
const createTestData = (startTime, endTime, count) => {
const data = [];
const interval = 60000;
const midInterval = interval / 2;
for (let i = 0; i < count - 3; i++) {
let startGenerated = getRandomArbitrary(startTime, endTime);
data.push({
anomalyGrade: 0,
confidence: 0,
dataEndTime: startGenerated + interval,
dataStartTime: startGenerated,
detectorId: 'nxEuT3YBdrEXnzbxJ7XZ',
plotTime: startGenerated + midInterval,
schemaVersion: 0,
});
}
// injected 3 anomalies: the beginning, the end, and the middle.
data.push({
anomalyGrade: 1,
confidence: 0.7,
dataEndTime: startTime + interval,
dataStartTime: startTime,
detectorId: 'nxEuT3YBdrEXnzbxJ7XZ',
plotTime: startTime + midInterval,
schemaVersion: 0,
});
data.push({
anomalyGrade: 0.9,
confidence: 0.8,
dataEndTime: endTime,
dataStartTime: endTime - interval,
detectorId: 'nxEuT3YBdrEXnzbxJ7XZ',
plotTime: endTime - interval + midInterval,
schemaVersion: 0,
});
let mid = startTime + (endTime - startTime) / 2;
data.push({
anomalyGrade: 0.7,
confidence: 0.9,
dataEndTime: mid + interval,
dataStartTime: mid,
detectorId: 'nxEuT3YBdrEXnzbxJ7XZ',
plotTime: mid + midInterval,
schemaVersion: 0,
});
return data;
};
describe('AnomaliesChart', () => {
test('renders ', () => {
const sampleData = [
{
anomalyGrade: 0.9983687181753063,
anomalyScore: 0.8381447468893426,
confidence: 0.42865659282252266,
detectorId: 'temp',
endTime: 1569097677667,
plotTime: 1569097377667,
startTime: 1569097077667,
},
];
const component = (
<AnomaliesChart
anomalies={sampleData}
startDateTime={startTime}
endDateTime={endTime}
isLoading={false}
displayGrade
displayConfidence
showTitle
/>
);
expect(render(component)).toMatchSnapshot();
});
test('hc detector trigger definition', () => {
let startTime = 1608327992253;
let endTime = 1608759992253;
const preparedAnomalies = prepareDataForChart(
createTestData(startTime, endTime, MAX_DATA_POINTS * 30)
);
expect(preparedAnomalies.length).toBeCloseTo(MAX_DATA_POINTS);
expect(preparedAnomalies[MAX_DATA_POINTS - 1].anomalyGrade).toBeCloseTo(0.9);
expect(preparedAnomalies[MAX_DATA_POINTS - 1].confidence).toBeCloseTo(0.8);
var anomalyNumber = 0;
for (let i = 0; i < MAX_DATA_POINTS; i++) {
if (preparedAnomalies[i].anomalyGrade > 0) {
anomalyNumber++;
// we injected an anomaly in the middle. Due to randomness, we cannot predict which one it is.
if (i > 0 && i < MAX_DATA_POINTS - 1) {
expect(preparedAnomalies[i].anomalyGrade).toBeCloseTo(0.7);
expect(preparedAnomalies[i].confidence).toBeCloseTo(0.9);
}
}
}
// injected 3 anomalies
expect(anomalyNumber).toBe(3);
});
test('single-stream detector trigger definition', () => {
let startTime = 1608327992253;
let endTime = 1608759992253;
let originalPreviewResults = createTestData(startTime, endTime, MAX_DATA_POINTS);
// we only consolidate and reduce original data when the input data size is larger than MAX_DATA_POINTS
expect(prepareDataForChart(originalPreviewResults)).toBe(originalPreviewResults);
});
});

View File

@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnomaliesChart renders 1`] = `
Array [
<div
class="euiText euiText--extraSmall"
>
<strong>
Sample preview for anomaly score
</strong>
</div>,
<div
class="euiSpacer euiSpacer--s"
/>,
<div>
<div
style="border-radius:5px;padding:10px;border:1px solid #D9D9D9;height:300px;width:100%;opacity:1"
>
<div
class="echChart"
>
<div
class="echChartBackground"
style="background-color:transparent"
/>
<div
class="echChartStatus"
data-ech-render-complete="false"
data-ech-render-count="0"
/>
<div
class="echChartResizer"
/>
<div
class="echContainer"
/>
</div>
</div>
</div>,
]
`;

View File

@ -0,0 +1,16 @@
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
export { AnomaliesChart } from './AnomaliesChart';

View File

@ -0,0 +1,67 @@
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiEmptyPrompt, EuiButton, EuiText, EuiLoadingChart } from '@elastic/eui';
import { KIBANA_AD_PLUGIN } from '../../../../../utils/constants';
const EmptyFeaturesMessage = props => (
<div
style={{
borderRadius: '5px',
padding: '10px',
border: '1px solid #D9D9D9',
height: '250px',
width: '100%',
...props.containerStyle,
}}
>
{props.isLoading ? (
<EuiEmptyPrompt style={{ maxWidth: '45em' }} body={<EuiLoadingChart size="xl" />} />
) : (
<EuiEmptyPrompt
style={{ maxWidth: '45em' }}
body={
<EuiText>
No features have been added to this anomaly detector. A feature is a metric that is used
for anomaly detection. A detector can discover anomalies across one or more features.
</EuiText>
}
actions={[
<EuiButton
data-test-subj="createButton"
href={`${KIBANA_AD_PLUGIN}#/detectors/${props.detectorId}/features`}
target="_blank"
>
Add Feature
</EuiButton>,
]}
/>
)}
</div>
);
EmptyFeaturesMessage.propTypes = {
detectorId: PropTypes.string,
isLoading: PropTypes.bool.isRequired,
containerStyle: PropTypes.object,
};
EmptyFeaturesMessage.defaultProps = {
detectorId: '',
containerStyle: {},
};
export { EmptyFeaturesMessage };

View File

@ -0,0 +1,25 @@
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import { EmptyFeaturesMessage } from './EmptyFeaturesMessage';
describe('EmptyFeaturesMessage', () => {
test('renders ', () => {
const component = <EmptyFeaturesMessage detectorId="tempId" />;
expect(render(component)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EmptyFeaturesMessage renders 1`] = `
<div
style="border-radius:5px;padding:10px;border:1px solid #D9D9D9;height:250px;width:100%"
>
<div
class="euiEmptyPrompt"
style="max-width:45em"
>
<span
class="euiTextColor euiTextColor--subdued"
>
<div
class="euiText euiText--medium"
>
<div
class="euiText euiText--medium"
>
No features have been added to this anomaly detector. A feature is a metric that is used for anomaly detection. A detector can discover anomalies across one or more features.
</div>
</div>
</span>
<div
class="euiSpacer euiSpacer--l"
/>
<div
class="euiSpacer euiSpacer--s"
/>
<div
class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentCenter euiFlexGroup--directionColumn euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<a
class="euiButton euiButton--primary"
data-test-subj="createButton"
href="opendistro-anomaly-detection-kibana#/detectors/tempId/features"
rel="noopener noreferrer"
target="_blank"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Add Feature
</span>
</span>
</a>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,104 @@
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { Chart, Axis, LineSeries, RectAnnotation, niceTimeFormatter } from '@elastic/charts';
import { EuiPagination, EuiText, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import DelayedLoader from '../../../../../components/DelayedLoader';
import { ChartContainer } from '../../../../../components/ChartContainer/ChartContainer';
class FeatureChart extends React.Component {
constructor(props) {
super(props);
this.state = {
activePage: 0,
};
this.goToPage = this.goToPage.bind(this);
}
goToPage(pageNumber) {
this.setState({
activePage: pageNumber,
});
}
render() {
const { isLoading, startDateTime, endDateTime, featureData, annotations, title } = this.props;
const currentFeature = featureData[this.state.activePage];
const timeFormatter = niceTimeFormatter([startDateTime, endDateTime]);
return (
<DelayedLoader isLoading={isLoading}>
{showLoader => {
return currentFeature ? (
<React.Fragment>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText size="xs">
<strong>{title}</strong>
</EuiText>
</EuiFlexItem>
<EuiSpacer size="s" />
<EuiFlexItem grow={false}>
<EuiPagination
pageCount={featureData.length}
activePage={this.state.activePage}
onPageClick={this.goToPage}
/>
</EuiFlexItem>
</EuiFlexGroup>
<ChartContainer style={showLoader ? { opacity: 0.2 } : {}}>
<Chart>
<RectAnnotation
dataValues={annotations || []}
annotationId="react"
style={{
stroke: '#FCAAAA',
strokeWidth: 1.5,
fill: '#FCAAAA',
}}
/>
<Axis id="left" title={currentFeature.featureName} position="left" />
<Axis id="bottom" position="bottom" tickFormat={timeFormatter} />
<LineSeries
id="lines"
xScaleType="time"
yScaleType="linear"
xAccessor={'startTime'}
yAccessors={['data']}
data={currentFeature.data}
/>
</Chart>
</ChartContainer>
</React.Fragment>
) : null;
}}
</DelayedLoader>
);
}
}
FeatureChart.propTypes = {
isLoading: PropTypes.bool.isRequired,
startDateTime: PropTypes.number.isRequired,
endDateTime: PropTypes.number.isRequired,
featureData: PropTypes.array.isRequired,
annotations: PropTypes.array,
title: PropTypes.string,
};
FeatureChart.defaultProps = {
title: 'Sample feature data',
};
export { FeatureChart };

Some files were not shown because too many files have changed in this diff Show More