Timelion App removal (#110255)

* Remove timelion app and stuff which related to it

* Fix CI

* Fix lint

* Fix tests

* Fix tests

* Fis tests

* Fix some comments

* Clean up

* fix CI

* fix some comments

* Fix deprecation examples

* Return `enabled` property in config for timelion vis

* Remove unused angular lib

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Alexey Antonov <alexwizp@gmail.com>
This commit is contained in:
Uladzislau Lasitsa 2021-09-10 14:53:07 +03:00 committed by GitHub
parent 3c71408690
commit 70090e326c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
240 changed files with 147 additions and 10726 deletions

1
.github/CODEOWNERS vendored
View file

@ -25,7 +25,6 @@
/src/plugins/charts/ @elastic/kibana-vis-editors
/src/plugins/management/ @elastic/kibana-vis-editors
/src/plugins/kibana_legacy/ @elastic/kibana-vis-editors
/src/plugins/timelion/ @elastic/kibana-vis-editors
/src/plugins/vis_default_editor/ @elastic/kibana-vis-editors
/src/plugins/vis_types/metric/ @elastic/kibana-vis-editors
/src/plugins/vis_type_table/ @elastic/kibana-vis-editors

View file

@ -56,7 +56,7 @@
"server": "src/legacy/server",
"statusPage": "src/legacy/core_plugins/status_page",
"telemetry": ["src/plugins/telemetry", "src/plugins/telemetry_management_section"],
"timelion": ["src/plugins/timelion", "src/plugins/vis_type_timelion"],
"timelion": ["src/plugins/vis_type_timelion"],
"uiActions": "src/plugins/ui_actions",
"visDefaultEditor": "src/plugins/vis_default_editor",
"visTypeMarkdown": "src/plugins/vis_type_markdown",

View file

@ -1393,10 +1393,6 @@
"plugin": "kibanaOverview",
"path": "src/plugins/kibana_overview/public/application.tsx"
},
{
"plugin": "timelion",
"path": "src/plugins/timelion/public/application.ts"
},
{
"plugin": "management",
"path": "src/plugins/management/target/types/public/application.d.ts"

View file

@ -13,7 +13,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex
| Deprecated API | Referencing plugin(s) | Remove By |
| ---------------|-----------|-----------|
| <DocLink id="kibDataPluginApi" section="def-public.esFilters" text="esFilters"/> | discover, visualizations, dashboard, lens, observability, maps, dashboardEnhanced, discoverEnhanced, securitySolution, visualize, timelion, presentationUtil | 8.1 |
| <DocLink id="kibDataPluginApi" section="def-public.esFilters" text="esFilters"/> | discover, visualizations, dashboard, lens, observability, maps, dashboardEnhanced, discoverEnhanced, securitySolution, visualize, presentationUtil | 8.1 |
| <DocLink id="kibDataPluginApi" section="def-public.esQuery" text="esQuery"/> | lens, timelines, infra, securitySolution, stackAlerts, transform, indexPatternManagement, visTypeTimelion, visTypeVega | 8.1 |
| <DocLink id="kibDataPluginApi" section="def-public.Filter" text="Filter"/> | discover, visualizations, dashboard, lens, observability, timelines, maps, infra, dashboardEnhanced, discoverEnhanced, securitySolution, urlDrilldown, inputControlVis, visTypeTimelion, visualize, visTypeVega, presentationUtil, ml, visTypeTimeseries | 8.1 |
| <DocLink id="kibDataPluginApi" section="def-common.Filter" text="Filter"/> | discover, visualizations, dashboard, lens, observability, timelines, maps, infra, dashboardEnhanced, discoverEnhanced, securitySolution, urlDrilldown, inputControlVis, visTypeTimelion, visualize, visTypeVega, presentationUtil, ml, visTypeTimeseries | 8.1 |
@ -86,16 +86,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex
| <DocLink id="kibDataPluginApi" section="def-common.SearchSource.create" text="create"/> | discover | - |
| <DocLink id="kibDataPluginApi" section="def-server.IndexPatternAttributes" text="IndexPatternAttributes"/> | discover, ml, transform, canvas | - |
| <DocLink id="kibSavedObjectsPluginApi" section="def-public.SavedObjectSaveModal" text="SavedObjectSaveModal"/> | embeddable, discover, presentationUtil, dashboard, graph | - |
| <DocLink id="kibSavedObjectsPluginApi" section="def-public.SavedObjectLoader" text="SavedObjectLoader"/> | visualizations, discover, dashboard, savedObjectsManagement, timelion | - |
| <DocLink id="kibSavedObjectsPluginApi" section="def-public.SavedObjectLoader" text="SavedObjectLoader"/> | visualizations, discover, dashboard, savedObjectsManagement | - |
| <DocLink id="kibSavedObjectsPluginApi" section="def-public.SavedObject" text="SavedObject"/> | discover, savedObjectsTaggingOss, visualizations, dashboard, visualize, visDefaultEditor | - |
| <DocLink id="kibSavedObjectsPluginApi" section="def-public.SavedObjectsStart.SavedObjectClass" text="SavedObjectClass"/> | discover, visualizations, dashboard, timelion | - |
| <DocLink id="kibSavedObjectsPluginApi" section="def-public.SavedObjectsStart.SavedObjectClass" text="SavedObjectClass"/> | discover, visualizations, dashboard | - |
| <DocLink id="kibUiActionsPluginApi" section="def-public.UiActionsService.executeTriggerActions" text="executeTriggerActions"/> | data, discover, embeddable | - |
| <DocLink id="kibCorePluginApi" section="def-public.UiSettingsParams.metric" text="metric"/> | advancedSettings, discover | - |
| <DocLink id="kibCorePluginApi" section="def-server.UiSettingsParams.metric" text="metric"/> | advancedSettings, discover | - |
| <DocLink id="kibFeaturesPluginApi" section="def-common.FeatureElasticsearchPrivileges.requiredRoles" text="requiredRoles"/> | security | - |
| <DocLink id="kibFeaturesPluginApi" section="def-server.FeatureElasticsearchPrivileges.requiredRoles" text="requiredRoles"/> | security | - |
| <DocLink id="kibLicensingPluginApi" section="def-public.LicensingPluginSetup.license$" text="license$"/> | security, licenseManagement, ml, fleet, apm, reporting, crossClusterReplication, logstash, painlessLab, searchprofiler, watcher | - |
| <DocLink id="kibCorePluginApi" section="def-public.AppMountParameters.appBasePath" text="appBasePath"/> | management, fleet, security, kibanaOverview, timelion | - |
| <DocLink id="kibCorePluginApi" section="def-public.AppMountParameters.appBasePath" text="appBasePath"/> | management, fleet, security, kibanaOverview | - |
| <DocLink id="kibDataPluginApi" section="def-public.INDEX_PATTERN_SAVED_OBJECT_TYPE" text="INDEX_PATTERN_SAVED_OBJECT_TYPE"/> | visualizations, dashboard | - |
| <DocLink id="kibDataPluginApi" section="def-common.INDEX_PATTERN_SAVED_OBJECT_TYPE" text="INDEX_PATTERN_SAVED_OBJECT_TYPE"/> | visualizations, dashboard | - |
| <DocLink id="kibDataPluginApi" section="def-server.INDEX_PATTERN_SAVED_OBJECT_TYPE" text="INDEX_PATTERN_SAVED_OBJECT_TYPE"/> | visualizations, dashboard | - |

View file

@ -811,17 +811,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex
## timelion
| Deprecated API | Reference location(s) | Remove By |
| ---------------|-----------|-----------|
| <DocLink id="kibDataPluginApi" section="def-public.esFilters" text="esFilters"/> | [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/timelion/public/plugin.ts#:~:text=esFilters), [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/timelion/public/plugin.ts#:~:text=esFilters) | 8.1 |
| <DocLink id="kibSavedObjectsPluginApi" section="def-public.SavedObjectLoader" text="SavedObjectLoader"/> | [saved_sheets.ts](https://github.com/elastic/kibana/tree/master/src/plugins/timelion/public/services/saved_sheets.ts#:~:text=SavedObjectLoader), [saved_sheets.ts](https://github.com/elastic/kibana/tree/master/src/plugins/timelion/public/services/saved_sheets.ts#:~:text=SavedObjectLoader) | - |
| <DocLink id="kibSavedObjectsPluginApi" section="def-public.SavedObjectsStart.SavedObjectClass" text="SavedObjectClass"/> | [_saved_sheet.ts](https://github.com/elastic/kibana/tree/master/src/plugins/timelion/public/services/_saved_sheet.ts#:~:text=SavedObjectClass) | - |
| <DocLink id="kibCorePluginApi" section="def-public.AppMountParameters.appBasePath" text="appBasePath"/> | [application.ts](https://github.com/elastic/kibana/tree/master/src/plugins/timelion/public/application.ts#:~:text=appBasePath) | - |
## transform
| Deprecated API | Reference location(s) | Remove By |

View file

@ -679,14 +679,6 @@
{
"plugin": "savedObjectsManagement",
"path": "src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx"
},
{
"plugin": "timelion",
"path": "src/plugins/timelion/public/services/saved_sheets.ts"
},
{
"plugin": "timelion",
"path": "src/plugins/timelion/public/services/saved_sheets.ts"
}
],
"children": [
@ -3860,10 +3852,6 @@
{
"plugin": "dashboard",
"path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts"
},
{
"plugin": "timelion",
"path": "src/plugins/timelion/public/services/_saved_sheet.ts"
}
]
},

View file

@ -134,7 +134,6 @@ The API returns the following:
"index-pattern",
"search",
"visualization",
"timelion-sheet",
"canvas-workpad"
]
},
@ -152,7 +151,6 @@ The API returns the following:
"index-pattern",
"search",
"visualization",
"timelion-sheet",
"canvas-workpad",
"dashboard"
]

View file

@ -73,9 +73,6 @@ The API returns the following:
"indexPatterns": [
"read"
],
"timelion": [
"all"
],
"graph": [
"all"
],

View file

@ -94,9 +94,6 @@ $ curl -X PUT api/security/role/my_kibana_role
"indexPatterns": [
"read"
],
"timelion": [
"all"
],
"graph": [
"all"
],

View file

@ -30,7 +30,7 @@ experimental[] Create multiple {kib} saved objects.
==== Request body
`type`::
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`, and `timelion-sheet`.
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`.
`id`::
(Optional, string) Specifies an ID instead of using a randomly generated ID.

View file

@ -23,7 +23,7 @@ experimental[] Retrieve multiple {kib} saved objects by ID.
==== Request Body
`type`::
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`, and `timelion-sheet`.
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`.
`id`::
(Required, string) ID of the retrieved object. The ID includes the {kib} unique identifier or a custom identifier.

View file

@ -24,7 +24,7 @@ experimental[] Create {kib} saved objects.
(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used.
`<type>`::
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`, and `timelion-sheet`.
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`.
`<id>`::
(Optional, string) Specifies an ID instead of using a randomly generated ID.

View file

@ -22,7 +22,7 @@ WARNING: Once you delete a saved object, _it cannot be recovered_.
(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used.
`type`::
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`, and `timelion-sheet`.
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`.
`id`::
(Required, string) The object ID that you want to remove.

View file

@ -21,7 +21,7 @@ experimental[] Retrieve a single {kib} saved object by ID.
`type`::
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`, and `timelion-sheet`.
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`.
`id`::
(Required, string) The ID of the object to retrieve.

View file

@ -25,7 +25,7 @@ object can be retrieved via the Resolve API using either its new ID or its old I
`type`::
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`, and `timelion-sheet`.
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`.
`id`::
(Required, string) The ID of the object to retrieve.

View file

@ -20,7 +20,7 @@ experimental[] Update the attributes for existing {kib} saved objects.
(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used.
`type`::
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`, and `timelion-sheet`.
(Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`.
`id`::
(Required, string) The object ID to update.

View file

@ -70,7 +70,7 @@ The API returns the following:
"id": "sales",
"name": "Sales",
"initials": "MK",
"disabledFeatures": ["discover", "timelion"],
"disabledFeatures": ["discover"],
"imageUrl": ""
}
]
@ -124,7 +124,7 @@ The API returns the following:
"id": "sales",
"name": "Sales",
"initials": "MK",
"disabledFeatures": ["discover", "timelion"],
"disabledFeatures": ["discover"],
"imageUrl": "",
"authorizedPurposes": {
"any": true,

View file

@ -54,7 +54,7 @@ $ curl -X POST api/spaces/space
"description" : "This is the Marketing Space",
"color": "#aabbcc",
"initials": "MK",
"disabledFeatures": ["timelion"],
"disabledFeatures": [],
"imageUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAABACAYAAABC6cT1AAAGf0lEQVRoQ+3abYydRRUH8N882xYo0IqagEVjokQJKAiKBjXExC9G/aCkGowCIghCkRcrVSSKIu/FEiqgGL6gBIlAYrAqUTH6hZgQFVEMKlQFfItWoQWhZe8z5uzMLdvbfbkLxb13d+fbvfe588x/zpn/+Z9zJpmnI81T3BaAzzfLL1h8weLzZAcWXH2eGHo7zAWLL1h8nuzAjFw9G1N6Kzq8HnuM36MR8iibF3Fv4q+7cv8yDV6K13bYq2furSP8Ag8ncr/vnSnwRViJT2GfCV7yL1yHGxLb+l3EdM9lluNEnIC9xz+f2ZL4Er6Z2DrdXN3fZwp8CU7OfDHxggle8lTLbQ1nJ/7Z7yKmey5zYGZt4h2IzR8/trRc2PDlxJPTzfVcgJ+CC0wMPOa9F6cm7up3EVM9V9386MxliVdM8GwAv6hh/awCz/w7lY25OtF5ruBz4ZLP42NYNrDAFbC3YPWuILnMAfgq3oaRQQYea/stViV+sgssvjKzLvGySeaaNVfP4d7Btokgvxj/bblgpueuF1hmWcyTCmfE3J3M1lTcv0vMswM88zR+jpw4osu6me8kzkpsfLZWzxyRuabO22buxxOJ12FxnXfWgEe83pB5sOE47BsLymzscOoi7nw2JJfZreUjiUsTyzKPZm5NvBDvSuw268AzNzV8H5/Am+qCnsAXgpgSW2Zq9cyKlksbPlTd+te4quWNieMHBfiNDdciYnwsdI/MaOaWhnMTf54J8CqNj8x8JXFIZltYu+HqlmNT8YSBsHgAPw/vxvlVV4du/s0oaxbxg0TbL/jMni0nNcVjQq7+HZfgtpbzBg342TgQ63AkmsymxBW4IjE6A+D7Vzd/fyWxIM/VuCe+HzTgZ2Jpy/kNJ2FJLmLm24mPJ/42A+Bvrxt4SISwlhsaPodH26LZB8rVA3inwwebsrixJCZzX+KMxI/7AV61eVh3DV6Mx3EOvh4kN6jAg8nfUCXm4d1wE66OyxNPTQc+s3/o/MoXizL3JE5O3F3P/uBZPPF4Zr+Wi5uSO48ZPRdyCwn7YB/A35m5KhWNHox4fcNnIs0ddOCRSBxf8+cQG+Huf0l8NJVYP+nI7NXy2ar4QqIGm69JfKPOE2w/mBavCzwM11R2D+ChsUO7hyUfmwx55qDM1xJvqZ7y08TpifuGBfjeURVJnNIVGpkNiXNS0ds7jcySDitDCCWW56LJ10fRo8sNA+3qXUSZD2CtQlZh9T+1rB7h9oliembflnMbzqgSNZKbKGHdPm7OwXb1CvQ1metSETMpszmzvikCJNh/h5E5PHNl4qga/+/cxqrdeWDYgIe7X5L4cGJPJX2940lOX8pD41FnFnc4riluvQKbK0dcHJFi2IBHNTQSlguru4d2/wPOTNzRA3x5y+U1E1uqWDkETOT026XuUJzx6u7ReLhSYenQ7uHua0fKZmwfmcPqsQjxE5WVONcRxn7X89zgn/EKPMRMxOVQXmP18Mx3q3b/Y/0cQE/IhFtHESMsHFlZ1Ml3CH3DZPHImY+pxcKumNmYirtvqMBfhMuU6s3iqOQkTsMPe1tCQwO8Ajs0lxr7W+vnp1MJc9EgCNd/cy6x+9D4veXmprj5wxMw/3C4egW6zzgZOlYZzfwo3F2J7ael0pJamvlPKgWNKFft1AAcKotXoFEbD7kaoSoQPVKB35+5KHF0lai/rJo+up87jWEE/qqqwY+qrL21LWLm95lPJ16ppKw31XC3PXYPJauPEx7B6BHCgrSizRs18qiaRp8tlN3ueCTYPHH9RNaunjI8Z7wLYpT3jZSCYXQ8e9vTsRE/q+no3XMKeObgGtaintbb/AvXj4JDkNw/5hrwYPfIvlZFUbLn7G5q+eQIN09Vnho6cqvnM/Lt99RixH49wO8K0ZL41WTWHoQzvsNVkOheZqKhEGpsp3SzB+BBtZAYve7uOR9tuTaaB6l0XScdYfEQPpkTUyHEGP+XqyDBzu+NBCITUjNWHynkrbWKOuWFn1xKzqsyx0bdvS78odp0+N503Zao0uCsWuSIDku8/7EO60b41vN5+Ses9BKlTdvd8bhp9EBvJjWJAIn/vxwHe6b3tSk6JFPV4nq85oAOrx555v/x/rh3E6Lo+bnuNS4uB4Cuq0ZfvO8X1rM6q/+vnjLVqZq7v83onttc2oYF4HPJmv1gWbB4P7s0l55ZsPhcsmY/WBYs3s8uzaVn5q3F/wf70mRuBCtbjQAAAABJRU5ErkJggg=="
}
--------------------------------------------------

View file

@ -235,11 +235,6 @@ generating deep links to other apps, and creating short URLs.
|This plugin adds the Advanced Settings section for the Usage and Security Data collection (aka Telemetry).
|{kib-repo}blob/{branch}/src/plugins/timelion/README.md[timelion]
|Contains the deprecated timelion application. For the timelion visualization,
which also contains the timelion APIs and backend, look at the vis_type_timelion plugin.
|<<uiactions-plugin>>
|UI Actions plugins provides API to manage *triggers* and *actions*.

View file

@ -25,29 +25,29 @@ import { i18n } from '@kbn/i18n';
async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecationsContext): Promise<DeprecationsDetails[]> {
const deprecations: DeprecationsDetails[] = [];
const count = await getTimelionSheetsCount(savedObjectsClient);
// Example of an api correctiveAction
const count = await getFooCount(savedObjectsClient);
if (count > 0) {
// Example of a manual correctiveAction
deprecations.push({
title: i18n.translate('xpack.timelion.deprecations.worksheetsTitle', {
defaultMessage: 'Timelion worksheets are deprecated'
title: i18n.translate('xpack.foo.deprecations.title', {
defaultMessage: `Foo's are deprecated`
}),
message: i18n.translate('xpack.timelion.deprecations.worksheetsMessage', {
defaultMessage: 'You have {count} Timelion worksheets. Migrate your Timelion worksheets to a dashboard to continue using them.',
message: i18n.translate('xpack.foo.deprecations.message', {
defaultMessage: `You have {count} Foo's. Migrate your Foo's to a dashboard to continue using them.`,
values: { count },
}),
documentationUrl:
'https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html',
'https://www.elastic.co/guide/en/kibana/current/foo.html',
level: 'warning',
correctiveActions: {
manualSteps: [
i18n.translate('xpack.timelion.deprecations.worksheets.manualStepOneMessage', {
defaultMessage: 'Navigate to the Kibana Dashboard and click "Create dashboard".',
}),
i18n.translate('xpack.timelion.deprecations.worksheets.manualStepTwoMessage', {
defaultMessage: 'Select Timelion from the "New Visualization" window.',
}),
i18n.translate('xpack.foo.deprecations.manualStepOneMessage', {
defaultMessage: 'Navigate to the Kibana Dashboard and click "Create dashboard".',
}),
i18n.translate('xpack.foo.deprecations.manualStepTwoMessage', {
defaultMessage: 'Select Foo from the "New Visualization" window.',
}),
],
api: {
path: '/internal/security/users/test_dashboard_user',
@ -68,6 +68,7 @@ async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecations
},
});
}
return deprecations;
}

View file

@ -470,13 +470,6 @@ The default period of time in the Security time filter.
[[kibana-timelion-settings]]
==== Timelion
[horizontal]
[[timelion-defaultcolumns]]`timelion:default_columns`::
The default number of columns to use on a Timelion sheet.
[[timelion-defaultrows]]`timelion:default_rows`::
The default number of rows to use on a Timelion sheet.
[[timelion-esdefaultindex]]`timelion:es.default_index`::
The default index when using the `.es()` query.
@ -502,9 +495,6 @@ experimental:[]
Used with quandl queries, this is your API key from
https://www.quandl.com/[www.quandl.com].
[[timelion-showtutorial]]`timelion:showTutorial`::
Shows the Timelion tutorial to users when they first open the Timelion app.
[[timelion-targetbuckets]]`timelion:target_buckets`::
Used for calculating automatic intervals in visualizations, this is the number
of buckets to try to represent.

View file

@ -13,8 +13,6 @@ The syntax enables some features that classical point series charts don't offer,
[role="screenshot"]
image:dashboard/images/timelion.png[Timelion]
deprecated::[7.0.0,"*Timelion* is still supported. The *Timelion app* is deprecated in 7.0, replaced by dashboard features. In 7.16 and later, the *Timelion app* is removed from {kib}. To prepare for the removal of *Timelion app*, you must migrate *Timelion app* worksheets to a dashboard. For information on how to migrate *Timelion app* worksheets, refer to the link:https://www.elastic.co/guide/en/kibana/7.10/release-notes-7.10.0.html#deprecation-v7.10.0[7.10.0 Release Notes]."]
[float]
==== Timelion expressions

View file

@ -189,7 +189,6 @@
"angular-recursion": "^1.0.5",
"angular-route": "^1.8.0",
"angular-sanitize": "^1.8.0",
"angular-sortable-view": "^0.0.17",
"antlr4ts": "^0.5.0-alpha.3",
"archiver": "^5.2.0",
"axios": "^0.21.1",

View file

@ -70,7 +70,6 @@ pageLoadAssetSize:
spaces: 57868
telemetry: 51957
telemetryManagementSection: 38586
timelion: 29920
transform: 41007
triggersActionsUi: 100000
uiActions: 97717

View file

@ -342,7 +342,6 @@ functions and will be impacted:
2. [tile_map](https://github.com/elastic/kibana/blob/6039709929caf0090a4130b8235f3a53bd04ed84/src/legacy/core_plugins/tile_map/public/plugin.ts#L62)
3. [vis_type_table](https://github.com/elastic/kibana/blob/6039709929caf0090a4130b8235f3a53bd04ed84/src/legacy/core_plugins/vis_type_table/public/plugin.ts#L61)
4. [vis_type_vega](https://github.com/elastic/kibana/blob/6039709929caf0090a4130b8235f3a53bd04ed84/src/legacy/core_plugins/vis_type_vega/public/plugin.ts#L59)
5. [timelion](https://github.com/elastic/kibana/blob/9d69b72a5f200e58220231035b19da852fc6b0a5/src/plugins/timelion/server/plugin.ts#L40)
6. [code](https://github.com/elastic/kibana/blob/5049b460b47d4ae3432e1d9219263bb4be441392/x-pack/legacy/plugins/code/server/plugin.ts#L129-L149)
7. [spaces](https://github.com/elastic/kibana/blob/096c7ee51136327f778845c636d7c4f1188e5db2/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts#L95)
8. [licensing](https://github.com/elastic/kibana/blob/4667c46caef26f8f47714504879197708debae32/x-pack/plugins/licensing/server/plugin.ts)

View file

@ -273,7 +273,6 @@ export class DocLinksService {
},
visualize: {
guide: `${KIBANA_DOCS}dashboard.html`,
timelionDeprecation: `${KIBANA_DOCS}timelion.html`,
lens: `${ELASTIC_WEBSITE_URL}what-is/kibana-lens`,
lensPanels: `${KIBANA_DOCS}lens.html`,
maps: `${ELASTIC_WEBSITE_URL}maps`,

View file

@ -139,7 +139,6 @@ Plugins are responsible for registering any deprecations during the `setup` life
the deprecations service.
Examples of non-config deprecations include things like
- timelion sheets
- kibana_user security roles
This service is not intended to be used for non-user facing deprecations or cases where the deprecation

View file

@ -37,28 +37,27 @@ import { SavedObjectsClientContract } from '../saved_objects/types';
*
* async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecationsContext): Promise<DeprecationsDetails[]> {
* const deprecations: DeprecationsDetails[] = [];
* const count = await getTimelionSheetsCount(savedObjectsClient);
*
* const count = await getFooCount(savedObjectsClient);
* if (count > 0) {
* // Example of a manual correctiveAction
* deprecations.push({
* title: i18n.translate('xpack.timelion.deprecations.worksheetsTitle', {
* defaultMessage: 'Timelion worksheets are deprecated'
* title: i18n.translate('xpack.foo.deprecations.title', {
* defaultMessage: `Foo's are deprecated`
* }),
* message: i18n.translate('xpack.timelion.deprecations.worksheetsMessage', {
* defaultMessage: 'You have {count} Timelion worksheets. Migrate your Timelion worksheets to a dashboard to continue using them.',
* message: i18n.translate('xpack.foo.deprecations.message', {
* defaultMessage: `You have {count} Foo's. Migrate your Foo's to a dashboard to continue using them.`,
* values: { count },
* }),
* documentationUrl:
* 'https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html',
* 'https://www.elastic.co/guide/en/kibana/current/foo.html',
* level: 'warning',
* correctiveActions: {
* manualSteps: [
* i18n.translate('xpack.timelion.deprecations.worksheets.manualStepOneMessage', {
* i18n.translate('xpack.foo.deprecations.manualStepOneMessage', {
* defaultMessage: 'Navigate to the Kibana Dashboard and click "Create dashboard".',
* }),
* i18n.translate('xpack.timelion.deprecations.worksheets.manualStepTwoMessage', {
* defaultMessage: 'Select Timelion from the "New Visualization" window.',
* i18n.translate('xpack.foo.deprecations.manualStepTwoMessage', {
* defaultMessage: 'Select Foo from the "New Visualization" window.',
* }),
* ],
* api: {

View file

@ -45,6 +45,8 @@ export const REMOVED_TYPES: string[] = [
'tsvb-validation-telemetry',
// replaced by osquery-manager-usage-metric
'osquery-usage-metric',
// Was removed in 7.16
'timelion-sheet',
].sort();
// When migrating from the outdated index we use a read query which excludes

View file

@ -200,7 +200,6 @@ kibana_vars=(
tilemap.options.minZoom
tilemap.options.subdomains
tilemap.url
timelion.enabled
url_drilldown.enabled
vega.enableExternalUrls
vis_type_vega.enableExternalUrls

View file

@ -9,7 +9,7 @@ This plugin registers the Platform Usage Collectors in Kibana.
| **Config Usage** | Reports the non-default values set via `kibana.yml` config file or CLI options. It `[redacts]` any potential PII-sensitive values. | [Link](./server/collectors/config_usage/README.md) |
| **User-changed UI Settings** | Reports all the UI Settings that have been overwritten by the user. It `[redacts]` any potential PII-sensitive values. | [Link](./server/collectors/management/README.md) |
| **CSP configuration** | Reports the key values regarding the CSP configuration. | - |
| **Kibana** | It reports the number of Saved Objects per type. It is limited to `dashboard`, `visualization`, `search`, `index-pattern`, `graph-workspace` and `timelion-sheet`.<br> It exists for legacy purposes, and may still be used by Monitoring via Metricbeat. | - |
| **Kibana** | It reports the number of Saved Objects per type. It is limited to `dashboard`, `visualization`, `search`, `index-pattern`, `graph-workspace`.<br> It exists for legacy purposes, and may still be used by Monitoring via Metricbeat. | - |
| **Saved Objects Counts** | Number of Saved Objects per type. | - |
| **Localization data** | Localization settings: setup locale and installed translation files. | - |
| **Ops stats** | Operation metrics from the system. | - |

View file

@ -124,7 +124,6 @@ export const applicationUsageSchema = {
kibana: commonSchema, // It's a forward app so we'll likely never report it
management: commonSchema,
short_url_redirect: commonSchema, // It's a forward app so we'll likely never report it
timelion: commonSchema,
visualize: commonSchema,
error: commonSchema,
status: commonSchema,

View file

@ -104,22 +104,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'keyword',
_meta: { description: 'Non-default value of setting.' },
},
'timelion:default_rows': {
type: 'long',
_meta: { description: 'Non-default value of setting.' },
},
'timelion:default_columns': {
type: 'long',
_meta: { description: 'Non-default value of setting.' },
},
'timelion:es.default_index': {
type: 'keyword',
_meta: { description: 'Non-default value of setting.' },
},
'timelion:showTutorial': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'securitySolution:timeDefaults': {
type: 'keyword',
_meta: { description: 'Non-default value of setting.' },

View file

@ -49,10 +49,7 @@ export interface UsageStats {
'timelion:max_buckets': number;
'timelion:es.timefield': string;
'timelion:min_interval': string;
'timelion:default_rows': number;
'timelion:default_columns': number;
'timelion:es.default_index': string;
'timelion:showTutorial': boolean;
'securitySolution:timeDefaults': string;
'securitySolution:defaultAnomalyScore': number;
'securitySolution:refreshIntervalDefaults': string;

View file

@ -56,7 +56,6 @@ describe('kibana_usage', () => {
search: { total: 0 },
index_pattern: { total: 0 },
graph_workspace: { total: 0 },
timelion_sheet: { total: 0 },
});
});
});
@ -81,7 +80,6 @@ describe('getKibanaSavedObjectCounts', () => {
search: { total: 0 },
index_pattern: { total: 0 },
graph_workspace: { total: 0 },
timelion_sheet: { total: 0 },
});
});
@ -91,7 +89,6 @@ describe('getKibanaSavedObjectCounts', () => {
types: {
buckets: [
{ key: 'dashboard', doc_count: 1 },
{ key: 'timelion-sheet', doc_count: 2 },
{ key: 'index-pattern', value: 2 }, // Malformed on purpose
{ key: 'graph_workspace', doc_count: 3 }, // already snake_cased
],
@ -106,7 +103,6 @@ describe('getKibanaSavedObjectCounts', () => {
search: { total: 0 },
index_pattern: { total: 0 },
graph_workspace: { total: 3 },
timelion_sheet: { total: 2 },
});
});
});

View file

@ -19,21 +19,13 @@ interface KibanaSavedObjectCounts {
search: { total: number };
index_pattern: { total: number };
graph_workspace: { total: number };
timelion_sheet: { total: number };
}
interface KibanaUsage extends KibanaSavedObjectCounts {
index: string;
}
const TYPES = [
'dashboard',
'visualization',
'search',
'index-pattern',
'graph-workspace',
'timelion-sheet',
];
const TYPES = ['dashboard', 'visualization', 'search', 'index-pattern', 'graph-workspace'];
export async function getKibanaSavedObjectCounts(
esClient: ElasticsearchClient,
@ -89,12 +81,6 @@ export function registerKibanaUsageCollector(
_meta: { description: 'Total number of graph_workspace saved objects' },
},
},
timelion_sheet: {
total: {
type: 'long',
_meta: { description: 'Total number of timelion_sheet saved objects' },
},
},
},
async fetch({ esClient }) {
const {

View file

@ -28,7 +28,6 @@ const uiMetricFromDataPluginSchema: MakeSchemaFrom<UIMetricUsage> = {
kibana: commonSchema, // It's a forward app so we'll likely never report it
management: commonSchema,
short_url_redirect: commonSchema, // It's a forward app so we'll likely never report it
timelion: commonSchema,
visualize: commonSchema,
// X-Pack

View file

@ -1088,137 +1088,6 @@
}
}
},
"timelion": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "Always `main`"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 90 days"
}
},
"views": {
"type": "array",
"items": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "The application view being tracked"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application sub view since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application sub view is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 90 days"
}
}
}
}
}
}
},
"visualize": {
"properties": {
"appId": {
@ -7253,30 +7122,12 @@
"description": "Non-default value of setting."
}
},
"timelion:default_rows": {
"type": "long",
"_meta": {
"description": "Non-default value of setting."
}
},
"timelion:default_columns": {
"type": "long",
"_meta": {
"description": "Non-default value of setting."
}
},
"timelion:es.default_index": {
"type": "keyword",
"_meta": {
"description": "Non-default value of setting."
}
},
"timelion:showTutorial": {
"type": "boolean",
"_meta": {
"description": "Non-default value of setting."
}
},
"securitySolution:timeDefaults": {
"type": "keyword",
"_meta": {
@ -7855,16 +7706,6 @@
}
}
}
},
"timelion_sheet": {
"properties": {
"total": {
"type": "long",
"_meta": {
"description": "Total number of timelion_sheet saved objects"
}
}
}
}
}
},
@ -8396,25 +8237,6 @@
}
}
},
"timelion": {
"type": "array",
"items": {
"properties": {
"key": {
"type": "keyword",
"_meta": {
"description": "The event that is tracked"
}
},
"value": {
"type": "long",
"_meta": {
"description": "The value of the event"
}
}
}
}
},
"csm": {
"type": "array",
"items": {

View file

@ -64,13 +64,6 @@
},
"kibana": {
"properties": {
"timelion_sheet": {
"properties": {
"total": {
"type": "long"
}
}
},
"visualization": {
"properties": {
"total": {

View file

@ -1,2 +0,0 @@
Contains the deprecated timelion application. For the timelion visualization,
which also contains the timelion APIs and backend, look at the vis_type_timelion plugin.

View file

@ -1,23 +0,0 @@
{
"id": "timelion",
"version": "kibana",
"ui": true,
"server": true,
"requiredBundles": [
"kibanaLegacy",
"kibanaUtils",
"visTypeTimelion"
],
"requiredPlugins": [
"visualizations",
"data",
"navigation",
"visTypeTimelion",
"savedObjects",
"kibanaLegacy"
],
"owner": {
"name": "Vis Editors",
"githubTeam": "kibana-vis-editors"
}
}

View file

@ -1,83 +0,0 @@
.timApp {
position: relative;
background: $euiColorEmptyShade;
[ng-click] {
cursor: pointer;
}
}
.timApp__container {
margin: $euiSizeM;
}
.timApp__menus {
margin: $euiSizeM;
}
.timApp__title {
display: flex;
align-items: center;
padding: $euiSizeM $euiSizeS;
font-size: $euiSize;
font-weight: $euiFontWeightBold;
border-bottom: 1px solid $euiColorLightShade;
flex-grow: 1;
background-color: $euiColorEmptyShade;
}
.timApp__stats {
font-weight: $euiFontWeightRegular;
color: $euiColorMediumShade;
}
.timApp__form {
display: flex;
align-items: flex-start;
margin-top: $euiSize;
margin-bottom: $euiSize;
}
.timApp__expression {
display: flex;
flex: 1;
margin-right: $euiSizeS;
}
.timApp__button {
margin-top: $euiSizeS;
padding: $euiSizeXS $euiSizeM;
font-size: $euiSize;
border: none;
border-radius: $euiSizeXS;
color: $euiColorEmptyShade;
background-color: $euiColorPrimary;
}
.timApp__button--secondary {
margin-top: $euiSizeS;
padding: $euiSizeXS $euiSizeM;
font-size: $euiSize;
border: 1px solid $euiColorPrimary;
border-radius: $euiSizeXS;
color: $euiColorPrimary;
width: 100%;
}
.timApp__sectionTitle {
margin-bottom: $euiSizeM;
font-size: 18px;
color: $euiColorDarkestShade;
}
.timApp__helpText {
margin-bottom: $euiSize;
font-size: 14px;
color: $euiColorDarkShade;
}
.timApp__label {
font-size: $euiSize;
line-height: 1.5;
font-weight: $euiFontWeightBold;
}

View file

@ -1,18 +0,0 @@
// Angular form states
.ng-invalid {
&.ng-dirty,
&.ng-touched {
border-color: $euiColorDanger;
}
}
input[type='radio'],
input[type='checkbox'],
.radio,
.checkbox {
&[disabled],
fieldset[disabled] & {
cursor: default;
opacity: .8;
}
}

View file

@ -1,655 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { createHashHistory } from 'history';
import { createKbnUrlStateStorage, withNotifyOnErrors } from '../../kibana_utils/public';
import { syncQueryStateWithUrl } from '../../data/public';
import { getSavedSheetBreadcrumbs, getCreateBreadcrumbs } from './breadcrumbs';
import {
addFatalError,
registerListenEventListener,
watchMultiDecorator,
} from '../../kibana_legacy/public';
import { _LEGACY_ as visTypeTimelion } from '../../vis_type_timelion/public';
import { initCellsDirective } from './directives/cells/cells';
import { initFullscreenDirective } from './directives/fullscreen/fullscreen';
import { initFixedElementDirective } from './directives/fixed_element';
import { initTimelionLoadSheetDirective } from './directives/timelion_load_sheet';
import { initTimelionHelpDirective } from './directives/timelion_help/timelion_help';
import { initTimelionSaveSheetDirective } from './directives/timelion_save_sheet';
import { initTimelionOptionsSheetDirective } from './directives/timelion_options_sheet';
import { initSavedObjectSaveAsCheckBoxDirective } from './directives/saved_object_save_as_checkbox';
import { initSavedObjectFinderDirective } from './directives/saved_object_finder';
import { initTimelionTabsDirective } from './components/timelionhelp_tabs_directive';
import { initTimelionTDeprecationDirective } from './components/timelion_deprecation_directive';
import { initTimelionTopNavDirective } from './components/timelion_top_nav_directive';
import { initInputFocusDirective } from './directives/input_focus';
import { Chart } from './directives/chart/chart';
import { TimelionInterval } from './directives/timelion_interval/timelion_interval';
import { timelionExpInput } from './directives/timelion_expression_input';
import { TimelionExpressionSuggestions } from './directives/timelion_expression_suggestions/timelion_expression_suggestions';
import { initSavedSheetService } from './services/saved_sheets';
import { initTimelionAppState } from './timelion_app_state';
import rootTemplate from './index.html';
export function initTimelionApp(app, deps) {
app.run(registerListenEventListener);
const savedSheetLoader = initSavedSheetService(app, deps);
app.factory('history', () => createHashHistory());
app.factory('kbnUrlStateStorage', (history) =>
createKbnUrlStateStorage({
history,
useHash: deps.core.uiSettings.get('state:storeInSessionStorage'),
...withNotifyOnErrors(deps.core.notifications.toasts),
})
);
app.config(watchMultiDecorator);
app
.controller('TimelionVisController', function ($scope) {
$scope.$on('timelionChartRendered', (event) => {
event.stopPropagation();
$scope.renderComplete();
});
})
.constant('timelionPanels', deps.timelionPanels)
.directive('chart', Chart)
.directive('timelionInterval', TimelionInterval)
.directive('timelionExpressionSuggestions', TimelionExpressionSuggestions)
.directive('timelionExpressionInput', timelionExpInput(deps));
initTimelionHelpDirective(app);
initInputFocusDirective(app);
initTimelionTabsDirective(app, deps);
initTimelionTDeprecationDirective(app, deps);
initTimelionTopNavDirective(app, deps);
initSavedObjectFinderDirective(app, savedSheetLoader, deps.core.uiSettings);
initSavedObjectSaveAsCheckBoxDirective(app);
initCellsDirective(app);
initFixedElementDirective(app);
initFullscreenDirective(app);
initTimelionSaveSheetDirective(app);
initTimelionLoadSheetDirective(app);
initTimelionOptionsSheetDirective(app);
const location = 'Timelion';
app.directive('timelionApp', function () {
return {
restrict: 'E',
controllerAs: 'timelionApp',
controller: timelionController,
};
});
function timelionController(
$http,
$route,
$routeParams,
$scope,
$timeout,
history,
kbnUrlStateStorage
) {
// Keeping this at app scope allows us to keep the current page when the user
// switches to say, the timepicker.
$scope.page = deps.core.uiSettings.get('timelion:showTutorial', true) ? 1 : 0;
$scope.setPage = (page) => ($scope.page = page);
const timefilter = deps.plugins.data.query.timefilter.timefilter;
timefilter.enableAutoRefreshSelector();
timefilter.enableTimeRangeSelector();
deps.core.chrome.docTitle.change('Timelion - Kibana');
// starts syncing `_g` portion of url with query services
const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl(
deps.plugins.data.query,
kbnUrlStateStorage
);
const savedSheet = $route.current.locals.savedSheet;
function getStateDefaults() {
return {
sheet: savedSheet.timelion_sheet,
selected: 0,
columns: savedSheet.timelion_columns,
rows: savedSheet.timelion_rows,
interval: savedSheet.timelion_interval,
};
}
const { stateContainer, stopStateSync } = initTimelionAppState({
stateDefaults: getStateDefaults(),
kbnUrlStateStorage,
});
$scope.state = _.cloneDeep(stateContainer.getState());
$scope.expression = _.clone($scope.state.sheet[$scope.state.selected]);
$scope.updatedSheets = [];
const savedVisualizations = deps.plugins.visualizations.savedVisualizationsLoader;
const timezone = visTypeTimelion.getTimezone(deps.core.uiSettings);
const defaultExpression = '.es(*)';
$scope.topNavMenu = getTopNavMenu();
$timeout(function () {
if (deps.core.uiSettings.get('timelion:showTutorial', true)) {
$scope.toggleMenu('showHelp');
}
}, 0);
$scope.transient = {};
function getTopNavMenu() {
const newSheetAction = {
id: 'new',
label: i18n.translate('timelion.topNavMenu.newSheetButtonLabel', {
defaultMessage: 'New',
}),
description: i18n.translate('timelion.topNavMenu.newSheetButtonAriaLabel', {
defaultMessage: 'New Sheet',
}),
run: function () {
history.push('/');
$route.reload();
},
testId: 'timelionNewButton',
};
const addSheetAction = {
id: 'add',
label: i18n.translate('timelion.topNavMenu.addChartButtonLabel', {
defaultMessage: 'Add',
}),
description: i18n.translate('timelion.topNavMenu.addChartButtonAriaLabel', {
defaultMessage: 'Add a chart',
}),
run: function () {
$scope.$evalAsync(() => $scope.newCell());
},
testId: 'timelionAddChartButton',
};
const saveSheetAction = {
id: 'save',
label: i18n.translate('timelion.topNavMenu.saveSheetButtonLabel', {
defaultMessage: 'Save',
}),
description: i18n.translate('timelion.topNavMenu.saveSheetButtonAriaLabel', {
defaultMessage: 'Save Sheet',
}),
run: () => {
$scope.$evalAsync(() => $scope.toggleMenu('showSave'));
},
testId: 'timelionSaveButton',
};
const deleteSheetAction = {
id: 'delete',
label: i18n.translate('timelion.topNavMenu.deleteSheetButtonLabel', {
defaultMessage: 'Delete',
}),
description: i18n.translate('timelion.topNavMenu.deleteSheetButtonAriaLabel', {
defaultMessage: 'Delete current sheet',
}),
disableButton: function () {
return !savedSheet.id;
},
run: function () {
const title = savedSheet.title;
function doDelete() {
savedSheet
.delete()
.then(() => {
deps.core.notifications.toasts.addSuccess(
i18n.translate('timelion.topNavMenu.delete.modal.successNotificationText', {
defaultMessage: `Deleted '{title}'`,
values: { title },
})
);
history.push('/');
})
.catch((error) => addFatalError(deps.core.fatalErrors, error, location));
}
const confirmModalOptions = {
confirmButtonText: i18n.translate(
'timelion.topNavMenu.delete.modal.confirmButtonLabel',
{
defaultMessage: 'Delete',
}
),
title: i18n.translate('timelion.topNavMenu.delete.modalTitle', {
defaultMessage: `Delete Timelion sheet '{title}'?`,
values: { title },
}),
};
$scope.$evalAsync(() => {
deps.core.overlays
.openConfirm(
i18n.translate('timelion.topNavMenu.delete.modal.warningText', {
defaultMessage: `You can't recover deleted sheets.`,
}),
confirmModalOptions
)
.then((isConfirmed) => {
if (isConfirmed) {
doDelete();
}
});
});
},
testId: 'timelionDeleteButton',
};
const openSheetAction = {
id: 'open',
label: i18n.translate('timelion.topNavMenu.openSheetButtonLabel', {
defaultMessage: 'Open',
}),
description: i18n.translate('timelion.topNavMenu.openSheetButtonAriaLabel', {
defaultMessage: 'Open Sheet',
}),
run: () => {
$scope.$evalAsync(() => $scope.toggleMenu('showLoad'));
},
testId: 'timelionOpenButton',
};
const optionsAction = {
id: 'options',
label: i18n.translate('timelion.topNavMenu.optionsButtonLabel', {
defaultMessage: 'Options',
}),
description: i18n.translate('timelion.topNavMenu.optionsButtonAriaLabel', {
defaultMessage: 'Options',
}),
run: () => {
$scope.$evalAsync(() => $scope.toggleMenu('showOptions'));
},
testId: 'timelionOptionsButton',
};
const helpAction = {
id: 'help',
label: i18n.translate('timelion.topNavMenu.helpButtonLabel', {
defaultMessage: 'Help',
}),
description: i18n.translate('timelion.topNavMenu.helpButtonAriaLabel', {
defaultMessage: 'Help',
}),
run: () => {
$scope.$evalAsync(() => $scope.toggleMenu('showHelp'));
},
testId: 'timelionDocsButton',
};
if (deps.core.application.capabilities.timelion.save) {
return [
newSheetAction,
addSheetAction,
saveSheetAction,
deleteSheetAction,
openSheetAction,
optionsAction,
helpAction,
];
}
return [newSheetAction, addSheetAction, openSheetAction, optionsAction, helpAction];
}
let refresher;
const setRefreshData = function () {
if (refresher) $timeout.cancel(refresher);
const interval = timefilter.getRefreshInterval();
if (interval.value > 0 && !interval.pause) {
function startRefresh() {
refresher = $timeout(function () {
if (!$scope.running) $scope.search();
startRefresh();
}, interval.value);
}
startRefresh();
}
};
const init = function () {
$scope.running = false;
$scope.search();
setRefreshData();
$scope.model = {
timeRange: timefilter.getTime(),
refreshInterval: timefilter.getRefreshInterval(),
};
const unsubscribeStateUpdates = stateContainer.subscribe((state) => {
const clonedState = _.cloneDeep(state);
$scope.updatedSheets.forEach((updatedSheet) => {
clonedState.sheet[updatedSheet.id] = updatedSheet.expression;
});
$scope.state = clonedState;
$scope.opts.state = clonedState;
$scope.expression = _.clone($scope.state.sheet[$scope.state.selected]);
$scope.search();
});
timefilter.getFetch$().subscribe($scope.search);
$scope.opts = {
saveExpression: saveExpression,
saveSheet: saveSheet,
savedSheet: savedSheet,
state: _.cloneDeep(stateContainer.getState()),
search: $scope.search,
dontShowHelp: function () {
deps.core.uiSettings.set('timelion:showTutorial', false);
$scope.setPage(0);
$scope.closeMenus();
},
};
$scope.$watch('opts.state.rows', function (newRow) {
const state = stateContainer.getState();
if (state.rows !== newRow) {
stateContainer.transitions.set('rows', newRow);
}
});
$scope.$watch('opts.state.columns', function (newColumn) {
const state = stateContainer.getState();
if (state.columns !== newColumn) {
stateContainer.transitions.set('columns', newColumn);
}
});
$scope.menus = {
showHelp: false,
showSave: false,
showLoad: false,
showOptions: false,
};
$scope.toggleMenu = (menuName) => {
const curState = $scope.menus[menuName];
$scope.closeMenus();
$scope.menus[menuName] = !curState;
};
$scope.closeMenus = () => {
_.forOwn($scope.menus, function (value, key) {
$scope.menus[key] = false;
});
};
$scope.$on('$destroy', () => {
stopSyncingQueryServiceStateWithUrl();
unsubscribeStateUpdates();
stopStateSync();
});
};
$scope.onTimeUpdate = function ({ dateRange }) {
$scope.model.timeRange = {
...dateRange,
};
timefilter.setTime(dateRange);
if (!$scope.running) $scope.search();
};
$scope.onRefreshChange = function ({ isPaused, refreshInterval }) {
$scope.model.refreshInterval = {
pause: isPaused,
value: refreshInterval,
};
timefilter.setRefreshInterval({
pause: isPaused,
value: refreshInterval ? refreshInterval : $scope.refreshInterval.value,
});
setRefreshData();
};
$scope.$watch(
function () {
return savedSheet.lastSavedTitle;
},
function (newTitle) {
if (savedSheet.id && newTitle) {
deps.core.chrome.docTitle.change(newTitle);
}
}
);
$scope.$watch('expression', function (newExpression) {
const state = stateContainer.getState();
if (state.sheet[state.selected] !== newExpression) {
const updatedSheet = $scope.updatedSheets.find(
(updatedSheet) => updatedSheet.id === state.selected
);
if (updatedSheet) {
updatedSheet.expression = newExpression;
} else {
$scope.updatedSheets.push({
id: state.selected,
expression: newExpression,
});
}
}
});
$scope.toggle = function (property) {
$scope[property] = !$scope[property];
};
$scope.changeInterval = function (interval) {
$scope.currentInterval = interval;
};
$scope.updateChart = function () {
const state = stateContainer.getState();
const newSheet = _.clone(state.sheet);
if ($scope.updatedSheets.length) {
$scope.updatedSheets.forEach((updatedSheet) => {
newSheet[updatedSheet.id] = updatedSheet.expression;
});
$scope.updatedSheets = [];
}
stateContainer.transitions.updateState({
interval: $scope.currentInterval ? $scope.currentInterval : state.interval,
sheet: newSheet,
});
};
$scope.newSheet = function () {
history.push('/');
};
$scope.removeSheet = function (removedIndex) {
const state = stateContainer.getState();
const newSheet = state.sheet.filter((el, index) => index !== removedIndex);
$scope.updatedSheets = $scope.updatedSheets.filter((el) => el.id !== removedIndex);
stateContainer.transitions.updateState({
sheet: newSheet,
selected: removedIndex ? removedIndex - 1 : removedIndex,
});
};
$scope.newCell = function () {
const state = stateContainer.getState();
const newSheet = [...state.sheet, defaultExpression];
stateContainer.transitions.updateState({ sheet: newSheet, selected: newSheet.length - 1 });
};
$scope.setActiveCell = function (cell) {
const state = stateContainer.getState();
if (state.selected !== cell) {
stateContainer.transitions.updateState({ sheet: $scope.state.sheet, selected: cell });
}
};
$scope.search = function () {
$scope.running = true;
const state = stateContainer.getState();
// parse the time range client side to make sure it behaves like other charts
const timeRangeBounds = timefilter.getBounds();
const httpResult = $http
.post('../api/timelion/run', {
sheet: state.sheet,
time: _.assignIn(
{
from: timeRangeBounds.min,
to: timeRangeBounds.max,
},
{
interval: state.interval,
timezone: timezone,
}
),
})
.then((resp) => resp.data)
.catch((resp) => {
throw resp.data;
});
httpResult
.then(function (resp) {
$scope.stats = resp.stats;
$scope.sheet = resp.sheet;
_.forEach(resp.sheet, function (cell) {
if (cell.exception && cell.plot !== state.selected) {
stateContainer.transitions.set('selected', cell.plot);
}
});
$scope.running = false;
})
.catch(function (resp) {
$scope.sheet = [];
$scope.running = false;
const err = new Error(resp.message);
err.stack = resp.stack;
deps.core.notifications.toasts.addError(err, {
title: i18n.translate('timelion.searchErrorTitle', {
defaultMessage: 'Timelion request error',
}),
});
});
};
$scope.safeSearch = _.debounce($scope.search, 500);
function saveSheet() {
const state = stateContainer.getState();
savedSheet.timelion_sheet = state.sheet;
savedSheet.timelion_interval = state.interval;
savedSheet.timelion_columns = state.columns;
savedSheet.timelion_rows = state.rows;
savedSheet.save().then(function (id) {
if (id) {
deps.core.notifications.toasts.addSuccess({
title: i18n.translate('timelion.saveSheet.successNotificationText', {
defaultMessage: `Saved sheet '{title}'`,
values: { title: savedSheet.title },
}),
'data-test-subj': 'timelionSaveSuccessToast',
});
if (savedSheet.id !== $routeParams.id) {
history.push(`/${savedSheet.id}`);
}
}
});
}
async function saveExpression(title) {
const vis = await deps.plugins.visualizations.createVis('timelion', {
title,
params: {
expression: $scope.state.sheet[$scope.state.selected],
interval: $scope.state.interval,
},
});
const state = deps.plugins.visualizations.convertFromSerializedVis(vis.serialize());
const visSavedObject = await savedVisualizations.get();
Object.assign(visSavedObject, state);
const id = await visSavedObject.save();
if (id) {
deps.core.notifications.toasts.addSuccess(
i18n.translate('timelion.saveExpression.successNotificationText', {
defaultMessage: `Saved expression '{title}'`,
values: { title: state.title },
})
);
}
}
init();
}
app.config(function ($routeProvider) {
$routeProvider
.when('/:id?', {
template: rootTemplate,
reloadOnSearch: false,
k7Breadcrumbs: ($injector, $route) =>
$injector.invoke(
$route.current.params.id ? getSavedSheetBreadcrumbs : getCreateBreadcrumbs
),
badge: () => {
if (deps.core.application.capabilities.timelion.save) {
return undefined;
}
return {
text: i18n.translate('timelion.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n.translate('timelion.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save Timelion sheets',
}),
iconType: 'glasses',
};
},
resolve: {
savedSheet: function (savedSheets, $route) {
return savedSheets
.get($route.current.params.id)
.then((savedSheet) => {
if ($route.current.params.id) {
deps.core.chrome.recentlyAccessed.add(
savedSheet.getFullPath(),
savedSheet.title,
savedSheet.id
);
}
return savedSheet;
})
.catch();
},
},
})
.otherwise('/');
});
}

View file

@ -1,129 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import './index.scss';
import { EuiIcon } from '@elastic/eui';
import angular, { IModule } from 'angular';
// required for `ngSanitize` angular module
import 'angular-sanitize';
// required for ngRoute
import 'angular-route';
import 'angular-sortable-view';
import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular';
import {
IUiSettingsClient,
CoreStart,
PluginInitializerContext,
AppMountParameters,
} from 'kibana/public';
import { getTimeChart } from './panels/timechart/timechart';
import { Panel } from './panels/panel';
import { configureAppAngularModule } from '../../kibana_legacy/public';
import { TimelionPluginStartDependencies } from './plugin';
import { DataPublicPluginStart } from '../../data/public';
// @ts-ignore
import { initTimelionApp } from './app';
export interface RenderDeps {
pluginInitializerContext: PluginInitializerContext;
mountParams: AppMountParameters;
core: CoreStart;
plugins: TimelionPluginStartDependencies;
timelionPanels: Map<string, Panel>;
}
export interface TimelionVisualizationDependencies {
uiSettings: IUiSettingsClient;
timelionPanels: Map<string, Panel>;
data: DataPublicPluginStart;
$rootScope: any;
$compile: any;
}
let angularModuleInstance: IModule | null = null;
export const renderApp = (deps: RenderDeps) => {
if (!angularModuleInstance) {
angularModuleInstance = createLocalAngularModule(deps);
// global routing stuff
configureAppAngularModule(
angularModuleInstance,
{ core: deps.core, env: deps.pluginInitializerContext.env },
true
);
initTimelionApp(angularModuleInstance, deps);
}
const $injector = mountTimelionApp(deps.mountParams.appBasePath, deps.mountParams.element, deps);
return () => {
$injector.get('$rootScope').$destroy();
};
};
function registerPanels(dependencies: TimelionVisualizationDependencies) {
const timeChartPanel: Panel = getTimeChart(dependencies);
dependencies.timelionPanels.set(timeChartPanel.name, timeChartPanel);
}
const mainTemplate = (basePath: string) => `<div ng-view class="timelionAppContainer">
<base href="${basePath}" />
</div>`;
const moduleName = 'app/timelion';
const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react', 'angular-sortable-view'];
function mountTimelionApp(appBasePath: string, element: HTMLElement, deps: RenderDeps) {
const mountpoint = document.createElement('div');
mountpoint.setAttribute('class', 'timelionAppContainer');
// eslint-disable-next-line no-unsanitized/property
mountpoint.innerHTML = mainTemplate(appBasePath);
// bootstrap angular into detached element and attach it later to
// make angular-within-angular possible
const $injector = angular.bootstrap(mountpoint, [moduleName]);
registerPanels({
uiSettings: deps.core.uiSettings,
timelionPanels: deps.timelionPanels,
data: deps.plugins.data,
$rootScope: $injector.get('$rootScope'),
$compile: $injector.get('$compile'),
});
element.appendChild(mountpoint);
return $injector;
}
function createLocalAngularModule(deps: RenderDeps) {
createLocalI18nModule();
createLocalIconModule();
const dashboardAngularModule = angular.module(moduleName, [
...thirdPartyAngularDependencies,
'app/timelion/I18n',
'app/timelion/icon',
]);
return dashboardAngularModule;
}
function createLocalIconModule() {
angular
.module('app/timelion/icon', ['react'])
.directive('icon', (reactDirective) => reactDirective(EuiIcon));
}
function createLocalI18nModule() {
angular
.module('app/timelion/I18n', [])
.provider('i18n', I18nProvider)
.filter('i18n', i18nFilter)
.directive('i18nId', i18nDirective);
}

View file

@ -1,37 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
const ROOT_BREADCRUMB = {
text: i18n.translate('timelion.breadcrumbs.root', {
defaultMessage: 'Timelion',
}),
href: '#',
};
export function getCreateBreadcrumbs() {
return [
ROOT_BREADCRUMB,
{
text: i18n.translate('timelion.breadcrumbs.create', {
defaultMessage: 'Create',
}),
},
];
}
export function getSavedSheetBreadcrumbs($route) {
const { savedSheet } = $route.current.locals;
return [
ROOT_BREADCRUMB,
{
text: savedSheet.title,
},
];
}

View file

@ -1,42 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiSpacer, EuiCallOut, EuiLink } from '@elastic/eui';
import React from 'react';
import { DocLinksStart } from '../../../../core/public';
export const TimelionDeprecation = ({ links }: DocLinksStart) => {
const timelionDeprecationLink = links.visualize.timelionDeprecation;
return (
<>
<EuiCallOut
title={
<FormattedMessage
id="timelion.deprecation.message"
defaultMessage="Deprecated since 7.0, the Timelion app will be removed in 7.16. To continue using your Timelion worksheets, {timeLionDeprecationLink}."
values={{
timeLionDeprecationLink: (
<EuiLink href={timelionDeprecationLink} target="_blank" external>
<FormattedMessage
id="timelion.deprecation.here"
defaultMessage="migrate them to a dashboard."
/>
</EuiLink>
),
}}
/>
}
color="warning"
iconType="alert"
size="s"
/>
<EuiSpacer size="s" />
</>
);
};

View file

@ -1,31 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { TimelionDeprecation } from './timelion_deprecation';
export function initTimelionTDeprecationDirective(app, deps) {
app.directive('timelionDeprecation', function (reactDirective) {
return reactDirective(
() => {
return (
<deps.core.i18n.Context>
<TimelionDeprecation links={deps.core.docLinks.links} />
</deps.core.i18n.Context>
);
},
[],
{
restrict: 'E',
scope: {
docLinks: '=',
},
}
);
});
}

View file

@ -1,48 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
export function initTimelionTopNavDirective(app, deps) {
app.directive('timelionTopNav', function (reactDirective) {
return reactDirective(
(props) => {
const { TopNavMenu } = deps.plugins.navigation.ui;
return (
<deps.core.i18n.Context>
<TopNavMenu
appName="timelion"
showTopNavMenu
config={props.topNavMenu}
setMenuMountPoint={deps.mountParams.setHeaderActionMenu}
onQuerySubmit={props.onTimeUpdate}
screenTitle="timelion"
showDatePicker
showFilterBar={false}
showQueryInput={false}
showSaveQuery={false}
showSearchBar
useDefaultBehaviors
/>
</deps.core.i18n.Context>
);
},
[
['topNavMenu', { watchDepth: 'reference' }],
['onTimeUpdate', { watchDepth: 'reference' }],
],
{
restrict: 'E',
scope: {
topNavMenu: '=',
onTimeUpdate: '=',
},
}
);
});
}

View file

@ -1,48 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import PropTypes from 'prop-types';
import React from 'react';
import { EuiTabs, EuiTab } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
function handleClick(activateTab, tabName) {
activateTab(tabName);
}
export function TimelionHelpTabs(props) {
return (
<EuiTabs size="s">
<EuiTab
isSelected={props.activeTab === 'funcref'}
onClick={() => handleClick(props.activateTab, 'funcref')}
>
<FormattedMessage
id="timelion.help.mainPage.functionReferenceTitle"
defaultMessage="Function reference"
/>
</EuiTab>
<EuiTab
isSelected={props.activeTab === 'keyboardtips'}
onClick={() => handleClick(props.activateTab, 'keyboardtips')}
>
<FormattedMessage
id="timelion.help.mainPage.keyboardTipsTitle"
defaultMessage="Keyboard tips"
/>
</EuiTab>
</EuiTabs>
);
}
TimelionHelpTabs.propTypes = {
activeTab: PropTypes.string,
activateTab: PropTypes.func,
};

View file

@ -1,32 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { TimelionHelpTabs } from './timelionhelp_tabs';
export function initTimelionTabsDirective(app, deps) {
app.directive('timelionHelpTabs', function (reactDirective) {
return reactDirective(
(props) => {
return (
<deps.core.i18n.Context>
<TimelionHelpTabs {...props} />
</deps.core.i18n.Context>
);
},
[['activeTab'], ['activateTab', { watchDepth: 'reference' }]],
{
restrict: 'E',
scope: {
activeTab: '=',
activateTab: '=',
},
}
);
});
}

View file

@ -1,83 +0,0 @@
.form-control {
@include euiFontSizeS;
display: block;
width: 100%;
height: $euiFormControlCompressedHeight;
padding: $euiSizeXS $euiSizeM;
border: $euiBorderThin;
background-color: $euiFormBackgroundColor;
color: $euiTextColor;
border-radius: $euiBorderRadius;
cursor: pointer;
&:not([type='range']) {
appearance: none;
}
&:focus {
border-color: $euiColorPrimary;
outline: none;
box-shadow: none;
}
}
select.form-control { // stylelint-disable-line selector-no-qualifying-type
// Makes the select arrow similar to EUI's arrowDown icon
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"%3E%3Cpath fill="#{hexToRGB($euiTextColor)}" d="M13.0688508,5.15725038 L8.38423975,9.76827428 C8.17054415,9.97861308 7.82999214,9.97914095 7.61576025,9.76827428 L2.93114915,5.15725038 C2.7181359,4.94758321 2.37277319,4.94758321 2.15975994,5.15725038 C1.94674669,5.36691756 1.94674669,5.70685522 2.15975994,5.9165224 L6.84437104,10.5275463 C7.48517424,11.1582836 8.51644979,11.1566851 9.15562896,10.5275463 L13.8402401,5.9165224 C14.0532533,5.70685522 14.0532533,5.36691756 13.8402401,5.15725038 C13.6272268,4.94758321 13.2818641,4.94758321 13.0688508,5.15725038 Z"/%3E%3C/svg%3E');
background-size: $euiSize;
background-repeat: no-repeat;
background-position: calc(100% - #{$euiSizeS});
padding-right: $euiSizeXL;
}
.fullWidth {
width: 100%;
}
.timDropdownWarning {
margin-bottom: $euiSize;
padding: $euiSizeXS $euiSizeS;
color: $euiColorDarkestShade;
border-left: solid 2px $euiColorDanger;
font-size: $euiSizeM;
}
.timFormCheckbox {
display: flex;
align-items: center;
line-height: 1.5;
position: relative;
}
.timFormCheckbox__input {
appearance: none;
background-color: $euiColorLightestShade;
border: 1px solid $euiColorLightShade;
border-radius: $euiSizeXS;
width: $euiSize;
height: $euiSize;
font-size: $euiSizeM;
transition: background-color .1s linear;
}
.timFormCheckbox__input:checked {
border-color: $euiColorPrimary;
background-color: $euiColorPrimary;
}
.timFormCheckbox__icon {
position: absolute;
top: 0;
left: 2px;
}
.timFormTextarea {
padding: $euiSizeXS $euiSizeM;
font-size: $euiSize;
line-height: 1.5;
color: $euiColorDarkestShade;
background-color: $euiFormBackgroundColor;
border: 1px solid $euiColorLightShade;
border-radius: $euiSizeXS;
transition: border-color .1s linear;
}

View file

@ -1,7 +0,0 @@
@import './timelion_expression_input';
@import './cells/index';
@import './timelion_expression_suggestions/index';
@import './timelion_help/index';
@import './timelion_interval/index';
@import './saved_object_finder';
@import './form';

View file

@ -1,132 +0,0 @@
.list-group-menu {
&.select-mode a {
outline: none;
color: tintOrShade($euiColorPrimary, 10%, 10%);
}
.list-group-menu-item {
list-style: none;
color: tintOrShade($euiColorPrimary, 10%, 10%);
&.active {
font-weight: bold;
background-color: $euiColorLightShade;
}
&:hover {
background-color: tintOrShade($euiColorPrimary, 90%, 90%);
}
li {
list-style: none;
color: tintOrShade($euiColorPrimary, 10%, 10%);
}
}
}
saved-object-finder {
.timSearchBar {
display: flex;
align-items: center;
}
.timSearchBar__section {
position: relative;
margin-right: $euiSize;
flex: 1;
}
.timSearchBar__icon {
position: absolute;
top: $euiSizeS;
left: $euiSizeS;
font-size: $euiSize;
color: $euiColorDarkShade;
}
.timSearchBar__input {
padding: $euiSizeS $euiSizeM;
color: $euiColorDarkestShade;
background-color: $euiColorEmptyShade;
border: 1px solid $euiColorLightShade;
border-radius: $euiSizeXS;
transition: border-color .1s linear;
padding-left: $euiSizeXL;
width: 100%;
font-size: $euiSize;
}
.timSearchBar__pagecount {
font-size: $euiSize;
color: $euiColorDarkShade;
}
.list-sort-button {
border-top-left-radius: 0;
border-top-right-radius: 0;
border: none;
padding: $euiSizeS $euiSize;
font-weight: $euiFontWeightRegular;
background-color: $euiColorLightestShade;
margin-top: $euiSize;
}
.li-striped {
li {
border: none;
}
li:nth-child(even) {
background-color: $euiColorLightestShade;
}
li:nth-child(odd) {
background-color: $euiColorEmptyShade;
}
.paginate-heading {
font-weight: $euiFontWeightRegular;
color: $euiColorDarkestShade;
}
.list-group-item {
padding: $euiSizeS $euiSize;
ul {
padding: 0;
display: flex;
flex-direction: row;
.finder-type {
margin-right: $euiSizeS;
}
}
a {
display: block;
color: $euiColorPrimary;
i {
color: shade($euiColorPrimary, 10%);
margin-right: $euiSizeS;
}
}
&:first-child {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
&.list-group-no-results p {
margin-bottom: 0;
}
}
}
paginate {
paginate-controls {
margin: $euiSize;
}
}
}

View file

@ -1,15 +0,0 @@
/**
* 1. Anchor suggestions beneath input.
* 2. Allow for option of positioning suggestions absolutely.
*/
.timExpressionInput__container {
flex: 1 1 auto;
display: flex;
flex-direction: column; /* 1 */
position: relative; /* 2 */
}
.timExpressionInput {
min-height: 70px; // Matches buttons on the right with new vertical rhythm sizing
}

View file

@ -1,61 +0,0 @@
.timCell {
display: inline-block;
cursor: pointer;
position: relative;
box-sizing: border-box;
border: 2px dashed transparent;
// sass-lint:disable-block no-important
padding-left: 0 !important;
padding-right: 0 !important;
margin-bottom: $euiSizeM;
&.active {
border-color: $euiColorLightShade;
}
}
.timCell.running {
opacity: .5;
}
.timCell__actions {
position: absolute;
bottom: $euiSizeXS;
left: $euiSizeXS;
> .timCell__action,
> .timCell__id {
@include euiFontSizeXS;
font-weight: $euiFontWeightBold;
color: $euiColorMediumShade;
display: inline-block;
text-align: center;
width: $euiSizeL;
height: $euiSizeL;
border-radius: $euiSizeL / 2;
border: $euiBorderThin;
background-color: $euiColorLightestShade;
z-index: $euiZLevel1;
}
> .timCell__action {
opacity: 0;
&:focus {
opacity: 1;
}
&:hover,
&:focus {
color: $euiTextColor;
border-color: $euiColorMediumShade;
background-color: $euiColorLightShade;
}
}
}
.timCell:hover {
.timCell__action {
opacity: 1;
}
}

View file

@ -1 +0,0 @@
@import './cells';

View file

@ -1,52 +0,0 @@
<div sv-root
sv-part="state.sheet"
sv-on-sort="dropCell($item, $partFrom, $partTo, $indexFrom, $indexTo)"
>
<div sv-element
ng-repeat="cell in state.sheet track by $index"
class="timCell col-md-{{12 / state.columns}} col-sm-12 col-xs-12"
timelion-grid timelion-grid-rows="state.rows"
ng-click="onSelect($index)"
ng-class="{active: $index === state.selected}"
kbn-accessible-click
aria-label="Timelion chart {{$index + 1}}"
aria-current="{{$index === state.selected}}"
>
<div chart="sheet[$index]" class="timChart" search="onSearch" interval="state.interval"></div>
<div class="timCell__actions">
<div class="timCell__id"><span>{{$index + 1}}</span></div>
<button
class="timCell__action"
ng-click="removeCell($index)"
tooltip="{{ ::'timelion.cells.actions.removeTooltip' | i18n: { defaultMessage: 'Remove' } }}"
tooltip-append-to-body="1"
aria-label="{{ ::'timelion.cells.actions.removeAriaLabel' | i18n: { defaultMessage: 'Remove chart' } }}"
>
<icon type="'cross'"></icon>
</button>
<button
class="timCell__action"
tooltip="{{ ::'timelion.cells.actions.reorderTooltip' | i18n: { defaultMessage: 'Drag to reorder' } }}"
tooltip-append-to-body="1"
sv-handle
aria-label="{{ ::'timelion.cells.actions.reorderAriaLabel' | i18n: { defaultMessage: 'Drag to reorder' } }}"
tabindex="-1"
>
<icon type="'grab'"></icon>
</button>
<button
class="timCell__action"
ng-click="transient.fullscreen = true"
tooltip="{{ ::'timelion.cells.actions.fullscreenTooltip' | i18n: { defaultMessage: 'Full screen' } }}"
tooltip-append-to-body="1"
aria-label="{{ ::'timelion.cells.actions.fullscreenAriaLabel' | i18n: { defaultMessage: 'Full screen chart' } }}"
>
<icon type="'expandMini'"></icon>
</button>
</div>
</div>
</div>

View file

@ -1,41 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { move } from './collection';
import { initTimelionGridDirective } from '../timelion_grid';
import html from './cells.html';
export function initCellsDirective(app) {
initTimelionGridDirective(app);
app.directive('timelionCells', function () {
return {
restrict: 'E',
scope: {
sheet: '=',
state: '=',
transient: '=',
onSearch: '=',
onSelect: '=',
onRemoveSheet: '=',
},
template: html,
link: function ($scope) {
$scope.removeCell = function (index) {
$scope.onRemoveSheet(index);
};
$scope.dropCell = function (item, partFrom, partTo, indexFrom, indexTo) {
move($scope.sheet, indexFrom, indexTo);
$scope.onSelect(indexTo);
};
},
};
});
}

View file

@ -1,65 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
/**
* move an obj either up or down in the collection by
* injecting it either before/after the prev/next obj that
* satisfied the qualifier
*
* or, just from one index to another...
*
* @param {array} objs - the list to move the object within
* @param {number|any} obj - the object that should be moved, or the index that the object is currently at
* @param {number|boolean} below - the index to move the object to, or whether it should be moved up or down
* @param {function} qualifier - a lodash-y callback, object = _.where, string = _.pluck
* @return {array} - the objs argument
*/
export function move(
objs: any[],
obj: object | number,
below: number | boolean,
qualifier?: ((object: object, index: number) => any) | Record<string, any> | string
): object[] {
const origI = _.isNumber(obj) ? obj : objs.indexOf(obj);
if (origI === -1) {
return objs;
}
if (_.isNumber(below)) {
// move to a specific index
objs.splice(below, 0, objs.splice(origI, 1)[0]);
return objs;
}
below = !!below;
qualifier = qualifier && _.iteratee(qualifier);
const above = !below;
const finder = below ? _.findIndex : _.findLastIndex;
// find the index of the next/previous obj that meets the qualifications
const targetI = finder(objs, (otherAgg, otherI) => {
if (below && otherI <= origI) {
return;
}
if (above && otherI >= origI) {
return;
}
return Boolean(_.isFunction(qualifier) && qualifier(otherAgg, otherI));
});
if (targetI === -1) {
return objs;
}
// place the obj at it's new index
objs.splice(targetI, 0, objs.splice(origI, 1)[0]);
return objs;
}

View file

@ -1,55 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
export function Chart(timelionPanels) {
return {
restrict: 'A',
scope: {
seriesList: '=chart', // The flot object, data, config and all
search: '=', // The function to execute to kick off a search
interval: '=', // Required for formatting x-axis ticks
rerenderTrigger: '=',
},
link: function ($scope, $elem) {
let panelScope = $scope.$new(true);
function render() {
panelScope.$destroy();
if (!$scope.seriesList) return;
$scope.seriesList.render = $scope.seriesList.render || {
type: 'timechart',
};
const panelSchema = timelionPanels.get($scope.seriesList.render.type);
if (!panelSchema) {
$elem.text(
i18n.translate('timelion.chart.seriesList.noSchemaWarning', {
defaultMessage: 'No such panel type: {renderType}',
values: { renderType: $scope.seriesList.render.type },
})
);
return;
}
panelScope = $scope.$new(true);
panelScope.seriesList = $scope.seriesList;
panelScope.interval = $scope.interval;
panelScope.search = $scope.search;
panelSchema.render(panelScope, $elem);
}
$scope.$watchGroup(['seriesList', 'rerenderTrigger'], render);
},
};
}

View file

@ -1,39 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import $ from 'jquery';
export function initFixedElementDirective(app) {
app.directive('fixedElementRoot', function () {
return {
restrict: 'A',
link: function ($elem) {
let fixedAt;
$(window).bind('scroll', function () {
const fixed = $('[fixed-element]', $elem);
const body = $('[fixed-element-body]', $elem);
const top = fixed.offset().top;
if ($(window).scrollTop() > top) {
// This is a gross hack, but its better than it was. I guess
fixedAt = $(window).scrollTop();
fixed.addClass(fixed.attr('fixed-element'));
body.addClass(fixed.attr('fixed-element-body'));
body.css({ top: fixed.height() });
}
if ($(window).scrollTop() < fixedAt) {
fixed.removeClass(fixed.attr('fixed-element'));
body.removeClass(fixed.attr('fixed-element-body'));
body.removeAttr('style');
}
});
},
};
});
}

View file

@ -1,14 +0,0 @@
<div class="timCell col-md-12 col-sm-12 col-xs-12" timelion-grid timelion-grid-rows="1">
<div chart="series" class="timChart" search="onSearch" interval="state.interval"></div>
<div class="timCell__actions">
<button
class="timCell__action"
ng-click="transient.fullscreen = false"
tooltip="{{ ::'timelion.fullscreen.exitTooltip' | i18n: { defaultMessage: 'Exit full screen' } }}"
tooltip-append-to-body="1"
aria-label="{{ ::'timelion.fullscreen.exitAriaLabel' | i18n: { defaultMessage: 'Exit full screen' } }}"
>
<icon type="'minimize'"></icon>
</button>
</div>
</div>

View file

@ -1,25 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import html from './fullscreen.html';
export function initFullscreenDirective(app) {
app.directive('timelionFullscreen', function () {
return {
restrict: 'E',
scope: {
expression: '=',
series: '=',
state: '=',
transient: '=',
onSearch: '=',
},
template: html,
};
});
}

View file

@ -1,24 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export function initInputFocusDirective(app) {
app.directive('inputFocus', function ($parse, $timeout) {
return {
restrict: 'A',
link: function ($scope, $elem, attrs) {
const isDisabled = attrs.disableInputFocus && $parse(attrs.disableInputFocus)($scope);
if (!isDisabled) {
$timeout(function () {
$elem.focus();
if (attrs.inputFocus === 'select') $elem.select();
});
}
},
};
});
}

View file

@ -1,110 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const keyMap: { [key: number]: string } = {
8: 'backspace',
9: 'tab',
13: 'enter',
16: 'shift',
17: 'ctrl',
18: 'alt',
19: 'pause',
20: 'capsLock',
27: 'escape',
32: 'space',
33: 'pageUp',
34: 'pageDown',
35: 'end',
36: 'home',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
45: 'insert',
46: 'delete',
48: '0',
49: '1',
50: '2',
51: '3',
52: '4',
53: '5',
54: '6',
55: '7',
56: '8',
57: '9',
65: 'a',
66: 'b',
67: 'c',
68: 'd',
69: 'e',
70: 'f',
71: 'g',
72: 'h',
73: 'i',
74: 'j',
75: 'k',
76: 'l',
77: 'm',
78: 'n',
79: 'o',
80: 'p',
81: 'q',
82: 'r',
83: 's',
84: 't',
85: 'u',
86: 'v',
87: 'w',
88: 'x',
89: 'y',
90: 'z',
91: 'leftWindowKey',
92: 'rightWindowKey',
93: 'selectKey',
96: '0',
97: '1',
98: '2',
99: '3',
100: '4',
101: '5',
102: '6',
103: '7',
104: '8',
105: '9',
106: 'multiply',
107: 'add',
109: 'subtract',
110: 'period',
111: 'divide',
112: 'f1',
113: 'f2',
114: 'f3',
115: 'f4',
116: 'f5',
117: 'f6',
118: 'f7',
119: 'f8',
120: 'f9',
121: 'f10',
122: 'f11',
123: 'f12',
144: 'numLock',
145: 'scrollLock',
186: 'semiColon',
187: 'equalSign',
188: 'comma',
189: 'dash',
190: 'period',
191: 'forwardSlash',
192: 'graveAccent',
219: 'openBracket',
220: 'backSlash',
221: 'closeBracket',
222: 'singleQuote',
224: 'meta',
};

View file

@ -1,112 +0,0 @@
<form
role="form"
>
<div class="timSearchBar">
<div class="timSearchBar__section">
<icon class="timSearchBar__icon" type="'search'"></icon>
<input
class="timSearchBar__input"
input-focus
disable-input-focus="disableAutoFocus"
ng-model="filter"
ng-attr-placeholder="{{ finder.getLabel() }} Filter..."
ng-keydown="finder.filterKeyDown($event)"
name="filter"
type="text"
autocomplete="off"
data-test-subj="savedObjectFinderSearchInput"
>
</div>
<div>
<p class="timSearchBar__pagecount"
i18n-id="timelion.savedObjectFinder.pageItemsFromHitCountDescription"
i18n-default-message="{pageFirstItem}-{pageLastItem} of {hitCount}"
i18n-values="{pageFirstItem, pageLastItem, hitCount: finder.hitCount}"
></p>
<div>
<button
class="timApp__button"
ng-if="onAddNew"
ng-click="onAddNew()"
data-test-subj="addNewSavedObjectLink"
i18n-id="timelion.savedObjectFinder.addNewItemButtonLabel"
i18n-default-message="Add new {item}"
i18n-values="{item: finder.properties.noun}"
i18n-description="{item} can be a type of object in Kibana, like 'visualization', 'dashboard', etc"
></button>
<button
class="timApp__button--secondary"
ng-if="!useLocalManagement"
ng-click="finder.manageObjects(finder.properties.name)"
i18n-id="timelion.savedObjectFinder.manageItemsButtonLabel"
i18n-default-message="Manage {items}"
i18n-values="{items: finder.properties.nouns}"
i18n-description="{items} can be a type of object in Kibana, like 'visualizations', 'dashboards', etc"
></button>
</div>
</div>
</div>
</form>
<paginate
list="finder.hits"
per-page="20"
>
<button
class="paginate-heading list-group-item list-sort-button"
ng-click="finder.sortHits(finder.hits)"
aria-live="assertive"
>
<span class="euiScreenReaderOnly"
i18n-id="timelion.savedObjectFinder.sortByButtonLabelScreenReaderOnly"
i18n-default-message="Sort by"
></span>
<span
i18n-id="timelion.savedObjectFinder.sortByButtonLabel"
i18n-default-message="Name"
></span>
<icon type="'sortUp'" ng-if="finder.isAscending"></icon>
<icon type="'sortDown'" ng-if="!finder.isAscending"></icon>
<span class="euiScreenReaderOnly"
ng-if="finder.isAscending"
i18n-id="timelion.savedObjectFinder.sortByButtonLabeAscendingScreenReaderOnly"
i18n-default-message="ascending"
></span>
<span class="euiScreenReaderOnly"
ng-if="!finder.isAscending"
i18n-id="timelion.savedObjectFinder.sortByButtonLabeDescendingScreenReaderOnly"
i18n-default-message="descending"
></span>
</span>
</button>
<ul class="li-striped list-group list-group-menu" ng-class="{'select-mode': finder.selector.enabled}">
<li
class="list-group-item list-group-menu-item"
ng-class="{'active': finder.selector.index === $index && finder.selector.enabled}"
ng-repeat="hit in page"
ng-keydown="finder.hitKeyDown($event, page, paginate)"
ng-click="finder.onChoose(hit, $event)">
<a ng-href="{{finder.makeUrl(hit)}}"
ng-blur="finder.hitBlur($event)"
ng-click="finder.preventClick($event)">
<icon aria-hidden="true" class="finder-type" ng-if="hit.icon" ng-class="hit.icon"></icon>
<icon type="'beaker'" ng-if="hit.type.shouldMarkAsExperimentalInUI()"></icon>
<span>{{hit.title}}</span>
<p ng-if="hit.description" ng-bind="hit.description"></p>
</a>
</li>
<li
class="list-group-item list-group-no-results"
ng-if="finder.hits.length === 0"
>
<p i18n-id="timelion.savedObjectFinder.noMatchesFoundDescription"
i18n-default-message="No matching {items} found."
i18n-values="{items: finder.properties.nouns}"
i18n-description="{items} can be a type of object in Kibana, like 'visualizations', 'dashboards', etc"
></p>
</li>
</ul>
</paginate>

View file

@ -1,302 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
import rison from 'rison-node';
import savedObjectFinderTemplate from './saved_object_finder.html';
import { keyMap } from './key_map';
import {
PaginateControlsDirectiveProvider,
PaginateDirectiveProvider,
} from '../../../kibana_legacy/public';
import { PER_PAGE_SETTING } from '../../../saved_objects/public';
import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../visualizations/public';
export function initSavedObjectFinderDirective(app, savedSheetLoader, uiSettings) {
app
.directive('paginate', PaginateDirectiveProvider)
.directive('paginateControls', PaginateControlsDirectiveProvider)
.directive('savedObjectFinder', function () {
return {
restrict: 'E',
scope: {
type: '@',
// optional make-url attr, sets the userMakeUrl in our scope
userMakeUrl: '=?makeUrl',
// optional on-choose attr, sets the userOnChoose in our scope
userOnChoose: '=?onChoose',
// optional useLocalManagement attr, removes link to management section
useLocalManagement: '=?useLocalManagement',
/**
* @type {function} - an optional function. If supplied an `Add new X` button is shown
* and this function is called when clicked.
*/
onAddNew: '=',
/**
* @{type} boolean - set this to true, if you don't want the search box above the
* table to automatically gain focus once loaded
*/
disableAutoFocus: '=',
},
template: savedObjectFinderTemplate,
controllerAs: 'finder',
controller: function ($scope, $element, $location, history) {
const self = this;
// the text input element
const $input = $element.find('input[ng-model=filter]');
// The number of items to show in the list
$scope.perPage = uiSettings.get(PER_PAGE_SETTING);
// the list that will hold the suggestions
const $list = $element.find('ul');
// the current filter string, used to check that returned results are still useful
let currentFilter = $scope.filter;
// the most recently entered search/filter
let prevSearch;
// the list of hits, used to render display
self.hits = [];
self.service = savedSheetLoader;
self.properties = self.service.loaderProperties;
filterResults();
/**
* Boolean that keeps track of whether hits are sorted ascending (true)
* or descending (false) by title
* @type {Boolean}
*/
self.isAscending = true;
/**
* Sorts saved object finder hits either ascending or descending
* @param {Array} hits Array of saved finder object hits
* @return {Array} Array sorted either ascending or descending
*/
self.sortHits = function (hits) {
self.isAscending = !self.isAscending;
self.hits = self.isAscending
? _.sortBy(hits, ['title'])
: _.sortBy(hits, ['title']).reverse();
};
/**
* Passed the hit objects and will determine if the
* hit should have a url in the UI, returns it if so
* @return {string|null} - the url or nothing
*/
self.makeUrl = function (hit) {
if ($scope.userMakeUrl) {
return $scope.userMakeUrl(hit);
}
if (!$scope.userOnChoose) {
return hit.url;
}
return '#';
};
self.preventClick = function ($event) {
$event.preventDefault();
};
/**
* Called when a hit object is clicked, can override the
* url behavior if necessary.
*/
self.onChoose = function (hit, $event) {
if ($scope.userOnChoose) {
$scope.userOnChoose(hit, $event);
}
const url = self.makeUrl(hit);
if (!url || url === '#' || url.charAt(0) !== '#') return;
$event.preventDefault();
history.push(url.substr(1));
};
$scope.$watch('filter', function (newFilter) {
// ensure that the currentFilter changes from undefined to ''
// which triggers
currentFilter = newFilter || '';
filterResults();
});
$scope.pageFirstItem = 0;
$scope.pageLastItem = 0;
$scope.onPageChanged = (page) => {
$scope.pageFirstItem = page.firstItem;
$scope.pageLastItem = page.lastItem;
};
//manages the state of the keyboard selector
self.selector = {
enabled: false,
index: -1,
};
self.getLabel = function () {
return _.words(self.properties.nouns).map(_.capitalize).join(' ');
};
//key handler for the filter text box
self.filterKeyDown = function ($event) {
switch (keyMap[$event.keyCode]) {
case 'enter':
if (self.hitCount !== 1) return;
const hit = self.hits[0];
if (!hit) return;
self.onChoose(hit, $event);
$event.preventDefault();
break;
}
};
//key handler for the list items
self.hitKeyDown = function ($event, page, paginate) {
switch (keyMap[$event.keyCode]) {
case 'tab':
if (!self.selector.enabled) break;
self.selector.index = -1;
self.selector.enabled = false;
//if the user types shift-tab return to the textbox
//if the user types tab, set the focus to the currently selected hit.
if ($event.shiftKey) {
$input.focus();
} else {
$list.find('li.active a').focus();
}
$event.preventDefault();
break;
case 'down':
if (!self.selector.enabled) break;
if (self.selector.index + 1 < page.length) {
self.selector.index += 1;
}
$event.preventDefault();
break;
case 'up':
if (!self.selector.enabled) break;
if (self.selector.index > 0) {
self.selector.index -= 1;
}
$event.preventDefault();
break;
case 'right':
if (!self.selector.enabled) break;
if (page.number < page.count) {
paginate.goToPage(page.number + 1);
self.selector.index = 0;
selectTopHit();
}
$event.preventDefault();
break;
case 'left':
if (!self.selector.enabled) break;
if (page.number > 1) {
paginate.goToPage(page.number - 1);
self.selector.index = 0;
selectTopHit();
}
$event.preventDefault();
break;
case 'escape':
if (!self.selector.enabled) break;
$input.focus();
$event.preventDefault();
break;
case 'enter':
if (!self.selector.enabled) break;
const hitIndex = (page.number - 1) * paginate.perPage + self.selector.index;
const hit = self.hits[hitIndex];
if (!hit) break;
self.onChoose(hit, $event);
$event.preventDefault();
break;
case 'shift':
break;
default:
$input.focus();
break;
}
};
self.hitBlur = function () {
self.selector.index = -1;
self.selector.enabled = false;
};
self.manageObjects = function (type) {
$location.url('/management/kibana/objects?_a=' + rison.encode({ tab: type }));
};
self.hitCountNoun = function () {
return (self.hitCount === 1
? self.properties.noun
: self.properties.nouns
).toLowerCase();
};
function selectTopHit() {
setTimeout(function () {
//triggering a focus event kicks off a new angular digest cycle.
$list.find('a:first').focus();
}, 0);
}
function filterResults() {
if (!self.service) return;
if (!self.properties) return;
// track the filter that we use for this search,
// but ensure that we don't search for the same
// thing twice. This is called from multiple places
// and needs to be smart about when it actually searches
const filter = currentFilter;
if (prevSearch === filter) return;
prevSearch = filter;
const isLabsEnabled = uiSettings.get(VISUALIZE_ENABLE_LABS_SETTING);
self.service.find(filter).then(function (hits) {
hits.hits = hits.hits.filter(
(hit) => isLabsEnabled || _.get(hit, 'type.stage') !== 'experimental'
);
hits.total = hits.hits.length;
// ensure that we don't display old results
// as we can't really cancel requests
if (currentFilter === filter) {
self.hitCount = hits.total;
self.hits = _.sortBy(hits.hits, ['title']);
}
});
}
},
};
});
}

View file

@ -1,29 +0,0 @@
<div ng-hide="!savedObject.id || savedObject.isSaving">
<div
ng-hide="!savedObject.isTitleChanged() || savedObject.copyOnSave"
class="timDropdownWarning"
i18n-id="timelion.savedObjects.howToSaveAsNewDescription"
i18n-default-message="In previous versions of Kibana, changing the name of a {savedObjectName} would make a copy with the new name. Use the 'Save as a new {savedObjectName}' checkbox to do this now."
i18n-values="{ savedObjectName: savedObject.getDisplayName() }"
i18n-description="'Save as a new {savedObjectName}' refers to timelion.savedObjects.saveAsNewLabel and should be the same text."
></div>
<label class="timFormCheckbox">
<input
class="timFormCheckbox__input"
type="checkbox"
data-test-subj="saveAsNewCheckbox"
ng-model="savedObject.copyOnSave"
ng-checked="savedObject.copyOnSave"
>
<icon type="'check'" class="timFormCheckbox__icon" color="'white'" size="'s'"></icon>
<span
style="margin-left: 8px;"
i18n-id="timelion.savedObjects.saveAsNewLabel"
i18n-default-message="Save as a new {savedObjectName}"
i18n-values="{ savedObjectName: savedObject.getDisplayName() }"
></span>
</label>
</div>

View file

@ -1,22 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import saveObjectSaveAsCheckboxTemplate from './saved_object_save_as_checkbox.html';
export function initSavedObjectSaveAsCheckBoxDirective(app) {
app.directive('savedObjectSaveAsCheckBox', function () {
return {
restrict: 'E',
template: saveObjectSaveAsCheckboxTemplate,
replace: true,
scope: {
savedObject: '=',
},
};
});
}

View file

@ -1,41 +0,0 @@
<div
class="timExpressionInput__container"
role="combobox"
aria-expanded="{{suggestions.isVisible}}"
aria-owns="timelionSuggestionList"
aria-haspopup="true"
>
<!-- The `role=textbox` is required by VoiceOver to properly detect the autocompletion.
For some reasons it doesn't work without it (even though the default role of
the element is textbox anyway). -->
<textarea
data-expression-input
role="textbox"
rows="{{ rows }}"
class="timExpressionInput timFormTextarea fullWidth"
placeholder="{{ ::'timelion.expressionInputPlaceholder' | i18n: { defaultMessage: 'Try a query with {esQuery}', values: { esQuery: '.es(*)' } } }}"
ng-model="sheet"
ng-focus="onFocusInput()"
ng-keydown="onKeyDownInput($event)"
ng-keyup="onKeyUpInput($event)"
ng-blur="onBlurInput()"
ng-mousedown="onMouseDownInput()"
ng-mouseup="onMouseUpInput()"
ng-click="onClickExpression()"
aria-label="{{ ::'timelion.expressionInputAriaLabel' | i18n: { defaultMessage: 'Timelion expression' } }}"
aria-multiline="false"
aria-autocomplete="list"
aria-controls="timelionSuggestionList"
aria-activedescendant="{{ getActiveSuggestionId() }}"
data-test-subj="timelionExpressionTextArea"
></textarea>
<timelion-expression-suggestions
ng-show="suggestions.isVisible"
suggestions="suggestions.list"
suggestions-type="suggestions.type"
selected-index="suggestions.index"
on-click-suggestion="onClickSuggestion(suggestionIndex)"
should-popover="shouldPopoverSuggestions"
></timelion-expression-suggestions>
</div>

View file

@ -1,266 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/**
* Timelion Expression Autocompleter
*
* This directive allows users to enter multiline timelion expressions. If the user has entered
* a valid expression and then types a ".", this directive will display a list of suggestions.
*
* Users can navigate suggestions using the arrow keys. When a user selects a suggestion, it's
* inserted into the expression and the caret position is updated to be inside of the newly-
* added function's parentheses.
*
* Beneath the hood, we use a PEG grammar to validate the Timelion expression and detect if
* the caret is in a position within the expression that allows functions to be suggested.
*
* NOTE: This directive doesn't work well with contenteditable divs. Challenges include:
* - You have to replace markup with newline characters and spaces when passing the expression
* to the grammar.
* - You have to do the opposite when loading a saved expression, so that it appears correctly
* within the contenteditable (i.e. replace newlines with <br> markup).
* - The Range and Selection APIs ignore newlines when providing caret position, so there is
* literally no way to insert suggestions into the correct place in a multiline expression
* that has more than a single consecutive newline.
*/
import _ from 'lodash';
import $ from 'jquery';
import timelionExpressionInputTemplate from './timelion_expression_input.html';
import {
SUGGESTION_TYPE,
Suggestions,
suggest,
insertAtLocation,
} from './timelion_expression_input_helpers';
import { comboBoxKeyCodes } from '@elastic/eui';
export function timelionExpInput(deps) {
return ($http, $timeout) => {
return {
restrict: 'E',
scope: {
rows: '=',
sheet: '=',
updateChart: '&',
shouldPopoverSuggestions: '@',
},
replace: true,
template: timelionExpressionInputTemplate,
link: function (scope, elem) {
const argValueSuggestions = deps.plugins.visTypeTimelion.getArgValueSuggestions();
const expressionInput = elem.find('[data-expression-input]');
const functionReference = {};
let suggestibleFunctionLocation = {};
scope.suggestions = new Suggestions();
function init() {
$http.get('../api/timelion/functions').then(function (resp) {
Object.assign(functionReference, {
byName: _.keyBy(resp.data, 'name'),
list: resp.data,
});
});
}
function setCaretOffset(caretOffset) {
// Wait for Angular to update the input with the new expression and *then* we can set
// the caret position.
$timeout(() => {
expressionInput.focus();
expressionInput[0].selectionStart = expressionInput[0].selectionEnd = caretOffset;
scope.$apply();
}, 0);
}
function insertSuggestionIntoExpression(suggestionIndex) {
if (scope.suggestions.isEmpty()) {
return;
}
const { min, max } = suggestibleFunctionLocation;
let insertedValue;
let insertPositionMinOffset = 0;
switch (scope.suggestions.type) {
case SUGGESTION_TYPE.FUNCTIONS: {
// Position the caret inside of the function parentheses.
insertedValue = `${scope.suggestions.list[suggestionIndex].name}()`;
// min advanced one to not replace function '.'
insertPositionMinOffset = 1;
break;
}
case SUGGESTION_TYPE.ARGUMENTS: {
// Position the caret after the '='
insertedValue = `${scope.suggestions.list[suggestionIndex].name}=`;
break;
}
case SUGGESTION_TYPE.ARGUMENT_VALUE: {
// Position the caret after the argument value
insertedValue = `${scope.suggestions.list[suggestionIndex].name}`;
break;
}
}
const updatedExpression = insertAtLocation(
insertedValue,
scope.sheet,
min + insertPositionMinOffset,
max
);
scope.sheet = updatedExpression;
const newCaretOffset = min + insertedValue.length;
setCaretOffset(newCaretOffset);
}
function scrollToSuggestionAt(index) {
// We don't cache these because the list changes based on user input.
const suggestionsList = $('[data-suggestions-list]');
const suggestionListItem = $('[data-suggestion-list-item]')[index];
// Scroll to the position of the item relative to the list, not to the window.
suggestionsList.scrollTop(suggestionListItem.offsetTop - suggestionsList[0].offsetTop);
}
function getCursorPosition() {
if (expressionInput.length) {
return expressionInput[0].selectionStart;
}
return null;
}
async function getSuggestions() {
const suggestions = await suggest(
scope.sheet,
functionReference.list,
getCursorPosition(),
argValueSuggestions
);
// We're using ES6 Promises, not $q, so we have to wrap this in $apply.
scope.$apply(() => {
if (suggestions) {
scope.suggestions.setList(suggestions.list, suggestions.type);
scope.suggestions.show();
suggestibleFunctionLocation = suggestions.location;
$timeout(() => {
const suggestionsList = $('[data-suggestions-list]');
suggestionsList.scrollTop(0);
}, 0);
return;
}
suggestibleFunctionLocation = undefined;
scope.suggestions.reset();
});
}
function isNavigationalKey(keyCode) {
const keyCodes = _.values(comboBoxKeyCodes);
return keyCodes.includes(keyCode);
}
scope.onFocusInput = () => {
// Wait for the caret position of the input to update and then we can get suggestions
// (which depends on the caret position).
$timeout(getSuggestions, 0);
};
scope.onBlurInput = () => {
scope.suggestions.hide();
};
scope.onKeyDownInput = (e) => {
// If we've pressed any non-navigational keys, then the user has typed something and we
// can exit early without doing any navigation. The keyup handler will pull up suggestions.
if (!isNavigationalKey(e.keyCode)) {
return;
}
switch (e.keyCode) {
case comboBoxKeyCodes.UP:
if (scope.suggestions.isVisible) {
// Up and down keys navigate through suggestions.
e.preventDefault();
scope.suggestions.stepForward();
scrollToSuggestionAt(scope.suggestions.index);
}
break;
case comboBoxKeyCodes.DOWN:
if (scope.suggestions.isVisible) {
// Up and down keys navigate through suggestions.
e.preventDefault();
scope.suggestions.stepBackward();
scrollToSuggestionAt(scope.suggestions.index);
}
break;
case comboBoxKeyCodes.TAB:
// If there are no suggestions or none is selected, the user tabs to the next input.
if (scope.suggestions.isEmpty() || scope.suggestions.index < 0) {
// Before letting the tab be handled to focus the next element
// we need to hide the suggestions, otherwise it will focus these
// instead of the time interval select.
scope.suggestions.hide();
return;
}
// If we have suggestions, complete the selected one.
e.preventDefault();
insertSuggestionIntoExpression(scope.suggestions.index);
break;
case comboBoxKeyCodes.ENTER:
if (e.metaKey || e.ctrlKey) {
// Re-render the chart when the user hits CMD+ENTER.
e.preventDefault();
scope.updateChart();
} else if (!scope.suggestions.isEmpty()) {
// If the suggestions are open, complete the expression with the suggestion.
e.preventDefault();
insertSuggestionIntoExpression(scope.suggestions.index);
}
break;
case comboBoxKeyCodes.ESCAPE:
e.preventDefault();
scope.suggestions.hide();
break;
}
};
scope.onKeyUpInput = (e) => {
// If the user isn't navigating, then we should update the suggestions based on their input.
if (!isNavigationalKey(e.keyCode)) {
getSuggestions();
}
};
scope.onClickExpression = () => {
getSuggestions();
};
scope.onClickSuggestion = (index) => {
insertSuggestionIntoExpression(index);
};
scope.getActiveSuggestionId = () => {
if (scope.suggestions.isVisible && scope.suggestions.index > -1) {
return `timelionSuggestion${scope.suggestions.index}`;
}
return '';
};
init();
},
};
};
}

View file

@ -1,264 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
import { _LEGACY_ as visTypeTimelion } from '../../../vis_type_timelion/public';
export const SUGGESTION_TYPE = {
ARGUMENTS: 'arguments',
ARGUMENT_VALUE: 'argument_value',
FUNCTIONS: 'functions',
};
export class Suggestions {
constructor() {
this.reset();
}
reset() {
this.index = -1;
this.list = [];
this.type = null;
this.isVisible = false;
}
setList(list, type) {
this.list = list.sort((a, b) => {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
// names must be equal
return 0;
});
this.type = type;
// Only try to position index inside of list range, when it was already focused
// beforehand (i.e. not -1)
if (this.index > -1) {
// We may get a shorter list than the one we have now, so we need to make sure our index doesn't
// fall outside of the new list's range.
this.index = Math.max(0, Math.min(this.index, this.list.length - 1));
}
}
getCount() {
return this.list.length;
}
isEmpty() {
return this.list.length === 0;
}
show() {
this.isVisible = true;
}
hide() {
this.isVisible = false;
}
stepForward() {
if (this.index > 0) {
this.index -= 1;
}
}
stepBackward() {
if (this.index < this.list.length - 1) {
this.index += 1;
}
}
}
function inLocation(cursorPosition, location) {
return cursorPosition >= location.min && cursorPosition <= location.max;
}
function getArgumentsHelp(functionHelp, functionArgs = []) {
if (!functionHelp) {
return [];
}
// Do not provide 'inputSeries' as argument suggestion for chainable functions
const argsHelp = functionHelp.chainable ? functionHelp.args.slice(1) : functionHelp.args.slice(0);
// ignore arguments that are already provided in function declaration
const functionArgNames = functionArgs.map((arg) => {
return arg.name;
});
return argsHelp.filter((arg) => {
return !functionArgNames.includes(arg.name);
});
}
async function extractSuggestionsFromParsedResult(
result,
cursorPosition,
functionList,
argValueSuggestions
) {
const activeFunc = result.functions.find((func) => {
return cursorPosition >= func.location.min && cursorPosition < func.location.max;
});
if (!activeFunc) {
return;
}
const functionHelp = functionList.find((func) => {
return func.name === activeFunc.function;
});
// return function suggestion when cursor is outside of parentheses
// location range includes '.', function name, and '('.
const openParen = activeFunc.location.min + activeFunc.function.length + 2;
if (cursorPosition < openParen) {
return { list: [functionHelp], location: activeFunc.location, type: SUGGESTION_TYPE.FUNCTIONS };
}
// return argument value suggestions when cursor is inside argument value
const activeArg = activeFunc.arguments.find((argument) => {
return inLocation(cursorPosition, argument.location);
});
if (
activeArg &&
activeArg.type === 'namedArg' &&
inLocation(cursorPosition, activeArg.value.location)
) {
const { function: functionName, arguments: functionArgs } = activeFunc;
const {
name: argName,
value: { text: partialInput },
} = activeArg;
let valueSuggestions;
if (argValueSuggestions.hasDynamicSuggestionsForArgument(functionName, argName)) {
valueSuggestions = await argValueSuggestions.getDynamicSuggestionsForArgument(
functionName,
argName,
functionArgs,
partialInput
);
} else {
const { suggestions: staticSuggestions } = functionHelp.args.find((arg) => {
return arg.name === activeArg.name;
});
valueSuggestions = argValueSuggestions.getStaticSuggestionsForInput(
partialInput,
staticSuggestions
);
}
return {
list: valueSuggestions,
location: activeArg.value.location,
type: SUGGESTION_TYPE.ARGUMENT_VALUE,
};
}
// return argument suggestions
const argsHelp = getArgumentsHelp(functionHelp, activeFunc.arguments);
const argumentSuggestions = argsHelp.filter((arg) => {
if (_.get(activeArg, 'type') === 'namedArg') {
return _.startsWith(arg.name, activeArg.name);
} else if (activeArg) {
return _.startsWith(arg.name, activeArg.text);
}
return true;
});
const location = activeArg ? activeArg.location : { min: cursorPosition, max: cursorPosition };
return { list: argumentSuggestions, location: location, type: SUGGESTION_TYPE.ARGUMENTS };
}
export async function suggest(expression, functionList, cursorPosition, argValueSuggestions) {
try {
const result = await visTypeTimelion.parseTimelionExpressionAsync(expression);
return await extractSuggestionsFromParsedResult(
result,
cursorPosition,
functionList,
argValueSuggestions
);
} catch (e) {
let message;
try {
// The grammar will throw an error containing a message if the expression is formatted
// correctly and is prepared to accept suggestions. If the expression is not formatted
// correctly the grammar will just throw a regular PEG SyntaxError, and this JSON.parse
// attempt will throw an error.
message = JSON.parse(e.message);
} catch (e) {
// The expression isn't correctly formatted, so JSON.parse threw an error.
return;
}
switch (message.type) {
case 'incompleteFunction': {
let list;
if (message.function) {
// The user has start typing a function name, so we'll filter the list down to only
// possible matches.
list = functionList.filter((func) => _.startsWith(func.name, message.function));
} else {
// The user hasn't typed anything yet, so we'll just return the entire list.
list = functionList;
}
return { list, location: message.location, type: SUGGESTION_TYPE.FUNCTIONS };
}
case 'incompleteArgument': {
const { currentFunction: functionName, currentArgs: functionArgs } = message;
const functionHelp = functionList.find((func) => func.name === functionName);
return {
list: getArgumentsHelp(functionHelp, functionArgs),
location: message.location,
type: SUGGESTION_TYPE.ARGUMENTS,
};
}
case 'incompleteArgumentValue': {
const { name: argName, currentFunction: functionName, currentArgs: functionArgs } = message;
let valueSuggestions = [];
if (argValueSuggestions.hasDynamicSuggestionsForArgument(functionName, argName)) {
valueSuggestions = await argValueSuggestions.getDynamicSuggestionsForArgument(
functionName,
argName,
functionArgs
);
} else {
const functionHelp = functionList.find((func) => func.name === functionName);
if (functionHelp) {
const argHelp = functionHelp.args.find((arg) => arg.name === argName);
if (argHelp && argHelp.suggestions) {
valueSuggestions = argHelp.suggestions;
}
}
}
return {
list: valueSuggestions,
location: { min: cursorPosition, max: cursorPosition },
type: SUGGESTION_TYPE.ARGUMENT_VALUE,
};
}
}
}
}
export function insertAtLocation(
valueToInsert,
destination,
replacementRangeStart,
replacementRangeEnd
) {
// Insert the value at a location caret within the destination.
const prefix = destination.slice(0, replacementRangeStart);
const suffix = destination.slice(replacementRangeEnd, destination.length);
const result = `${prefix}${valueToInsert}${suffix}`;
return result;
}

View file

@ -1 +0,0 @@
@import './timelion_expression_suggestions';

View file

@ -1,36 +0,0 @@
.timSuggestions {
@include euiBottomShadowMedium;
background-color: $euiColorLightestShade;
color: $euiTextColor;
border: $euiBorderThin;
// sass-lint:disable-block no-important
border-radius: 0 0 $euiBorderRadius $euiBorderRadius !important;
z-index: $euiZLevel9;
max-height: $euiSizeXL * 10;
overflow-y: auto;
&.timSuggestions-isPopover {
position: absolute;
top: 100%;
}
}
.timSuggestions__item {
border-bottom: $euiBorderThin;
padding: $euiSizeXS $euiSizeL;
&:hover,
&.active {
background-color: $euiColorLightShade;
}
}
.timSuggestions__details {
background-color: $euiColorLightestShade;
padding: $euiSizeM;
border-radius: $euiBorderRadius;
> table {
margin-bottom: 0;
}
}

View file

@ -1,109 +0,0 @@
<div
id="timelionSuggestionList"
class="timSuggestions"
role="listbox"
ng-class="{ 'timSuggestions-isPopover': shouldPopover === 'true' }"
data-suggestions-list
>
<div
class="timSuggestions__item"
id="timelionSuggestion{{$index}}"
role="option"
tabindex="0"
data-suggestion-list-item
ng-class="{active: $index === selectedIndex}"
ng-repeat="suggestion in suggestions track by suggestion.name"
ng-mousedown="onMouseDown($event)"
ng-click="onClickSuggestion({ suggestionIndex: $index })"
aria-label="{{suggestion.name}}"
aria-describedby="timelionSuggestionDescription{{$index}}"
data-test-subj="timelionSuggestionListItem"
>
<div ng-switch on="suggestionsType">
<div ng-switch-when="functions">
<h4>
<strong>.{{suggestion.name}}()</strong>
<small id="timelionSuggestionDescription{{$index}}">
<span
ng-if="suggestion.chainable"
i18n-id="timelion.expressionSuggestions.func.description.chainableText"
i18n-default-message="{help} (Chainable)"
i18n-values="{ help: suggestion.help }"
></span>
<span
ng-if="!suggestion.chainable"
i18n-id="timelion.expressionSuggestions.func.description.dataSourceText"
i18n-default-message="{help} (Data Source)"
i18n-values="{ help: suggestion.help }"
></span>
</small>
</h4>
<div ng-show="suggestion.args.length > (suggestion.chainable ? 1: 0)">
<div ng-show="suggestions.length > 1">
<strong
i18n-id="timelion.expressionSuggestions.arg.listTitle"
i18n-default-message="Arguments:"
></strong>
<span ng-repeat="arg in suggestion.args" ng-hide="$index < 1 && suggestion.chainable">
<strong>{{arg.name}}</strong>=(<em>{{arg.types.join(' | ')}}</em>)
<em ng-show="!$last">,</em>
</span>
</div>
<div class="timSuggestions__details" ng-show="suggestions.length === 1">
<table class="table table-striped table-condensed table-bordered">
<thead>
<th
scope="col"
i18n-id="timelion.expressionSuggestions.arg.nameTitle"
i18n-default-message="Argument Name"
></th>
<th
scope="col"
i18n-id="timelion.expressionSuggestions.arg.typesTitle"
i18n-default-message="Accepted Types"
></th>
<th
scope="col"
i18n-id="timelion.expressionSuggestions.arg.infoTitle"
i18n-default-message="Information"
></th>
</thead>
<tr ng-repeat="arg in suggestion.args" ng-hide="$index < 1 && suggestion.chainable">
<td>{{arg.name}}</td>
<td><em>{{arg.types.join(', ')}}</em></td>
<td>{{arg.help}}</td>
</tr>
</table>
</div>
</div>
</div>
<div ng-switch-when="arguments">
<h4>
<strong>{{suggestion.name}}=</strong>
<small id="timelionSuggestionDescription{{$index}}">
{{suggestion.help}}
</small>
</h4>
<div>
<strong>Accepts:</strong>
<em>{{suggestion.types.join(', ')}}</em>
</div>
</div>
<div ng-switch-when="argument_value">
<h4>
<strong>{{suggestion.name}}</strong>
<small id="timelionSuggestionDescription{{$index}}">
{{suggestion.help}}
</small>
</h4>
</div>
</div>
</div>
</div>

View file

@ -1,28 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import template from './timelion_expression_suggestions.html';
export function TimelionExpressionSuggestions() {
return {
restrict: 'E',
scope: {
suggestions: '=',
suggestionsType: '=',
selectedIndex: '=',
onClickSuggestion: '&',
shouldPopover: '=',
},
replace: true,
template,
link: function (scope) {
// This will prevent the expression input from losing focus.
scope.onMouseDown = (e) => e.preventDefault();
},
};
}

View file

@ -1,57 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import $ from 'jquery';
export function initTimelionGridDirective(app) {
app.directive('timelionGrid', function () {
return {
restrict: 'A',
scope: {
timelionGridRows: '=',
timelionGridColumns: '=',
},
link: function ($scope, $elem) {
function init() {
setDimensions();
}
$scope.$on('$destroy', function () {
$(window).off('resize'); //remove the handler added earlier
});
$(window).resize(function () {
setDimensions();
});
$scope.$watchMulti(['timelionGridColumns', 'timelionGridRows'], function () {
setDimensions();
});
function setDimensions() {
const borderSize = 2;
const headerSize = 45 + 35 + 28 + 20 * 2; // chrome + subnav + buttons + (container padding)
const verticalPadding = 10;
if ($scope.timelionGridColumns != null) {
$elem.width($elem.parent().width() / $scope.timelionGridColumns - borderSize * 2);
}
if ($scope.timelionGridRows != null) {
$elem.height(
($(window).height() - headerSize) / $scope.timelionGridRows -
(verticalPadding + borderSize * 2)
);
}
}
init();
},
};
});
}

View file

@ -1 +0,0 @@
@import './timelion_help';

View file

@ -1,33 +0,0 @@
.timHelp {
// EUITODO: Make .euiText > code background transparent
code {
background-color: transparentize($euiTextColor, .9);
}
}
.timHelp__buttons {
display: flex;
justify-content: space-between;
}
.timHelp__functions {
height: $euiSizeXL * 10;
overflow-y: auto;
}
.timHelp__links {
color: $euiColorPrimary;
&:hover {
text-decoration: underline;
}
}
/**
* 1. Override bootstrap .table styles.
*/
.timHelp__functionsTableRow:hover,
.timHelp__functionDetailsTable {
// sass-lint:disable-block no-important
background-color: $euiColorLightestShade !important; /* 1 */
}

View file

@ -1,741 +0,0 @@
<div class="euiText timHelp">
<div ng-show="page === 1">
<div>
<h1
i18n-id="timelion.help.welcomeTitle"
i18n-default-message="Welcome to {strongTimelionLabel}!"
i18n-values="{ html_strongTimelionLabel: '<strong>Timelion</strong>' }"
></h1>
<p
i18n-id="timelion.help.welcome.content.paragraph1"
i18n-default-message="Timelion is the clawing, gnashing, zebra killing, pluggable time
series interface for {emphasizedEverything}. If your datastore can
produce a time series, then you have all of the awesome power of
Timelion at your disposal. Timelion lets you compare, combine, and
combobulate datasets across multiple datasources with one
easy-to-master expression syntax. This tutorial focuses on
Elasticsearch, but you'll quickly discover that what you learn here
applies to any datasource Timelion supports."
i18n-values="{ html_emphasizedEverything: '<em>' + translations.emphasizedEverythingText + '</em>' }"
></p>
<p>
<span
i18n-id="timelion.help.welcome.content.paragraph2"
i18n-default-message="Ready to get started? Click {strongNext}. Want to skip the tutorial and view the docs?"
i18n-values="{
html_strongNext: '<strong>' + translations.strongNextText + '</strong>',
}"
></span>
<a
ng-click="setPage(0)"
i18n-id="timelion.help.welcome.content.functionReferenceLinkText"
i18n-default-message="Jump to the function reference"
></a>.
</p>
</div>
<div class="timHelp__buttons">
<button
ng-click="opts.dontShowHelp()"
class="timHelp__links"
>
{{translations.dontShowHelpButtonLabel}}
</button>
<button
ng-click="setPage(page+1)"
class="timApp__button"
>
{{translations.nextButtonLabel}}
</button>
</div>
</div>
<div ng-show="page === 2">
<div ng-show="!es.valid">
<div>
<h2
i18n-id="timelion.help.configuration.notValidTitle"
i18n-default-message="First time configuration"
></h2>
<p
i18n-id="timelion.help.configuration.notValid.paragraph1"
i18n-default-message="If you're using Logstash, you don't need to configure anything to
start exploring your log data with Timelion. To search other
indices, go to {advancedSettingsPath} and configure the {esDefaultIndex}
and {esTimefield} settings to match your indices."
i18n-values="{
html_advancedSettingsPath: '<strong>' + translations.notValidAdvancedSettingsPath + '</strong>',
html_esDefaultIndex: '<code>timelion:es.default_index</code>',
html_esTimefield: '<code>timelion:es.timefield</code>',
}"
></p>
<p
i18n-id="timelion.help.configuration.notValid.paragraph2"
i18n-default-message="You'll also see some other Timelion settings. For now, you don't need
to worry about them. Later, you'll see that you can set most of
them on the fly if you need to."
></p>
</div>
<div class="timHelp__buttons">
<button
ng-click="setPage(page-1)"
class="timApp__button"
>
{{translations.previousButtonLabel}}
</button>
<span
ng-show="es.invalidCount > 0 && !es.valid"
i18n-id="timelion.help.configuration.notValid.notValidSettingsErrorMessage"
i18n-default-message="Could not validate Elasticsearch settings: {reason}.
Check your Advanced Settings and try again. ({count})"
i18n-values="{
html_reason: '<strong>' + es.invalidReason + '</strong>',
count: es.invalidCount,
}"
></span>
<button
ng-click="recheckElasticsearch()"
class="timApp__button"
i18n-id="timelion.help.configuration.notValid.validateButtonLabel"
i18n-default-message="Validate Config"
></button>
</div>
</div>
<div ng-show="es.valid">
<div>
<h2
i18n-id="timelion.help.configuration.validTitle"
i18n-default-message="Good news, Elasticsearch is configured correctly!"
></h2>
<p>
<span
i18n-id="timelion.help.configuration.valid.paragraph1Part1"
i18n-default-message="We validated your default index and your timefield and everything
looks ok. We found data from {statsMin} to {statsMax}.
You're probably all set. If this doesn't look right, see"
i18n-values="{
html_statsMin: '<strong>' + es.stats.min + '</strong>',
html_statsMax: '<strong>' + es.stats.max + '</strong>',
}"
i18n-description="Part of composite text timelion.help.configuration.valid.paragraph1Part1 +
timelion.help.configuration.firstTimeConfigurationLinkText +
timelion.help.configuration.valid.paragraph1Part2"
></span>
<a
ng-click="es.valid = false"
i18n-id="timelion.help.configuration.firstTimeConfigurationLinkText"
i18n-default-message="First time configuration"
i18n-description="Part of composite text timelion.help.configuration.valid.paragraph1Part1 +
timelion.help.configuration.firstTimeConfigurationLinkText +
timelion.help.configuration.valid.paragraph1Part2"
></a>
<span
i18n-id="timelion.help.configuration.valid.paragraph1Part2"
i18n-default-message="for information about configuring the Elasticsearch datasource."
i18n-description="Part of composite text timelion.help.configuration.valid.paragraph1Part1 +
timelion.help.configuration.firstTimeConfigurationLinkText +
timelion.help.configuration.valid.paragraph1Part2"
></span>
</p>
<p
i18n-id="timelion.help.configuration.valid.paragraph2"
i18n-default-message="You should already see one chart, but you might need to make a
couple adjustments before you see any interesting data:"
></p>
<ul>
<li>
<strong
i18n-id="timelion.help.configuration.valid.intervalsTitle"
i18n-default-message="Intervals"
></strong>
<p>
<span
i18n-id="timelion.help.configuration.valid.intervalsTextPart1"
i18n-default-message="The interval selector at the right of the input bar lets you
control the sampling frequency. It's currently set to {interval}."
i18n-values="{ html_interval: '<code>' + state.interval + '</code>' }"
i18n-description="Part of composite text
timelion.help.configuration.valid.intervalsTextPart1 +
(timelion.help.configuration.valid.intervalIsAutoText ||
timelion.help.configuration.valid.intervals.content.intervalIsNotAutoText) +
timelion.help.configuration.valid.intervalsTextPart2"
></span>
<span ng-show="state.interval == 'auto'">
<strong
i18n-id="timelion.help.configuration.valid.intervalIsAutoText"
i18n-default-message="You're all set!"
i18n-description="Part of composite text
timelion.help.configuration.valid.intervalsTextPart1 +
(timelion.help.configuration.valid.intervalIsAutoText ||
timelion.help.configuration.valid.intervals.content.intervalIsNotAutoText) +
timelion.help.configuration.valid.intervalsTextPart2"
></strong>
</span>
<span
ng-show="state.interval != 'auto'"
i18n-id="timelion.help.configuration.valid.intervals.content.intervalIsNotAutoText"
i18n-default-message="Set it to {auto} to let Timelion choose an appropriate interval."
i18n-description="Part of composite text
timelion.help.configuration.valid.intervalsTextPart1 +
(timelion.help.configuration.valid.intervalIsAutoText ||
timelion.help.configuration.valid.intervals.content.intervalIsNotAutoText) +
timelion.help.configuration.valid.intervalsTextPart2"
i18n-values="{ html_auto: '<code>auto</code>' }"
></span>
<span
i18n-id="timelion.help.configuration.valid.intervalsTextPart2"
i18n-default-message="If Timelion thinks your combination of time range and interval
will produce too many data points, it throws an error.
You can adjust that limit by configuring {maxBuckets} in {advancedSettingsPath}."
i18n-values="{
html_maxBuckets: '<code>timelion:max_buckets</code>',
html_advancedSettingsPath: '<strong>' + translations.validAdvancedSettingsPath + '</strong>',
}"
></span>
</p>
</li>
<li>
<strong
i18n-id="timelion.help.configuration.valid.timeRangeTitle"
i18n-default-message="Time range"
></strong>
<p
i18n-id="timelion.help.configuration.valid.timeRangeText"
i18n-default-message="Use the time filter to select the time period
that contains the data you want to visualize. Make sure you select
a time period that includes all or part of the time range shown above."
></p>
</li>
</ul>
<p
i18n-id="timelion.help.configuration.valid.paragraph3"
i18n-default-message="Now, you should see a line chart that displays a count of your data points over time."
></p>
</div>
<div class="timHelp__buttons">
<button
ng-click="setPage(page-1)"
class="timApp__button"
>
{{translations.previousButtonLabel}}
</button>
<button
ng-click="setPage(page+1)"
class="timApp__button"
>
{{translations.nextButtonLabel}}
</button>
</div>
</div>
</div>
<div ng-show="page === 3">
<div>
<h2
i18n-id="timelion.help.queryingTitle"
i18n-default-message="Querying the Elasticsearch datasource"
></h2>
<p
i18n-id="timelion.help.querying.paragraph1"
i18n-default-message="Now that we've validated that you have a working Elasticsearch
datasource, you can start submitting queries. For starters,
enter {esPattern} in the input bar and hit enter."
i18n-values="{
html_esPattern: '<code>.es(*)</code>',
}"
></p>
<p>
<span
i18n-id="timelion.help.querying.paragraph2Part1"
i18n-default-message="This says {esAsteriskQueryDescription}. If you want to find a subset, you could enter something
like {htmlQuery} to count events that match {html}, or {bobQuery}
to find events that contain {bob} in the {user} field and have a {bytes}
field that is greater than 100. Note that this query is enclosed in single
quotes&mdash;that's because it contains spaces. You can enter any"
i18n-values="{
html_esAsteriskQueryDescription: '<em>' + translations.esAsteriskQueryDescription + '</em>',
html_html: '<em>html</em>',
html_htmlQuery: '<code>.es(html)</code>',
html_bobQuery: '<code>.es(\'user:bob AND bytes:>100\')</code>',
html_bob: '<em>bob</em>',
html_user: '<code>user</code>',
html_bytes: '<code>bytes</code>',
}"
i18n-description="Part of composite text
timelion.help.querying.paragraph2Part1 +
timelion.help.querying.luceneQueryLinkText +
timelion.help.querying.paragraph2Part2"
></span>
<a
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax"
target="_blank"
rel="noopener"
i18n-id="timelion.help.querying.luceneQueryLinkText"
i18n-default-message="Lucene query string"
i18n-description="Part of composite text
timelion.help.querying.paragraph2Part1 +
timelion.help.querying.luceneQueryLinkText +
timelion.help.querying.paragraph2Part2"
></a>
<span
i18n-id="timelion.help.querying.paragraph2Part2"
i18n-default-message="as the first argument to the {esQuery} function."
i18n-values="{
html_esQuery: '<code>.es()</code>',
}"
i18n-description="Part of composite text
timelion.help.querying.paragraph2Part1 +
timelion.help.querying.luceneQueryLinkText +
timelion.help.querying.paragraph2Part2"
></span>
</p>
<h4
i18n-id="timelion.help.querying.passingArgumentsTitle"
i18n-default-message="Passing arguments"
></h4>
<p
i18n-id="timelion.help.querying.passingArgumentsText"
i18n-default-message="Timelion has a number of shortcuts that make it easy to do common things.
One is that for simple arguments that don't contain spaces or special
characters, you don't need to use quotes. Many functions also have defaults.
For example, {esEmptyQuery} and {esStarQuery} do the same thing.
Arguments also have names, so you don't have to specify them in a specific order.
For example, you can enter {esLogstashQuery} to tell the Elasticsearch datasource
{esIndexQueryDescription}."
i18n-values="{
html_esEmptyQuery: '<code>.es()</code>',
html_esStarQuery: '<code>.es(*)</code>',
html_esLogstashQuery: '<code>.es(index=\'logstash-*\', q=\'*\')</code>',
html_esIndexQueryDescription: '<em>' + translations.esIndexQueryDescription + '</em>',
}"
></p>
<h4
i18n-id="timelion.help.querying.countTitle"
i18n-default-message="Beyond count"
></h4>
<p>
<span
i18n-id="timelion.help.querying.countTextPart1"
i18n-default-message="Counting events is all well and good, but the Elasticsearch datasource also supports any"
i18n-description="Part of composite text
timelion.help.querying.countTextPart1 +
timelion.help.querying.countMetricAggregationLinkText +
timelion.help.querying.countTextPart2"
></span>
<a
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics.html"
target="_blank"
rel="noopener"
i18n-id="timelion.help.querying.countMetricAggregationLinkText"
i18n-default-message="Elasticsearch metric aggregation"
i18n-description="Part of composite text
timelion.help.querying.countTextPart1 +
timelion.help.querying.countMetricAggregationLinkText +
timelion.help.querying.countTextPart2"
></a>
<span
i18n-id="timelion.help.querying.countTextPart2"
i18n-default-message="that returns a single value. Some of the most useful are
{min}, {max}, {avg}, {sum}, and {cardinality}.
Let's say you want a unique count of the {srcIp} field.
Simply use the {cardinality} metric: {esCardinalityQuery}. To get the
average of the {bytes} field, you can use the {avg} metric: {esAvgQuery}."
i18n-values="{
html_min: '<code>min</code>',
html_max: '<code>max</code>',
html_avg: '<code>avg</code>',
html_sum: '<code>sum</code>',
html_cardinality: '<code>cardinality</code>',
html_bytes: '<code>bytes</code>',
html_srcIp: '<code>src_ip</code>',
html_esCardinalityQuery: '<code>.es(*, metric=\'cardinality:src_ip\')</code>',
html_esAvgQuery: '<code>.es(metric=\'avg:bytes\')</code>',
}"
i18n-description="Part of composite text
timelion.help.querying.countTextPart1 +
timelion.help.querying.countMetricAggregationLinkText +
timelion.help.querying.countTextPart2"
></span>
</p>
</div>
<div class="timHelp__buttons">
<button
ng-click="setPage(page-1)"
class="timApp__button"
>
{{translations.previousButtonLabel}}
</button>
<button
ng-click="setPage(page+1)"
class="timApp__button"
>
{{translations.nextButtonLabel}}
</button>
</div>
</div>
<div ng-show="page === 4">
<div>
<h2
i18n-id="timelion.help.expressionsTitle"
i18n-default-message="Expressing yourself with expressions"
></h2>
<p
i18n-id="timelion.help.expressions.paragraph1"
i18n-default-message="Every expression starts with a datasource function. From there, you
can append new functions to the datasource to transform and augment it."
></p>
<p
i18n-id="timelion.help.expressions.paragraph2"
i18n-default-message="By the way, from here on out you probably know more about your data
than we do. Feel free to replace the sample queries with something
more meaningful!"
></p>
<p
i18n-id="timelion.help.expressions.paragraph3"
i18n-default-message="We're going to experiment, so click {strongAdd} in the toolbar
to add another chart or three. Then, select a chart,
copy one of the following expressions, paste it into the input bar,
and hit enter. Rinse, repeat to try out the other expressions."
i18n-values="{ html_strongAdd: '<strong>' + translations.strongAddText + '</strong>' }"
></p>
<table class="table table-condensed table-striped">
<tr>
<td><code>.es(*), .es(US)</code></td>
<td
i18n-id="timelion.help.expressions.examples.twoExpressionsDescription"
i18n-default-message="{descriptionTitle} Two expressions on the same chart."
i18n-values="{
html_descriptionTitle: '<strong>' + translations.twoExpressionsDescriptionTitle + '</strong>',
}"
></td>
</tr>
<tr>
<td><code>.es(*).color(#f66), .es(US).bars(1)</code></td>
<td
i18n-id="timelion.help.expressions.examples.customStylingDescription"
i18n-default-message="{descriptionTitle} Colorizes the first series red and
uses 1 pixel wide bars for the second series."
i18n-values="{
html_descriptionTitle: '<strong>' + translations.customStylingDescriptionTitle + '</strong>',
}"
></td>
</tr>
<tr>
<td>
<code>.es(*).color(#f66).lines(fill=3),
.es(US).bars(1).points(radius=3, weight=1)</code>
</td>
<td
i18n-id="timelion.help.expressions.examples.namedArgumentsDescription"
i18n-default-message="{descriptionTitle} Forget trying to remember what order you need
to specify arguments in, use named arguments to make
the expressions easier to read and write."
i18n-values="{
html_descriptionTitle: '<strong>' + translations.namedArgumentsDescriptionTitle + '</strong>',
}"
></td>
</tr>
<tr>
<td><code>(.es(*), .es(GB)).points()</code></td>
<td
i18n-id="timelion.help.expressions.examples.groupedExpressionsDescription"
i18n-default-message="{descriptionTitle} You can also chain groups of expressions to
functions. Here, both series are shown as points instead of lines."
i18n-values="{
html_descriptionTitle: '<strong>' + translations.groupedExpressionsDescriptionTitle + '</strong>',
}"
></td>
</tr>
</table>
<p>
<span
i18n-id="timelion.help.expressions.paragraph4"
i18n-default-message="Timelion provides additional view transformation functions you can use
to customize the appearance of your charts. For the complete list, see the"
i18n-description="Part of composite text
timelion.help.expressions.paragraph4 +
timelion.help.expressions.functionReferenceLinkText"
></span>
<a
ng-click="setPage(0)"
i18n-id="timelion.help.expressions.functionReferenceLinkText"
i18n-default-message="Function reference"
i18n-description="Part of composite text
timelion.help.expressions.paragraph4 +
timelion.help.expressions.functionReferenceLinkText"
></a>.
</p>
</div>
<div class="timHelp__buttons">
<button
ng-click="setPage(page-1)"
class="timApp__button"
>
{{translations.previousButtonLabel}}
</button>
<button
ng-click="setPage(page+1)"
class="timApp__button"
>
{{translations.nextButtonLabel}}
</button>
</div>
</div>
<div ng-show="page === 5">
<div>
<h2
i18n-id="timelion.help.dataTransformingTitle"
i18n-default-message="Transforming your data: the real fun begins!"
></h2>
<p
i18n-id="timelion.help.dataTransforming.paragraph1"
i18n-default-message="Now that you've mastered the basics, it's time to unleash the power of
Timelion. Let's figure out what percentage some subset of our data
represents of the whole, over time. For example, what percentage of
our web traffic comes from the US?"
></p>
<p
i18n-id="timelion.help.dataTransforming.paragraph2"
i18n-default-message="First, we need to find all events that contain US: {esUsQuery}."
i18n-values="{ html_esUsQuery: '<code>.es(\'US\')</code>' }"
></p>
<p
i18n-id="timelion.help.dataTransforming.paragraph3"
i18n-default-message="Next, we want to calculate the ratio of US events to the whole.
To divide {us} by everything, we can use the {divide} function:
{divideDataQuery}."
i18n-values="{
html_us: '<code>\'US\'</code>',
html_divide: '<code>divide</code>',
html_divideDataQuery: '<code>.es(\'US\').divide(.es())</code>',
}"
></p>
<p
i18n-id="timelion.help.dataTransforming.paragraph4"
i18n-default-message="Not bad, but this gives us a number between 0 and 1. To convert it
to a percentage, simply multiply by 100: {multiplyDataQuery}."
i18n-values="{ html_multiplyDataQuery: '<code>.es(\'US\').divide(.es()).multiply(100)</code>' }"
></p>
<p
i18n-id="timelion.help.dataTransforming.paragraph5"
i18n-default-message="Now we know what percentage of our traffic comes from the US, and
can see how it has changed over time! Timelion has a number of
built-in arithmetic functions, such as {sum}, {subtract}, {multiply},
and {divide}. Many of these can take a series or a number. There are
also other useful data transformation functions, such as
{movingaverage}, {abs}, and {derivative}."
i18n-values="{
html_sum: '<code>sum</code>',
html_subtract: '<code>subtract</code>',
html_multiply: '<code>multiply</code>',
html_divide: '<code>divide</code>',
html_movingaverage: '<code>movingaverage</code>',
html_abs: '<code>abs</code>',
html_derivative: '<code>derivative</code>',
}"
></p>
<p>
<span
i18n-id="timelion.help.dataTransforming.paragraph6Part1"
i18n-default-message="Now that you're familiar with the syntax, refer to the"
i18n-description="Part of composite text
timelion.help.dataTransforming.paragraph6Part1 +
timelion.help.dataTransforming.functionReferenceLinkText +
timelion.help.dataTransforming.paragraph6Part2"
></span>
<a
ng-click="setPage(0)"
i18n-id="timelion.help.dataTransforming.functionReferenceLinkText"
i18n-default-message="Function reference"
i18n-description="Part of composite text
timelion.help.dataTransforming.paragraph6Part1 +
timelion.help.dataTransforming.functionReferenceLinkText +
timelion.help.dataTransforming.paragraph6Part2"
></a>
<span
i18n-id="timelion.help.dataTransforming.paragraph6Part2"
i18n-default-message="to see how to use all of the available Timelion functions.
You can view the reference at any time by clicking \{Docs\}
in the toolbar. To get back to this tutorial, click the
\{Tutorial\} link at the top of the reference."
i18n-description="Part of composite text
timelion.help.dataTransforming.paragraph6Part1 +
timelion.help.dataTransforming.functionReferenceLinkText +
timelion.help.dataTransforming.paragraph6Part2"
></span>
</p>
</div>
<div class="timHelp__buttons">
<button
ng-click="setPage(page-1)"
class="timApp__button"
>
{{translations.previousButtonLabel}}
</button>
<button
ng-click="opts.dontShowHelp()"
class="timHelp__links"
>
{{translations.dontShowHelpButtonLabel}}
</button>
</div>
</div>
</div>
<div ng-show="page === 0">
<h2
class="timApp__sectionTitle"
i18n-id="timelion.help.mainPageTitle"
i18n-default-message="Help"
></h2>
<timelion-help-tabs
activate-tab="activateTab"
active-tab="activeTab"
>
</timelion-help-tabs>
<div ng-show="activeTab == 'funcref'" class="list-group-item list-group-item--noBorder">
<div class="timApp__helpText">
<span
i18n-id="timelion.help.mainPage.functionReference.gettingStartedText"
i18n-default-message="Click any function for more information. Just getting started?"
></span>
<a
i18n-id="timelion.help.mainPage.functionReference.welcomePageLinkText"
i18n-default-message="Check out the tutorial"
style="color: #006BB4;"
ng-click="setPage(1)"
kbn-accessible-click
></a>.
</div>
<div class="timHelp__functions">
<table class="table table-condensed table-bordered">
<tr
class="timHelp__functionsTableRow"
ng-repeat-start="function in functions.list"
ng-class="{active: functions.details === function.name}"
ng-click="functions.details =
(functions.details === function.name ?
null : function.name)"
kbn-accessible-click
>
<td><strong>.{{function.name}}()</strong></td>
<td>{{function.help}}</td>
</tr>
<tr ng-if="functions.details === function.name" ng-repeat-end>
<td colspan=2>
<div class="suggestion-details" >
<table
class="table table-condensed table-bordered
timHelp__functionDetailsTable"
ng-show="function.args.length > (function.chainable ? 1: 0)"
>
<thead>
<th
scope="col"
i18n-id="timelion.help.mainPage.functionReference.detailsTable.argumentNameColumnLabel"
i18n-default-message="Argument Name"
></th>
<th
scope="col"
i18n-id="timelion.help.mainPage.functionReference.detailsTable.acceptedTypesColumnLabel"
i18n-default-message="Accepted Types"
></th>
<th
scope="col"
i18n-id="timelion.help.mainPage.functionReference.detailsTable.informationColumnLabel"
i18n-default-message="Information"
></th>
</thead>
<tr
ng-repeat="arg in function.args"
ng-hide="$index < 1 && function.chainable"
>
<td>{{arg.name}}</td>
<td><em>{{arg.types.join(', ')}}</em></td>
<td>{{arg.help}}</td>
</tr>
</table>
<div ng-hide="function.args.length > (function.chainable ? 1: 0)">
<em
i18n-id="timelion.help.mainPage.functionReference.noArgumentsFunctionErrorMessage"
i18n-default-message="This function does not accept any arguments. Well that's simple, isn't it?"
></em>
</div>
</div>
</td>
</tr>
</table>
</div>
</div>
<div ng-show="activeTab == 'keyboardtips'" class="list-group-item list-group-item--noBorder">
<!-- General editing tips -->
<dl class="dl-horizontal">
<dd>
<strong
i18n-id="timelion.help.mainPage.keyboardTips.generalEditingTitle"
i18n-default-message="General editing"
></strong></dd>
<dt></dt>
<dt>Ctrl/Cmd + Enter</dt>
<dd
i18n-id="timelion.help.mainPage.keyboardTips.generalEditing.submitRequestText"
i18n-default-message="Submit request"
></dd>
</dl>
<!-- Auto complete tips -->
<dl class="dl-horizontal">
<dt></dt>
<dd>
<strong
i18n-id="timelion.help.mainPage.keyboardTips.autoCompleteTitle"
i18n-default-message="When auto-complete is visible"
></strong>
</dd>
<dt
i18n-id="timelion.help.mainPage.keyboardTips.autoComplete.downArrowLabel"
i18n-default-message="Down arrow"
></dt>
<dd
i18n-id="timelion.help.mainPage.keyboardTips.autoComplete.downArrowDescription"
i18n-default-message="Switch focus to auto-complete menu. Use arrows to further select a term"
></dd>
<dt>Enter/Tab</dt>
<dd
i18n-id="timelion.help.mainPage.keyboardTips.autoComplete.enterTabDescription"
i18n-default-message="Select the currently selected or the top most term in auto-complete menu"
></dd>
<dt>Esc</dt>
<dd
i18n-id="timelion.help.mainPage.keyboardTips.autoComplete.escDescription"
i18n-default-message="Close auto-complete menu"
></dd>
</dl>
</div>
</div>

View file

@ -1,155 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import template from './timelion_help.html';
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import moment from 'moment';
export function initTimelionHelpDirective(app) {
app.directive('timelionHelp', function ($http) {
return {
restrict: 'E',
template,
controller: function ($scope) {
$scope.functions = {
list: [],
details: null,
};
$scope.activeTab = 'funcref';
$scope.activateTab = function (tabName) {
$scope.activeTab = tabName;
};
function init() {
$scope.es = {
invalidCount: 0,
};
$scope.translations = {
nextButtonLabel: i18n.translate('timelion.help.nextPageButtonLabel', {
defaultMessage: 'Next',
}),
previousButtonLabel: i18n.translate('timelion.help.previousPageButtonLabel', {
defaultMessage: 'Previous',
}),
dontShowHelpButtonLabel: i18n.translate('timelion.help.dontShowHelpButtonLabel', {
defaultMessage: `Don't show this again`,
}),
strongNextText: i18n.translate('timelion.help.welcome.content.strongNextText', {
defaultMessage: 'Next',
}),
emphasizedEverythingText: i18n.translate(
'timelion.help.welcome.content.emphasizedEverythingText',
{
defaultMessage: 'everything',
}
),
notValidAdvancedSettingsPath: i18n.translate(
'timelion.help.configuration.notValid.advancedSettingsPathText',
{
defaultMessage: 'Management / Kibana / Advanced Settings',
}
),
validAdvancedSettingsPath: i18n.translate(
'timelion.help.configuration.valid.advancedSettingsPathText',
{
defaultMessage: 'Management/Kibana/Advanced Settings',
}
),
esAsteriskQueryDescription: i18n.translate(
'timelion.help.querying.esAsteriskQueryDescriptionText',
{
defaultMessage: 'hey Elasticsearch, find everything in my default index',
}
),
esIndexQueryDescription: i18n.translate(
'timelion.help.querying.esIndexQueryDescriptionText',
{
defaultMessage: 'use * as the q (query) for the logstash-* index',
}
),
strongAddText: i18n.translate('timelion.help.expressions.strongAddText', {
defaultMessage: 'Add',
}),
twoExpressionsDescriptionTitle: i18n.translate(
'timelion.help.expressions.examples.twoExpressionsDescriptionTitle',
{
defaultMessage: 'Double the fun.',
}
),
customStylingDescriptionTitle: i18n.translate(
'timelion.help.expressions.examples.customStylingDescriptionTitle',
{
defaultMessage: 'Custom styling.',
}
),
namedArgumentsDescriptionTitle: i18n.translate(
'timelion.help.expressions.examples.namedArgumentsDescriptionTitle',
{
defaultMessage: 'Named arguments.',
}
),
groupedExpressionsDescriptionTitle: i18n.translate(
'timelion.help.expressions.examples.groupedExpressionsDescriptionTitle',
{
defaultMessage: 'Grouped expressions.',
}
),
};
getFunctions();
checkElasticsearch();
}
function getFunctions() {
return $http.get('../api/timelion/functions').then(function (resp) {
$scope.functions.list = resp.data;
});
}
$scope.recheckElasticsearch = function () {
$scope.es.valid = null;
checkElasticsearch().then(function (valid) {
if (!valid) $scope.es.invalidCount++;
});
};
function checkElasticsearch() {
return $http.get('../api/timelion/validate/es').then(function (resp) {
if (resp.data.ok) {
$scope.es.valid = true;
$scope.es.stats = {
min: moment(resp.data.min).format('LLL'),
max: moment(resp.data.max).format('LLL'),
field: resp.data.field,
};
} else {
$scope.es.valid = false;
$scope.es.invalidReason = (function () {
try {
const esResp = JSON.parse(resp.data.resp.response);
return _.get(esResp, 'error.root_cause[0].reason');
} catch (e) {
if (_.get(resp, 'data.resp.message')) return _.get(resp, 'data.resp.message');
if (_.get(resp, 'data.resp.output.payload.message'))
return _.get(resp, 'data.resp.output.payload.message');
return i18n.translate('timelion.help.unknownErrorMessage', {
defaultMessage: 'Unknown error',
});
}
})();
}
return $scope.es.valid;
});
}
init();
},
};
});
}

View file

@ -1 +0,0 @@
@import './timelion_interval';

View file

@ -1,30 +0,0 @@
timelion-interval {
display: flex;
}
.timInterval__input {
width: $euiSizeXL * 2;
padding: $euiSizeXS $euiSizeM;
color: $euiColorDarkestShade;
border: 1px solid $euiColorLightShade;
border-radius: $euiSizeXS;
transition: border-color .1s linear;
font-size: 14px;
}
.timInterval__input--compact {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.timInterval__presets {
width: $euiSizeXL * 3;
}
.timInterval__presets--compact {
width: $euiSizeXL * 1;
padding-left: 0;
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

View file

@ -1,22 +0,0 @@
<input
input-focus
aria-label="{{ ::'timelion.intervals.customIntervalAriaLabel' | i18n: { defaultMessage: 'Custom interval' } }}"
class="timInterval__input"
ng-show="interval === 'other'"
ng-class="{ 'timInterval__input--compact': interval === 'other' }"
ng-model="otherInterval"
><select
id="timelionInterval"
aria-label="{{ ::'timelion.intervals.selectIntervalAriaLabel' | i18n: { defaultMessage: 'Select interval' } }}"
class="form-control timInterval__presets"
ng-class="{ 'timInterval__presets--compact': interval === 'other'}"
ng-model="interval"
>
<option
ng-repeat="intervalOption in intervalOptions"
aria-label="{{::intervalLabels[intervalOption]}}"
value="{{::intervalOption}}"
>
{{::intervalOption}}
</option>
</select>

View file

@ -1,72 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
import $ from 'jquery';
import template from './timelion_interval.html';
export function TimelionInterval($timeout) {
return {
restrict: 'E',
scope: {
// The interval model
model: '=',
changeInterval: '=',
},
template,
link: function ($scope, $elem) {
$scope.intervalOptions = ['auto', '1s', '1m', '1h', '1d', '1w', '1M', '1y', 'other'];
$scope.intervalLabels = {
auto: 'auto',
'1s': '1 second',
'1m': '1 minute',
'1h': '1 hour',
'1d': '1 day',
'1w': '1 week',
'1M': '1 month',
'1y': '1 year',
other: 'other',
};
$scope.$watch('model', function (newVal, oldVal) {
// Only run this on initialization
if (newVal !== oldVal || oldVal == null) return;
if (_.includes($scope.intervalOptions, newVal)) {
$scope.interval = newVal;
} else {
$scope.interval = 'other';
}
if (newVal !== 'other') {
$scope.otherInterval = newVal;
}
});
$scope.$watch('interval', function (newVal, oldVal) {
if (newVal === oldVal || $scope.model === newVal) return;
if (newVal === 'other') {
$scope.otherInterval = oldVal;
$scope.changeInterval($scope.otherInterval);
$timeout(function () {
$('input', $elem).select();
}, 0);
} else {
$scope.otherInterval = $scope.interval;
$scope.changeInterval($scope.interval);
}
});
$scope.$watch('otherInterval', function (newVal, oldVal) {
if (newVal === oldVal || $scope.model === newVal) return;
$scope.changeInterval(newVal);
});
},
};
}

View file

@ -1,19 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import template from '../partials/load_sheet.html';
export function initTimelionLoadSheetDirective(app) {
app.directive('timelionLoad', function () {
return {
replace: true,
restrict: 'E',
template,
};
});
}

View file

@ -1,19 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import template from '../partials/sheet_options.html';
export function initTimelionOptionsSheetDirective(app) {
app.directive('timelionOptions', function () {
return {
replace: true,
restrict: 'E',
template,
};
});
}

View file

@ -1,19 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import saveTemplate from '../partials/save_sheet.html';
export function initTimelionSaveSheetDirective(app) {
app.directive('timelionSave', function () {
return {
replace: true,
restrict: 'E',
template: saveTemplate,
};
});
}

View file

@ -1,80 +0,0 @@
<timelion-app class="timApp app-container">
<span class="timApp__title">
<span class="timApp__stats" ng-show="stats">
<span
i18n-id="timelion.topNavMenu.statsDescription"
i18n-default-message="Query Time {queryTime}ms / Processing Time {processingTime}ms"
i18n-values="{
queryTime: stats.queryTime - stats.invokeTime,
processingTime: stats.sheetTime - stats.queryTime,
}"></span>
</span>
</span>
<!-- Local nav. -->
<timelion-top-nav top-nav-menu="topNavMenu" on-time-update="onTimeUpdate"></timelion-top-nav>
<div class="timApp__menus">
<timelion-deprecation></timelion-deprecation>
<timelion-help ng-show="menus.showHelp"></timelion-help>
<timelion-save ng-show="menus.showSave"></timelion-save>
<timelion-load ng-show="menus.showLoad"></timelion-load>
<timelion-options ng-show="menus.showOptions"></timelion-options>
</div>
<div class="timApp__container">
<div>
<!-- Search. -->
<form
role="form"
ng-submit="updateChart()"
class="timApp__form"
>
<div class="timApp__expression">
<timelion-expression-input
sheet="expression"
rows="1"
update-chart="updateChart()"
should-popover-suggestions="true"
></timelion-expression-input>
</div>
<div>
<timelion-interval
model="state.interval"
change-interval="changeInterval"
></timelion-interval>
<button
type="submit"
aria-label="{{ ::'timelion.search.submitAriaLabel' | i18n: { defaultMessage: 'Search' } }}"
class="timApp__button fullWidth"
>
<icon type="'play'"></icon>
</button>
</div>
</form>
<div>
<timelion-fullscreen
ng-show="transient.fullscreen"
transient="transient"
state="state"
series="sheet[state.selected]"
expression="state.sheet[state.selected]"
on-search="search"
></timelion-fullscreen>
<timelion-cells
ng-show="!transient.fullscreen"
transient="transient"
state="state"
sheet="sheet"
on-search="search"
on-select="setActiveCell"
on-remove-sheet="removeSheet"
></timelion-cells>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,18 +0,0 @@
/* Timelion plugin styles */
// Prefix all styles with "tim" to avoid conflicts.
// Examples
// timChart
// timChart__legend
// timChart__legend--small
// timChart__legend-isLoading
@import './app';
@import './base';
@import './directives/index';
// these styles is needed to be loaded here explicitly if the timelion visualization was not opened in browser
// styles for timelion visualization are lazy loaded only while a vis is opened
// this will duplicate styles only if both Timelion app and timelion visualization are loaded
// could be left here as it is since the Timelion app is deprecated
@import '../../vis_type_timelion/public/legacy/timelion_vis.scss';

View file

@ -1,14 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PluginInitializerContext } from 'kibana/public';
import { TimelionPlugin as Plugin } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new Plugin(initializerContext);
}

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export default function ($elem, fn, frequency) {
frequency = frequency || 500;
let currentHeight = $elem.height();
let currentWidth = $elem.width();
let timeout;
function checkLoop() {
timeout = setTimeout(function () {
if (currentHeight !== $elem.height() || currentWidth !== $elem.width()) {
currentHeight = $elem.height();
currentWidth = $elem.width();
if (currentWidth > 0 && currentWidth > 0) fn();
}
checkLoop();
}, frequency);
}
checkLoop();
return function () {
clearTimeout(timeout);
};
}

View file

@ -1,34 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
interface PanelConfig {
help?: string;
render?: Function;
}
export class Panel {
name: string;
help: string;
render: Function | undefined;
constructor(name: string, config: PanelConfig) {
this.name = name;
this.help = config.help || '';
this.render = config.render;
if (!config.render) {
throw new Error(
i18n.translate('timelion.panels.noRenderFunctionErrorMessage', {
defaultMessage: 'Panel must have a rendering function',
})
);
}
}
}

View file

@ -1,386 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
import $ from 'jquery';
import moment from 'moment-timezone';
// @ts-ignore
import observeResize from '../../lib/observe_resize';
import { _LEGACY_ as visTypeTimelion } from '../../../../vis_type_timelion/public';
import { TimelionVisualizationDependencies } from '../../application';
const DEBOUNCE_DELAY = 50;
export function timechartFn(dependencies: TimelionVisualizationDependencies) {
const {
$rootScope,
$compile,
uiSettings,
data: {
query: { timefilter },
},
} = dependencies;
return function () {
return {
help: 'Draw a timeseries chart',
render($scope: any, $elem: any) {
const template = '<div class="chart-top-title"></div><div class="chart-canvas"></div>';
const formatters = visTypeTimelion.tickFormatters() as any;
const getxAxisFormatter = visTypeTimelion.xaxisFormatterProvider(uiSettings);
const generateTicks = visTypeTimelion.generateTicksProvider();
// TODO: I wonder if we should supply our own moment that sets this every time?
// could just use angular's injection to provide a moment service?
moment.tz.setDefault(uiSettings.get('dateFormat:tz'));
const render = $scope.seriesList.render || {};
$scope.chart = $scope.seriesList.list;
$scope.interval = $scope.interval;
$scope.search = $scope.search || _.noop;
let legendValueNumbers: any;
let legendCaption: any;
const debouncedSetLegendNumbers = _.debounce(setLegendNumbers, DEBOUNCE_DELAY, {
maxWait: DEBOUNCE_DELAY,
leading: true,
trailing: false,
});
// ensure legend is the same height with or without a caption so legend items do not move around
const emptyCaption = '<br>';
const defaultOptions = {
xaxis: {
mode: 'time',
tickLength: 5,
timezone: 'browser',
},
selection: {
mode: 'x',
color: '#ccc',
},
crosshair: {
mode: 'x',
color: '#C66',
lineWidth: 2,
},
grid: {
show: render.grid,
borderWidth: 0,
borderColor: null,
margin: 10,
hoverable: true,
autoHighlight: false,
},
legend: {
backgroundColor: 'rgb(255,255,255,0)',
position: 'nw',
labelBoxBorderColor: 'rgb(255,255,255,0)',
labelFormatter(label: any, series: any) {
const wrapperSpan = document.createElement('span');
const labelSpan = document.createElement('span');
const numberSpan = document.createElement('span');
wrapperSpan.setAttribute('class', 'ngLegendValue');
wrapperSpan.setAttribute('kbn-accessible-click', '');
wrapperSpan.setAttribute('ng-click', `toggleSeries(${series._id})`);
wrapperSpan.setAttribute('ng-focus', `focusSeries(${series._id})`);
wrapperSpan.setAttribute('ng-mouseover', `highlightSeries(${series._id})`);
labelSpan.setAttribute('ng-non-bindable', '');
labelSpan.appendChild(document.createTextNode(label));
numberSpan.setAttribute('class', 'ngLegendValueNumber');
wrapperSpan.appendChild(labelSpan);
wrapperSpan.appendChild(numberSpan);
return wrapperSpan.outerHTML;
},
},
colors: [
'#01A4A4',
'#C66',
'#D0D102',
'#616161',
'#00A1CB',
'#32742C',
'#F18D05',
'#113F8C',
'#61AE24',
'#D70060',
],
};
const originalColorMap = new Map();
$scope.chart.forEach((series: any, seriesIndex: any) => {
if (!series.color) {
const colorIndex = seriesIndex % defaultOptions.colors.length;
series.color = defaultOptions.colors[colorIndex];
}
originalColorMap.set(series, series.color);
});
let highlightedSeries: any;
let focusedSeries: any;
function unhighlightSeries() {
if (highlightedSeries === null) {
return;
}
highlightedSeries = null;
focusedSeries = null;
$scope.chart.forEach((series: any) => {
series.color = originalColorMap.get(series); // reset the colors
});
drawPlot($scope.chart);
}
$scope.highlightSeries = _.debounce(function (id: any) {
if (highlightedSeries === id) {
return;
}
highlightedSeries = id;
$scope.chart.forEach((series: any, seriesIndex: any) => {
if (seriesIndex !== id) {
series.color = 'rgba(128,128,128,0.1)'; // mark as grey
} else {
series.color = originalColorMap.get(series); // color it like it was
}
});
drawPlot($scope.chart);
}, DEBOUNCE_DELAY);
$scope.focusSeries = function (id: any) {
focusedSeries = id;
$scope.highlightSeries(id);
};
$scope.toggleSeries = function (id: any) {
const series = $scope.chart[id];
series._hide = !series._hide;
drawPlot($scope.chart);
};
const cancelResize = observeResize($elem, function () {
drawPlot($scope.chart);
});
$scope.$on('$destroy', function () {
cancelResize();
$elem.off('plothover');
$elem.off('plotselected');
$elem.off('mouseleave');
});
$elem.on('plothover', function (event: any, pos: any, item: any) {
$rootScope.$broadcast('timelionPlotHover', event, pos, item);
});
$elem.on('plotselected', function (event: any, ranges: any) {
timefilter.timefilter.setTime({
from: moment(ranges.xaxis.from),
to: moment(ranges.xaxis.to),
});
});
$elem.on('mouseleave', function () {
$rootScope.$broadcast('timelionPlotLeave');
});
$scope.$on('timelionPlotHover', function (angularEvent: any, flotEvent: any, pos: any) {
if (!$scope.plot) return;
$scope.plot.setCrosshair(pos);
debouncedSetLegendNumbers(pos);
});
$scope.$on('timelionPlotLeave', function () {
if (!$scope.plot) return;
$scope.plot.clearCrosshair();
clearLegendNumbers();
});
// Shamelessly borrowed from the flotCrosshairs example
function setLegendNumbers(pos: any) {
unhighlightSeries();
const plot = $scope.plot;
const axes = plot.getAxes();
if (pos.x < axes.xaxis.min || pos.x > axes.xaxis.max) {
return;
}
let i;
const dataset = plot.getData();
if (legendCaption) {
legendCaption.text(
moment(pos.x).format(
_.get(dataset, '[0]._global.legend.timeFormat', visTypeTimelion.DEFAULT_TIME_FORMAT)
)
);
}
for (i = 0; i < dataset.length; ++i) {
const series = dataset[i];
const useNearestPoint = series.lines.show && !series.lines.steps;
const precision = _.get(series, '_meta.precision', 2);
if (series._hide) continue;
const currentPoint = series.data.find((point: any, index: number) => {
if (index + 1 === series.data.length) {
return true;
}
if (useNearestPoint) {
return pos.x - point[0] < series.data[index + 1][0] - pos.x;
} else {
return pos.x < series.data[index + 1][0];
}
});
const y = currentPoint[1];
if (y != null) {
let label = y.toFixed(precision);
if (series.yaxis.tickFormatter) {
label = series.yaxis.tickFormatter(label, series.yaxis);
}
legendValueNumbers.eq(i).text(`(${label})`);
} else {
legendValueNumbers.eq(i).empty();
}
}
}
function clearLegendNumbers() {
if (legendCaption) {
legendCaption.html(emptyCaption);
}
_.each(legendValueNumbers, function (num) {
$(num).empty();
});
}
let legendScope = $scope.$new();
function drawPlot(plotConfig: any) {
if (!$('.chart-canvas', $elem).length) $elem.html(template);
const canvasElem = $('.chart-canvas', $elem);
// we can't use `$.plot` to draw the chart when the height or width is 0
// so, we'll need another event to trigger drawPlot to actually draw it
if (canvasElem.height() === 0 || canvasElem.width() === 0) {
return;
}
const title = _(plotConfig).map('_title').compact().last() as any;
$('.chart-top-title', $elem).text(title == null ? '' : title);
const options = _.cloneDeep(defaultOptions) as any;
// Get the X-axis tick format
const time = timefilter.timefilter.getBounds() as any;
const interval = visTypeTimelion.calculateInterval(
time.min.valueOf(),
time.max.valueOf(),
uiSettings.get('timelion:target_buckets') || 200,
$scope.interval,
uiSettings.get('timelion:min_interval') || '1ms'
);
const format = getxAxisFormatter(interval);
// Use moment to format ticks so we get timezone correction
options.xaxis.tickFormatter = function (val: any) {
return moment(val).format(format);
};
// Calculate how many ticks can fit on the axis
const tickLetterWidth = 7;
const tickPadding = 45;
options.xaxis.ticks = Math.floor(
$elem.width() / (format.length * tickLetterWidth + tickPadding)
);
const series = _.map(plotConfig, function (serie: any, index) {
serie = _.cloneDeep(
_.defaults(serie, {
shadowSize: 0,
lines: {
lineWidth: 3,
},
})
);
serie._id = index;
if (serie.color) {
const span = document.createElement('span');
span.style.color = serie.color;
serie.color = span.style.color;
}
if (serie._hide) {
serie.data = [];
serie.stack = false;
// serie.color = "#ddd";
serie.label = '(hidden) ' + serie.label;
}
if (serie._global) {
_.mergeWith(options, serie._global, function (objVal, srcVal) {
// This is kind of gross, it means that you can't replace a global value with a null
// best you can do is an empty string. Deal with it.
if (objVal == null) return srcVal;
if (srcVal == null) return objVal;
});
}
return serie;
});
if (options.yaxes) {
options.yaxes.forEach((yaxis: any) => {
if (yaxis && yaxis.units) {
yaxis.tickFormatter = formatters[yaxis.units.type];
const byteModes = ['bytes', 'bytes/s'];
if (byteModes.includes(yaxis.units.type)) {
yaxis.tickGenerator = generateTicks;
}
}
});
}
// @ts-ignore
$scope.plot = $.plot(canvasElem, _.compact(series), options);
if ($scope.plot) {
$scope.$emit('timelionChartRendered');
}
legendScope.$destroy();
legendScope = $scope.$new();
// Used to toggle the series, and for displaying values on hover
legendValueNumbers = canvasElem.find('.ngLegendValueNumber');
_.each(canvasElem.find('.ngLegendValue'), function (elem) {
$compile(elem)(legendScope);
});
if (_.get($scope.plot.getData(), '[0]._global.legend.showTime', true)) {
legendCaption = $('<caption class="timChart__legendCaption"></caption>');
legendCaption.html(emptyCaption);
canvasElem.find('div.legend table').append(legendCaption);
// legend has been re-created. Apply focus on legend element when previously set
if (focusedSeries || focusedSeries === 0) {
const $legendLabels = canvasElem.find('div.legend table .legendLabel>span');
$legendLabels.get(focusedSeries).focus();
}
}
}
$scope.$watch('chart', drawPlot);
},
};
};
}

View file

@ -1,17 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { timechartFn } from './schema';
import { Panel } from '../panel';
import { TimelionVisualizationDependencies } from '../../application';
export function getTimeChart(dependencies: TimelionVisualizationDependencies) {
// Schema is broken out so that it may be extended for use in other plugins
// Its also easier to test.
return new Panel('timechart', timechartFn(dependencies)());
}

View file

@ -1,12 +0,0 @@
<form role="form" ng-submit="fetch()">
<h2
class="timApp__sectionTitle"
i18n-id="timelion.topNavMenu.openSheetTitle"
i18n-default-message="Open Sheet"
></h2>
<saved-object-finder
type="timelion-sheet"
use-local-management="true"
></saved-object-finder>
</form>

View file

@ -1,107 +0,0 @@
<div class="list-group">
<button class="list-group-item" ng-click="section = 'sheet'" type="button" data-test-subj="timelionSaveAsSheetButton">
<h4
class="list-group-item-heading"
i18n-id="timelion.topNavMenu.save.saveEntireSheetTitle"
i18n-default-message="Save entire Timelion sheet"
></h4>
<p
class="list-group-item-text"
i18n-id="timelion.topNavMenu.save.saveEntireSheetDescription"
i18n-default-message="You want this option if you mostly use Timelion expressions from within
the Timelion app and don't need to add Timelion charts to Kibana
dashboards. You may also want this if you make use of references to
other panels."
></p>
</button>
<div class="list-group-item" ng-show="section == 'sheet'">
<form role="form" class="container-fluid" ng-submit="opts.saveSheet()">
<label
for="savedSheet"
class="timApp__label"
i18n-id="timelion.topNavMenu.save.saveEntireSheetLabel"
i18n-default-message="Save sheet as"
></label>
<input
id="savedSheet"
ng-model="opts.savedSheet.title"
input-focus="select"
class="form-control"
style="margin-bottom: 4px;"
placeholder="{{ ::'timelion.topNavMenu.save.saveEntireSheet.inputPlaceholder' | i18n: { defaultMessage: 'Name this sheet...' } }}"
aria-label="{{ ::'timelion.topNavMenu.save.saveEntireSheet.inputAriaLabel' | i18n: { defaultMessage: 'Name' } }}"
>
<saved-object-save-as-check-box
saved-object="opts.savedSheet"
style="margin-bottom: 4px;"
></saved-object-save-as-check-box>
<button
ng-disabled="!opts.savedSheet.title"
type="submit"
class="timApp__button"
i18n-id="timelion.topNavMenu.save.saveEntireSheet.submitButtonLabel"
i18n-default-message="Save"
data-test-subj="timelionFinishSaveButton"
></button>
</form>
</div>
<button class="list-group-item" ng-click="section = 'expression'" type="button">
<h4
class="list-group-item-heading"
i18n-id="timelion.topNavMenu.save.saveAsDashboardPanelTitle"
i18n-default-message="Save current expression as Kibana dashboard panel"
></h4>
<p
class="list-group-item-text"
i18n-id="timelion.topNavMenu.save.saveAsDashboardPanelDescription"
i18n-default-message="Need to add a chart to a Kibana dashboard? We can do that! This option
will save your currently selected expression as a panel that can be
added to Kibana dashboards as you would add anything else. Note, if you
use references to other panels you will need to remove the refences by
copying the referenced expression directly into the expression you are
saving. Click a chart to select a different expression to save."
></p>
</button>
<div class="list-group-item" ng-show="section == 'expression'">
<form role="form" class="container-fluid" ng-submit="opts.saveExpression(panelTitle)">
<div class="form-group">
<label
class="control-label"
i18n-id="timelion.topNavMenu.save.saveAsDashboardPanel.selectedExpressionLabel"
i18n-default-message="Currently selected expression"
></label>
<code>{{opts.state.sheet[opts.state.selected]}}</code>
</div>
<div class="form-group">
<label
for="savedExpression"
class="control-label"
i18n-id="timelion.topNavMenu.save.saveAsDashboardPanelLabel"
i18n-default-message="Save expression as"
></label>
<input
id="savedExpression"
ng-model="panelTitle"
input-focus="select"
class="form-control"
placeholder="{{ ::'timelion.topNavMenu.save.saveAsDashboardPanel.inputPlaceholder' | i18n: { defaultMessage: 'Name this panel' } }}"
>
</div>
<div class="form-group">
<button
ng-disabled="!panelTitle"
type="submit"
class="timApp__button"
i18n-id="timelion.topNavMenu.save.saveAsDashboardPanel.submitButtonLabel"
i18n-default-message="Save"
></button>
</div>
</form>
</div>
</div>

View file

@ -1,36 +0,0 @@
<form role="form">
<h2
class="timApp__sectionTitle"
i18n-id="timelion.topNavMenu.sheetOptionsTitle"
i18n-default-message="Sheet options"
></h2>
<div class="clearfix">
<div class="form-group col-md-6">
<label
for="timelionColCount"
i18n-id="timelion.topNavMenu.options.columnsCountLabel"
i18n-default-message="Columns (Column count must divide evenly into 12)"
></label>
<select class="form-control"
id="timelionColCount"
ng-change="opts.search()"
ng-options="column for column in [1, 2, 3, 4, 6, 12]"
ng-model="opts.state.columns">
</select>
</div>
<div class="form-group col-md-6">
<label
for="timelionRowCount"
i18n-id="timelion.topNavMenu.options.rowsCountLabel"
i18n-default-message="Rows (This is a target based on the current window height)"
></label>
<select class="form-control"
id="timelionRowCount"
ng-change="opts.search()"
ng-options="row for row in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]"
ng-model="opts.state.rows">
</select>
</div>
</div>
</form>

View file

@ -1,140 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { BehaviorSubject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import {
CoreSetup,
Plugin,
PluginInitializerContext,
DEFAULT_APP_CATEGORIES,
AppMountParameters,
AppUpdater,
ScopedHistory,
AppNavLinkStatus,
} from '../../../core/public';
import { Panel } from './panels/panel';
import { KibanaLegacyStart } from '../../kibana_legacy/public';
import { createKbnUrlTracker } from '../../kibana_utils/public';
import { DataPublicPluginStart, esFilters, DataPublicPluginSetup } from '../../data/public';
import { NavigationPublicPluginStart } from '../../navigation/public';
import { VisualizationsStart } from '../../visualizations/public';
import { SavedObjectsStart } from '../../saved_objects/public';
import {
VisTypeTimelionPluginStart,
VisTypeTimelionPluginSetup,
} from '../../vis_type_timelion/public';
export interface TimelionPluginSetupDependencies {
data: DataPublicPluginSetup;
visTypeTimelion: VisTypeTimelionPluginSetup;
}
export interface TimelionPluginStartDependencies {
data: DataPublicPluginStart;
navigation: NavigationPublicPluginStart;
visualizations: VisualizationsStart;
visTypeTimelion: VisTypeTimelionPluginStart;
savedObjects: SavedObjectsStart;
kibanaLegacy: KibanaLegacyStart;
}
/** @internal */
export class TimelionPlugin
implements Plugin<void, void, TimelionPluginSetupDependencies, TimelionPluginStartDependencies> {
initializerContext: PluginInitializerContext;
private appStateUpdater = new BehaviorSubject<AppUpdater>(() => ({}));
private stopUrlTracking: (() => void) | undefined = undefined;
private currentHistory: ScopedHistory | undefined = undefined;
constructor(initializerContext: PluginInitializerContext) {
this.initializerContext = initializerContext;
}
public setup(
core: CoreSetup<TimelionPluginStartDependencies>,
{
data,
visTypeTimelion,
}: { data: DataPublicPluginSetup; visTypeTimelion: VisTypeTimelionPluginSetup }
) {
const timelionPanels: Map<string, Panel> = new Map();
const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({
baseUrl: core.http.basePath.prepend('/app/timelion'),
defaultSubUrl: '#/',
storageKey: `lastUrl:${core.http.basePath.get()}:timelion`,
navLinkUpdater$: this.appStateUpdater,
toastNotifications: core.notifications.toasts,
stateParams: [
{
kbnUrlKey: '_g',
stateUpdate$: data.query.state$.pipe(
filter(
({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval)
),
map(({ state }) => ({
...state,
filters: state.filters?.filter(esFilters.isFilterPinned),
}))
),
},
],
getHistory: () => this.currentHistory!,
});
this.stopUrlTracking = () => {
stopUrlTracker();
};
core.application.register({
id: 'timelion',
title: 'Timelion',
order: 8000,
defaultPath: '#/',
euiIconType: 'logoKibana',
category: DEFAULT_APP_CATEGORIES.kibana,
navLinkStatus:
visTypeTimelion.isUiEnabled === false ? AppNavLinkStatus.hidden : AppNavLinkStatus.default,
mount: async (params: AppMountParameters) => {
const [coreStart, pluginsStart] = await core.getStartServices();
await pluginsStart.kibanaLegacy.loadAngularBootstrap();
this.currentHistory = params.history;
appMounted();
const unlistenParentHistory = params.history.listen(() => {
window.dispatchEvent(new HashChangeEvent('hashchange'));
});
const { renderApp } = await import('./application');
params.element.classList.add('timelionAppContainer');
const unmount = renderApp({
mountParams: params,
pluginInitializerContext: this.initializerContext,
timelionPanels,
core: coreStart,
plugins: pluginsStart,
});
return () => {
unlistenParentHistory();
unmount();
appUnMounted();
};
},
});
}
public start() {}
public stop(): void {
if (this.stopUrlTracking) {
this.stopUrlTracking();
}
}
}

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