ydb-embedded-ui 1.4.2 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (27) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/components/GroupTreeViewer/GroupTreeViewer.js +3 -2
  3. package/dist/components/GroupTreeViewer/GroupTreeViewer.scss +0 -2
  4. package/dist/components/SplitPane/SplitPane.tsx +8 -8
  5. package/dist/containers/App/App.js +1 -0
  6. package/dist/containers/App/App.scss +0 -26
  7. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesViewer/IssueViewer.scss +11 -23
  8. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesViewer/IssuesViewer.js +7 -7
  9. package/dist/containers/Tenant/ObjectSummary/ObjectSummary.scss +9 -15
  10. package/dist/containers/Tenant/ObjectSummary/ObjectSummary.tsx +16 -15
  11. package/dist/containers/Tenant/QueryEditor/QueryEditor.js +2 -2
  12. package/dist/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx +64 -0
  13. package/dist/containers/Tenant/Tenant.tsx +2 -2
  14. package/dist/containers/Tenant/utils/schema.ts +17 -0
  15. package/dist/containers/Tenant/utils/schemaActions.ts +130 -0
  16. package/dist/services/api.d.ts +3 -0
  17. package/dist/services/api.js +2 -2
  18. package/package.json +8 -4
  19. package/dist/components/TreeView/TreeView.js +0 -60
  20. package/dist/components/TreeView/TreeView.scss +0 -39
  21. package/dist/containers/Tenant/Schema/SchemaNode/SchemaNode.js +0 -170
  22. package/dist/containers/Tenant/Schema/SchemaNode/SchemaNode.scss +0 -62
  23. package/dist/containers/Tenant/Schema/SchemaNodeActions/SchemaNodeActions.scss +0 -17
  24. package/dist/containers/Tenant/Schema/SchemaNodeActions/SchemaNodeActions.tsx +0 -125
  25. package/dist/containers/Tenant/Schema/SchemaTree/SchemaTree.js +0 -116
  26. package/dist/containers/Tenant/Schema/SchemaTree/SchemaTree.scss +0 -17
  27. package/dist/styles/react-treeview.scss +0 -45
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -13,7 +13,7 @@
13
13
  "@testing-library/react": "11.2.7",
14
14
  "@testing-library/user-event": "12.8.3",
15
15
  "@types/qs": "6.9.7",
16
- "@yandex-cloud/i18n": "0.2.0",
16
+ "@yandex-cloud/i18n": "0.4.0",
17
17
  "@yandex-cloud/paranoid": "1.0.0",
18
18
  "@yandex-cloud/react-data-table": "0.2.1",
19
19
  "axios": "0.19.2",
@@ -34,13 +34,13 @@
34
34
  "react-scripts": "4.0.3",
35
35
  "react-split": "2.0.14",
36
36
  "react-transition-group": "4.4.2",
37
- "react-treeview": "0.4.7",
38
37
  "redux": "4.0.1",
39
38
  "redux-location-state": "2.6.0",
40
39
  "redux-thunk": "2.3.0",
41
40
  "reselect": "4.0.0",
42
41
  "sass": "1.32.8",
43
- "web-vitals": "1.1.2"
42
+ "web-vitals": "1.1.2",
43
+ "ydb-ui-components": "1.2.3"
44
44
  },
45
45
  "scripts": {
46
46
  "start": "react-app-rewired start",
@@ -88,6 +88,7 @@
88
88
  "@types/react-transition-group": "^4.4.4",
89
89
  "@types/react-virtualized-auto-sizer": "^1.0.1",
90
90
  "@yandex-cloud/axios-wrapper": "^1.0.2",
91
+ "@yandex-cloud/browserslist-config": "1.0.1",
91
92
  "@yandex-cloud/eslint-config": "^1.0.0",
92
93
  "@yandex-cloud/prettier-config": "^1.0.0",
93
94
  "@yandex-cloud/stylelint-config": "^1.1.0",
@@ -104,5 +105,8 @@
104
105
  "react-dom": "^17.0.2",
105
106
  "stylelint": "^14.3.0",
106
107
  "typescript": "^4.5.5"
108
+ },
109
+ "peerDependencies": {
110
+ "@yandex-cloud/browserslist-config": "^1.0.1"
107
111
  }
108
112
  }
@@ -1,60 +0,0 @@
1
- import PropTypes from 'prop-types';
2
- import cn from 'bem-cn-lite';
3
- import TreeViewBase from 'react-treeview';
4
- import Icon from '../Icon/Icon';
5
-
6
- import './TreeView.scss';
7
-
8
- const b = cn('km-tree-view');
9
-
10
- const TreeView = (props) => {
11
- const {
12
- children,
13
- nodeLabel,
14
- onClick,
15
- collapsed,
16
- clickableLabel = false,
17
- className,
18
- hasArrow = true,
19
- ...rest
20
- } = props;
21
-
22
- const newNodeLabel = (
23
- <div
24
- className={b('node-wrapper', {clickable: clickableLabel})}
25
- onClick={clickableLabel ? onClick : undefined}
26
- >
27
- {hasArrow ? (
28
- <span
29
- className={b('arrow-icon', {extended: !collapsed})}
30
- onClick={clickableLabel ? undefined : onClick}
31
- >
32
- <Icon name="arrow-right" viewBox="0 0 6 11" width={6} height={11} />
33
- </span>
34
- ) : null}
35
- {nodeLabel}
36
- </div>
37
- );
38
-
39
- return (
40
- <TreeViewBase
41
- {...rest}
42
- treeViewClassName={b(null, className)}
43
- nodeLabel={newNodeLabel}
44
- collapsed={collapsed}
45
- >
46
- {children}
47
- </TreeViewBase>
48
- );
49
- };
50
-
51
- TreeView.propTypes = {
52
- children: PropTypes.any,
53
- nodeLabel: PropTypes.node,
54
- onClick: PropTypes.func,
55
- collapsed: PropTypes.bool,
56
- clickableLabel: PropTypes.bool,
57
- className: PropTypes.string,
58
- };
59
-
60
- export default TreeView;
@@ -1,39 +0,0 @@
1
- .km-tree-view {
2
- & .tree-view_arrow {
3
- display: none;
4
- }
5
-
6
- &__node-wrapper {
7
- display: flex;
8
- overflow: hidden;
9
- align-items: center;
10
-
11
- width: 100%;
12
-
13
- &_clickable {
14
- cursor: pointer;
15
- }
16
- }
17
-
18
- &__arrow-icon {
19
- display: flex;
20
- justify-content: center;
21
-
22
- width: 11px;
23
- margin-right: 8px;
24
-
25
- cursor: pointer;
26
-
27
- &_extended {
28
- transform: rotate(90deg);
29
- }
30
-
31
- & .yc-icon {
32
- vertical-align: middle;
33
- }
34
- }
35
- .yc-icon {
36
- display: flex;
37
- flex-shrink: 0;
38
- }
39
- }
@@ -1,170 +0,0 @@
1
- import React from 'react';
2
- import {connect} from 'react-redux';
3
- import PropTypes from 'prop-types';
4
- import cn from 'bem-cn-lite';
5
-
6
- import TreeView from '../../../../components/TreeView/TreeView';
7
- import SchemaTree from '../../../Tenant/Schema/SchemaTree/SchemaTree';
8
- import Icon from '../../../../components/Icon/Icon';
9
-
10
- import {getSchema, setCurrentSchemaPath} from '../../../../store/reducers/schema';
11
- import {getDescribe} from '../../../../store/reducers/describe';
12
- import {getSchemaAcl} from '../../../../store/reducers/schemaAcl';
13
-
14
- import './SchemaNode.scss';
15
- import SchemaNodeActions from '../SchemaNodeActions/SchemaNodeActions';
16
- import {isTableType} from '../../Tenant';
17
-
18
- const b = cn('schema-node');
19
-
20
- export const SUBDOMAIN_FOLDER_TYPE = 'EPathTypeSubDomain';
21
- export const TABLE_TYPE = 'EPathTypeTable';
22
- export const OLAP_TABLE_TYPE = 'EPathTypeOlapTable';
23
-
24
- export const FOLDERS_TYPE = ['EPathTypeDir', 'EPathTypeExtSubDomain', 'EPathTypeOlapStore'];
25
-
26
- class SchemaNode extends React.Component {
27
- static propTypes = {
28
- data: PropTypes.object.isRequired,
29
- fullPath: PropTypes.string.isRequired,
30
- getSchema: PropTypes.func.isRequired,
31
- setCurrentSchemaPath: PropTypes.func,
32
- currentSchemaPath: PropTypes.string,
33
- isRoot: PropTypes.bool,
34
- };
35
-
36
- state = {
37
- collapsed: true,
38
- active: false,
39
- };
40
-
41
- schemaNodeRef = React.createRef();
42
-
43
- componentDidMount() {
44
- const {currentSchemaPath, isRoot} = this.props;
45
- const schemaPath = this.getSchemaPath();
46
-
47
- if (schemaPath === currentSchemaPath && !this.state.active) {
48
- this.addActiveClass();
49
- }
50
-
51
- if (
52
- (currentSchemaPath &&
53
- currentSchemaPath.startsWith(schemaPath) &&
54
- currentSchemaPath !== schemaPath) ||
55
- isRoot
56
- ) {
57
- this.setState({collapsed: false});
58
- }
59
- }
60
-
61
- componentDidUpdate() {
62
- const {currentSchemaPath} = this.props;
63
- const schemaPath = this.getSchemaPath();
64
-
65
- if (schemaPath === currentSchemaPath && !this.state.active) {
66
- this.addActiveClass();
67
- }
68
- }
69
-
70
- getSchemaPath = () => {
71
- const {data, fullPath, isRoot} = this.props;
72
-
73
- return isRoot ? fullPath : `${fullPath}/${data.Name}`;
74
- };
75
-
76
- invertCollapsed = () => {
77
- this.setState({collapsed: !this.state.collapsed});
78
- };
79
-
80
- setIcon = (data) => {
81
- const viewBox = '0 0 16 16';
82
- const {collapsed} = this.state;
83
- if (FOLDERS_TYPE.indexOf(data.PathType) !== -1) {
84
- return collapsed ? (
85
- <Icon name="folder" viewBox={viewBox} width={16} height={16} />
86
- ) : (
87
- <Icon name="openFolder" viewBox={viewBox} width={16} height={16} />
88
- );
89
- } else if (data.PathType === TABLE_TYPE || data.PathType === OLAP_TABLE_TYPE) {
90
- return <Icon name="table" viewBox={viewBox} width={16} height={16} />;
91
- } else if (data.PathType === SUBDOMAIN_FOLDER_TYPE) {
92
- return <Icon name="subdomain" viewBox={viewBox} width={16} height={16} />;
93
- }
94
- };
95
-
96
- addActiveClass = () => {
97
- const activeClass = 'schema-node_active';
98
- const currentActiveSchemaNode = document.querySelector(`.${activeClass}`);
99
- if (currentActiveSchemaNode) {
100
- currentActiveSchemaNode.classList.remove(activeClass);
101
- }
102
- const activeNode = this.schemaNodeRef.current;
103
- if (activeNode) {
104
- this.setState({active: true});
105
- activeNode.classList.add(activeClass);
106
- }
107
- };
108
-
109
- handleClick = (e) => {
110
- const {getSchema, getDescribe, getSchemaAcl, setCurrentSchemaPath} = this.props;
111
- e.stopPropagation();
112
- this.addActiveClass();
113
-
114
- const schemaPath = this.getSchemaPath();
115
- setCurrentSchemaPath(schemaPath);
116
- getSchema({path: schemaPath});
117
- getDescribe({path: schemaPath});
118
- getSchemaAcl({path: schemaPath});
119
- };
120
-
121
- render() {
122
- const {data, fullPath, isRoot = false, currentSchemaPath, currentItem = {}} = this.props;
123
- const {collapsed} = this.state;
124
-
125
- if (!data) {
126
- return null;
127
- }
128
- const currentPathType = currentItem.PathDescription?.Self?.PathType;
129
- const type = isTableType(currentPathType);
130
-
131
- const hasArrow = data.PathType !== TABLE_TYPE;
132
- const label = (
133
- <div className={b('label')}>
134
- {this.setIcon(data)}
135
- <div className={b('name-wrapper')}>
136
- <div className={b('name')}>{data.Name}</div>
137
- <SchemaNodeActions name={currentSchemaPath} isTableType={type} />
138
- </div>
139
- </div>
140
- );
141
- return (
142
- <div onClick={this.handleClick} ref={this.schemaNodeRef}>
143
- <TreeView
144
- nodeLabel={label}
145
- collapsed={collapsed}
146
- onClick={this.invertCollapsed}
147
- hasArrow={hasArrow}
148
- >
149
- <SchemaTree path={isRoot ? fullPath : `${fullPath}/${data.Name}`} />
150
- </TreeView>
151
- </div>
152
- );
153
- }
154
- }
155
-
156
- function mapStateToProps(state) {
157
- return {
158
- currentSchemaPath: state.schema.currentSchemaPath,
159
- currentItem: state.schema.currentSchema,
160
- };
161
- }
162
-
163
- const mapDispatchToProps = {
164
- getSchema,
165
- getDescribe,
166
- getSchemaAcl,
167
- setCurrentSchemaPath,
168
- };
169
-
170
- export default connect(mapStateToProps, mapDispatchToProps)(SchemaNode);
@@ -1,62 +0,0 @@
1
- .schema-node {
2
- display: flex;
3
- align-items: center;
4
-
5
- .tree-view_item {
6
- flex-wrap: wrap;
7
-
8
- margin: 0;
9
- padding: 5px 0;
10
- }
11
-
12
- .tree-view_children {
13
- margin: 0;
14
- padding-left: 15px;
15
- }
16
-
17
- &__label {
18
- display: flex;
19
- overflow: hidden;
20
- align-items: center;
21
-
22
- width: 100%;
23
- height: 20px;
24
- padding: 0 5px;
25
-
26
- cursor: pointer;
27
-
28
- & > svg {
29
- color: var(--yc-color-text-hint);
30
- }
31
- }
32
-
33
- &_active > .tree-view > .tree-view_item {
34
- font-weight: 600;
35
-
36
- background-color: var(--yc-color-base-info);
37
- }
38
-
39
- &__name-wrapper {
40
- display: inline-flex;
41
- justify-content: space-between;
42
-
43
- width: 100%;
44
- }
45
-
46
- &__name {
47
- display: inline;
48
- overflow: hidden;
49
-
50
- margin-left: 6px;
51
-
52
- white-space: nowrap;
53
- text-overflow: ellipsis;
54
- }
55
- }
56
-
57
- .tree-view_item {
58
- border-bottom: 1px solid var(--yc-color-line-generic);
59
- &:hover {
60
- background-color: var(--yc-color-base-simple-hover);
61
- }
62
- }
@@ -1,17 +0,0 @@
1
- .kv-schema-node-actions {
2
- visibility: hidden;
3
-
4
- &__popup {
5
- .yc-menu__list-item {
6
- line-height: 36px;
7
- }
8
- }
9
- }
10
-
11
- .tree-view_item {
12
- &:hover {
13
- .kv-schema-node-actions {
14
- visibility: visible;
15
- }
16
- }
17
- }
@@ -1,125 +0,0 @@
1
- import {useDispatch} from 'react-redux';
2
- import {useHistory} from 'react-router';
3
- import cn from 'bem-cn-lite';
4
- import {DropdownMenu} from '@yandex-cloud/uikit';
5
- import qs from 'qs';
6
-
7
- import {changeUserInput} from '../../../../store/reducers/executeQuery';
8
- import {setShowPreview} from '../../../../store/reducers/schema';
9
- import routes, {createHref} from '../../../../routes';
10
-
11
- import './SchemaNodeActions.scss';
12
- import {TenantGeneralTabsIds, TenantTabsGroups} from '../../TenantPages';
13
- import createToast from '../../../../utils/createToast';
14
-
15
- const b = cn('kv-schema-node-actions');
16
-
17
- const createTableTemplate = (path: string) => {
18
- return `CREATE TABLE \`${path}/my_table\`
19
- (
20
- \`id\` Uint64,
21
- \`name\` String,
22
- PRIMARY KEY (\`id\`)
23
- );`;
24
- };
25
-
26
- const alterTableTemplate = (path: string) => {
27
- return `ALTER TABLE \`${path}\`
28
- ADD COLUMN is_deleted Bool;`;
29
- };
30
- const selectQueryTemplate = (path: string) => {
31
- return `SELECT \`id\`, \`name\`
32
- FROM \`${path}\`
33
- ORDER BY \`id\`
34
- LIMIT 10;`;
35
- };
36
- const upsertQueryTemplate = (path: string) => {
37
- return `UPSERT INTO \`${path}\`
38
- ( \`id\`, \`name\` )
39
- VALUES ( );`;
40
- };
41
-
42
- interface SchemaNodeActionsProps {
43
- name: string;
44
- isTableType: boolean;
45
- }
46
-
47
- function SchemaNodeActions({name, isTableType}: SchemaNodeActionsProps) {
48
- const dispatch = useDispatch();
49
- const history = useHistory();
50
-
51
- const queryParams = qs.parse(location.search, {
52
- ignoreQueryPrefix: true,
53
- });
54
-
55
- const onCreateTableClick = () => {
56
- dispatch(changeUserInput({input: createTableTemplate(name)}));
57
- };
58
-
59
- const onAlterTableClick = () => {
60
- dispatch(changeUserInput({input: alterTableTemplate(name)}));
61
- };
62
-
63
- const onSelectQueryClick = () => {
64
- dispatch(changeUserInput({input: selectQueryTemplate(name)}));
65
- };
66
-
67
- const onUpsertQueryClick = () => {
68
- dispatch(changeUserInput({input: upsertQueryTemplate(name)}));
69
- };
70
-
71
- const onCopyPathClick = () => {
72
- navigator.clipboard
73
- .writeText(name)
74
- .then(() => {
75
- createToast({
76
- name: 'Copied',
77
- title: 'Path was copied to clipboard successfully',
78
- type: 'success',
79
- });
80
- })
81
- .catch(() => {
82
- createToast({
83
- name: 'Not copied',
84
- title: 'Path was not copied to clipboard successfully',
85
- type: 'error',
86
- });
87
- });
88
- };
89
-
90
- const onOpenPreviewClick = () => {
91
- dispatch(setShowPreview(true));
92
- history.push(
93
- createHref(routes.tenant, undefined, {
94
- ...queryParams,
95
- [TenantTabsGroups.general]: TenantGeneralTabsIds.query,
96
- }),
97
- );
98
- };
99
-
100
- const copyItem = {text: 'Copy path', action: onCopyPathClick};
101
-
102
- const tableItems = [
103
- [{text: 'Open preview', action: onOpenPreviewClick}, copyItem],
104
- [
105
- {text: 'Alter table', action: onAlterTableClick},
106
- {text: 'Select query', action: onSelectQueryClick},
107
- {text: 'Upsert query', action: onUpsertQueryClick},
108
- ],
109
- ];
110
-
111
- const catalogItems = [[copyItem], [{text: 'Create table', action: onCreateTableClick}]];
112
-
113
- const items = isTableType ? tableItems : catalogItems;
114
-
115
- return (
116
- <DropdownMenu
117
- items={items}
118
- switcherWrapperClassName={b()}
119
- popupClassName={b('popup')}
120
- popupPlacement={['bottom-end']}
121
- />
122
- );
123
- }
124
-
125
- export default SchemaNodeActions;
@@ -1,116 +0,0 @@
1
- import React from 'react';
2
- import ReactDOM from 'react-dom';
3
- import cn from 'bem-cn-lite';
4
- import PropTypes from 'prop-types';
5
- import {connect} from 'react-redux';
6
- import {Loader} from '@yandex-cloud/uikit';
7
- import {getSchema} from '../../../../store/reducers/schema';
8
- import './SchemaTree.scss';
9
-
10
- import SchemaNode from '../SchemaNode/SchemaNode';
11
-
12
- const b = cn('schema');
13
-
14
- class SchemaTree extends React.Component {
15
- static propTypes = {
16
- className: PropTypes.string,
17
- loading: PropTypes.bool,
18
- error: PropTypes.object,
19
- schema: PropTypes.object,
20
- wasLoaded: PropTypes.bool,
21
- getSchema: PropTypes.func,
22
- path: PropTypes.string.isRequired,
23
- tenantPath: PropTypes.string,
24
- };
25
-
26
- componentDidMount() {
27
- const {path: tenantPath, getSchema} = this.props;
28
-
29
- getSchema({path: tenantPath});
30
- }
31
-
32
- emptySchema = React.createRef();
33
-
34
- renderLoader() {
35
- return (
36
- <div className={b('loader')}>
37
- <Loader size="m" />
38
- </div>
39
- );
40
- }
41
-
42
- removeArrow = () => {
43
- // And how else to solve this problem, except to get into the DOM?
44
- const nodeWithArrow =
45
- // eslint-disable-next-line react/no-find-dom-node
46
- ReactDOM.findDOMNode(this)?.parentNode?.parentNode?.querySelector('.tree-view_arrow');
47
- if (nodeWithArrow) {
48
- nodeWithArrow.setAttribute('style', 'visibility: hidden');
49
- }
50
-
51
- return '';
52
- };
53
-
54
- showEmptyNode() {
55
- const {tenantPath, schema} = this.props;
56
- if (schema.Path === tenantPath) {
57
- return 'no data';
58
- } else {
59
- return String(this.removeArrow());
60
- }
61
- }
62
-
63
- renderContent = () => {
64
- const {schema, path} = this.props;
65
- if (schema && schema.Status === 'StatusSuccess') {
66
- return (
67
- <div className={b()}>
68
- {schema && schema.PathDescription && schema.PathDescription.Children ? (
69
- schema.PathDescription.Children.map((it, key) => (
70
- <SchemaNode key={key} fullPath={path} data={it} />
71
- ))
72
- ) : (
73
- <div ref={this.emptySchema}>{this.showEmptyNode()}</div>
74
- )}
75
- </div>
76
- );
77
- } else {
78
- return null;
79
- }
80
- };
81
-
82
- render() {
83
- const {loading, wasLoaded, error, currentSchema: schema} = this.props;
84
-
85
- if (loading && !wasLoaded) {
86
- return this.renderLoader();
87
- } else if (
88
- (error && !error.isCancelled) ||
89
- (schema && schema.Status === 'StatusAccessDenied')
90
- ) {
91
- return <div>{error?.statusText || 'Access denied'}</div>;
92
- } else {
93
- return this.renderContent();
94
- }
95
- }
96
- }
97
-
98
- function mapStateToProps(state, ownProps) {
99
- const {data: schema = {}, loading, wasLoaded, error, currentSchema} = state.schema;
100
- const tenantPath = state.tenant.tenant.Name;
101
- const {path} = ownProps;
102
- return {
103
- tenantPath,
104
- schema: schema[`${path}`],
105
- loading,
106
- wasLoaded,
107
- error,
108
- currentSchema,
109
- };
110
- }
111
-
112
- const mapDispatchToProps = {
113
- getSchema,
114
- };
115
-
116
- export default connect(mapStateToProps, mapDispatchToProps)(SchemaTree);
@@ -1,17 +0,0 @@
1
- @import '../../../../styles/mixins.scss';
2
-
3
- .schema {
4
- .tree-view_item {
5
- margin: 0;
6
- padding: 2px 0;
7
- }
8
-
9
- .tree-view_children {
10
- margin: 0;
11
- padding-left: 15px;
12
- }
13
- &__loader {
14
- display: flex;
15
- justify-content: center;
16
- }
17
- }
@@ -1,45 +0,0 @@
1
- /* the tree node's style */
2
-
3
- .tree-view {
4
- overflow-y: hidden;
5
- }
6
-
7
- .tree-view_item {
8
- display: flex;
9
- align-items: center;
10
-
11
- margin: 10px 0;
12
-
13
- cursor: pointer;
14
- white-space: nowrap;
15
- }
16
-
17
- .tree-view_children {
18
- margin-left: 35px;
19
-
20
- cursor: pointer;
21
- white-space: nowrap;
22
- }
23
-
24
- .tree-view_children-collapsed {
25
- height: 0px;
26
- }
27
-
28
- .tree-view_arrow {
29
- display: inline-block;
30
-
31
- margin-right: 6px;
32
-
33
- cursor: pointer;
34
- user-select: none;
35
- }
36
-
37
- .tree-view_arrow:after {
38
- content: '▾';
39
- }
40
-
41
- /* rotate the triangle to close it */
42
-
43
- .tree-view_arrow-collapsed {
44
- transform: rotate(-90deg);
45
- }