mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
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:
parent
3c71408690
commit
70090e326c
240 changed files with 147 additions and 10726 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 | - |
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -73,9 +73,6 @@ The API returns the following:
|
|||
"indexPatterns": [
|
||||
"read"
|
||||
],
|
||||
"timelion": [
|
||||
"all"
|
||||
],
|
||||
"graph": [
|
||||
"all"
|
||||
],
|
||||
|
|
|
@ -94,9 +94,6 @@ $ curl -X PUT api/security/role/my_kibana_role
|
|||
"indexPatterns": [
|
||||
"read"
|
||||
],
|
||||
"timelion": [
|
||||
"all"
|
||||
],
|
||||
"graph": [
|
||||
"all"
|
||||
],
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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=="
|
||||
}
|
||||
--------------------------------------------------
|
||||
|
|
|
@ -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*.
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -70,7 +70,6 @@ pageLoadAssetSize:
|
|||
spaces: 57868
|
||||
telemetry: 51957
|
||||
telemetryManagementSection: 38586
|
||||
timelion: 29920
|
||||
transform: 41007
|
||||
triggersActionsUi: 100000
|
||||
uiActions: 97717
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. | - |
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.' },
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -64,13 +64,6 @@
|
|||
},
|
||||
"kibana": {
|
||||
"properties": {
|
||||
"timelion_sheet": {
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualization": {
|
||||
"properties": {
|
||||
"total": {
|
||||
|
|
|
@ -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.
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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('/');
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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" />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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: '=',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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: '=',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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: '=',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import './cells';
|
|
@ -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>
|
|
@ -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);
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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>
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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',
|
||||
};
|
|
@ -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>
|
|
@ -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']);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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>
|
||||
|
|
@ -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: '=',
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import './timelion_expression_suggestions';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import './timelion_help';
|
|
@ -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 */
|
||||
}
|
|
@ -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—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>
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import './timelion_interval';
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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>
|
|
@ -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';
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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',
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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)());
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue