diff --git a/package.json b/package.json index 80fa9a699f8..d7fb5d8e6f8 100644 --- a/package.json +++ b/package.json @@ -153,6 +153,7 @@ "react-popper": "^0.7.5", "react-select": "^1.1.0", "react-sizeme": "^2.3.6", + "react-transition-group": "^2.2.1", "remarkable": "^1.7.1", "rst2html": "github:thoward/rst2html#990cb89", "rxjs": "^5.4.3", diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index 3214382732a..637f811969e 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -6,11 +6,14 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import AddPermissions from 'app/core/components/Permissions/AddPermissions'; +import SlideDown from 'app/core/components/Animations/SlideDown'; @inject('nav', 'folder', 'view', 'permissions') @observer export class FolderPermissions extends Component { constructor(props) { super(props); + this.handleAddPermission = this.handleAddPermission.bind(this); this.loadStore(); } @@ -21,6 +24,11 @@ export class FolderPermissions extends Component { }); } + handleAddPermission() { + const { permissions } = this.props; + permissions.toggleAddPermissions(); + } + render() { const { nav, folder, permissions, backendSrv } = this.props; @@ -34,13 +42,23 @@ export class FolderPermissions extends Component {
-
+

Folder Permissions

+
+
- + + +
diff --git a/public/app/core/components/Animations/SlideDown.tsx b/public/app/core/components/Animations/SlideDown.tsx new file mode 100644 index 00000000000..4d515f98f16 --- /dev/null +++ b/public/app/core/components/Animations/SlideDown.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import Transition from 'react-transition-group/Transition'; + +const defaultMaxHeight = '200px'; // When animating using max-height we need to use a static value. +// If this is not enough, pass in + +
+
Add Permission For
+
+
+
+ +
+
+ + {newItem.type === 'User' ? ( +
+ +
+ ) : null} + + {newItem.type === 'Group' ? ( +
+ +
+ ) : null} + +
+ +
+ +
+ +
+
+
+ {permissions.error ? ( +
+ + + {permissions.error} + +
+ ) : null} +
+ ); + } +} + +export default AddPermissions; diff --git a/public/app/core/components/Permissions/DashboardPermissions.tsx b/public/app/core/components/Permissions/DashboardPermissions.tsx index 2636b0d4db4..a1b86e121bf 100644 --- a/public/app/core/components/Permissions/DashboardPermissions.tsx +++ b/public/app/core/components/Permissions/DashboardPermissions.tsx @@ -1,8 +1,11 @@ import React, { Component } from 'react'; +import { observer } from 'mobx-react'; import { store } from 'app/stores/store'; import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import AddPermissions from 'app/core/components/Permissions/AddPermissions'; +import SlideDown from 'app/core/components/Animations/SlideDown'; export interface IProps { dashboardId: number; @@ -11,26 +14,44 @@ export interface IProps { folderSlug: string; backendSrv: any; } - +@observer class DashboardPermissions extends Component { permissions: any; constructor(props) { super(props); + this.handleAddPermission = this.handleAddPermission.bind(this); this.permissions = store.permissions; } + handleAddPermission() { + this.permissions.toggleAddPermissions(); + } + render() { const { dashboardId, folderTitle, folderSlug, folderId, backendSrv } = this.props; return (
-

Permissions

- - - +
+

Permissions

+ + + +
+ +
+ + + { - let wrapper; - - beforeAll(() => { - backendSrv.get.mockReturnValue( - Promise.resolve([ - { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, - { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, - { - id: 4, - dashboardId: 1, - userId: 2, - userLogin: 'danlimerick', - userEmail: 'dan.limerick@gmail.com', - permission: 4, - permissionName: 'Admin', - }, - ]) - ); - - backendSrv.post = jest.fn(); - - const store = RootStore.create( - {}, - { - backendSrv: backendSrv, - } - ); - - wrapper = shallow(); - return wrapper.instance().loadStore(1, true); - }); - - describe('when permission for a user is added', () => { - it('should save permission to db', () => { - const userItem = { - id: 2, - login: 'user2', - }; - - wrapper - .instance() - .userPicked(userItem) - .then(() => { - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); - }); - }); - }); - - describe('when permission for team is added', () => { - it('should save permission to db', () => { - const teamItem = { - id: 2, - name: 'ug1', - }; - - wrapper - .instance() - .teamPicked(teamItem) - .then(() => { - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); - }); - }); - }); -}); diff --git a/public/app/core/components/Permissions/Permissions.tsx b/public/app/core/components/Permissions/Permissions.tsx index 647a9060b0d..0a0572ed86e 100644 --- a/public/app/core/components/Permissions/Permissions.tsx +++ b/public/app/core/components/Permissions/Permissions.tsx @@ -1,9 +1,6 @@ -import React, { Component } from 'react'; +import React, { Component } from 'react'; import PermissionsList from './PermissionsList'; import { observer } from 'mobx-react'; -import UserPicker, { User } from 'app/core/components/Picker/UserPicker'; -import TeamPicker, { Team } from 'app/core/components/Picker/TeamPicker'; -import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore'; import { FolderInfo } from './FolderInfo'; export interface DashboardAcl { @@ -40,8 +37,6 @@ class Permissions extends Component { this.permissionChanged = this.permissionChanged.bind(this); this.typeChanged = this.typeChanged.bind(this); this.removeItem = this.removeItem.bind(this); - this.userPicked = this.userPicked.bind(this); - this.teamPicked = this.teamPicked.bind(this); this.loadStore(dashboardId, isFolder, folderInfo && folderInfo.id === 0); } @@ -77,28 +72,8 @@ class Permissions extends Component { permissions.setNewType(value); } - userPicked(user: User) { - const { permissions, dashboardId } = this.props; - return permissions.addStoreItem({ - userId: user.id, - userLogin: user.login, - permission: 1, - dashboardId: dashboardId, - }); - } - - teamPicked(team: Team) { - const { permissions, dashboardId } = this.props; - return permissions.addStoreItem({ - teamId: team.id, - team: team.name, - permission: 1, - dashboardId: dashboardId, - }); - } - render() { - const { permissions, folderInfo, backendSrv } = this.props; + const { permissions, folderInfo } = this.props; return (
@@ -109,50 +84,6 @@ class Permissions extends Component { fetching={permissions.fetching} folderInfo={folderInfo} /> -
-
-
Add Permission For
-
-
-
- -
-
- - {permissions.newType === 'User' ? ( -
- -
- ) : null} - - {permissions.newType === 'Group' ? ( -
- -
- ) : null} -
-
- {permissions.error ? ( -
- - - {permissions.error} - -
- ) : null} -
); } diff --git a/public/app/core/components/Picker/TeamPicker.jest.tsx b/public/app/core/components/Picker/TeamPicker.jest.tsx new file mode 100644 index 00000000000..20b7620e0ac --- /dev/null +++ b/public/app/core/components/Picker/TeamPicker.jest.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import TeamPicker from './TeamPicker'; + +const model = { + backendSrv: { + get: () => { + return new Promise((resolve, reject) => {}); + }, + }, + handlePicked: () => {}, +}; + +describe('TeamPicker', () => { + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/Picker/TeamPicker.tsx b/public/app/core/components/Picker/TeamPicker.tsx index 4b5a049c0ff..2dfff1850dd 100644 --- a/public/app/core/components/Picker/TeamPicker.tsx +++ b/public/app/core/components/Picker/TeamPicker.tsx @@ -9,6 +9,8 @@ export interface IProps { isLoading: boolean; toggleLoading: any; handlePicked: (user) => void; + value?: string; + className?: string; } export interface Team { @@ -54,7 +56,7 @@ class TeamPicker extends Component { render() { const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; - const { isLoading, handlePicked } = this.props; + const { isLoading, handlePicked, value, className } = this.props; return (
@@ -66,10 +68,13 @@ class TeamPicker extends Component { isLoading={isLoading} loadOptions={this.debouncedSearch} loadingPlaceholder="Loading..." + noResultsText="No teams found" onChange={handlePicked} - className="width-8 gf-form-input gf-form-input--form-dropdown" + className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`} optionComponent={PickerOption} placeholder="Choose" + value={value} + autosize={true} />
); diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index 141371813a2..5c36505aeaa 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -9,6 +9,8 @@ export interface IProps { isLoading: boolean; toggleLoading: any; handlePicked: (user) => void; + value?: string; + className?: string; } export interface User { @@ -53,8 +55,7 @@ class UserPicker extends Component { render() { const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; - const { isLoading, handlePicked } = this.props; - + const { isLoading, handlePicked, value, className } = this.props; return (
{ loadingPlaceholder="Loading..." noResultsText="No users found" onChange={handlePicked} - className="width-8 gf-form-input gf-form-input--form-dropdown" + className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`} optionComponent={PickerOption} placeholder="Choose" + value={value} + autosize={true} />
); diff --git a/public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap new file mode 100644 index 00000000000..67232d0ea5b --- /dev/null +++ b/public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TeamPicker renders correctly 1`] = ` +
+
+
+ +
+ Loading... +
+
+ +
+ +
+
+
+
+
+
+`; diff --git a/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap index a1563ba8bc3..3262dc10efe 100644 --- a/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap +++ b/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap @@ -5,7 +5,7 @@ exports[`UserPicker renders correctly 1`] = ` className="user-picker" >
void; + value?: string; + className?: string; } export default function withPicker(WrappedComponent) { diff --git a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts index 3608e0a9017..97a9906d0e5 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts @@ -1,4 +1,4 @@ -import { PermissionsStore } from './PermissionsStore'; +import { PermissionsStore, aclTypeValues } from './PermissionsStore'; import { backendSrv } from 'test/mocks/common'; describe('PermissionsStore', () => { @@ -47,21 +47,6 @@ describe('PermissionsStore', () => { expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); }); - it('should save newly added permissions automatically', () => { - expect(store.items.length).toBe(3); - - const newItem = { - userId: 10, - userLogin: 'tester1', - permission: 1, - }; - store.addStoreItem(newItem); - - expect(store.items.length).toBe(4); - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); - }); - it('should save removed permissions automatically', () => { expect(store.items.length).toBe(3); @@ -72,54 +57,48 @@ describe('PermissionsStore', () => { expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); }); + describe('when duplicate team permissions are added', () => { + beforeEach(() => { + const newItem = { + teamId: 10, + team: 'tester-team', + permission: 1, + dashboardId: 1, + }; + store.resetNewType(); + store.newItem.setTeam(newItem.teamId, newItem.team); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); + + store.newItem.setTeam(newItem.teamId, newItem.team); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); + }); + + it('should return a validation error', () => { + expect(store.items.length).toBe(4); + expect(store.error).toBe('This permission exists already.'); + expect(backendSrv.post.mock.calls.length).toBe(1); + }); + }); + describe('when duplicate user permissions are added', () => { beforeEach(() => { + expect(store.items.length).toBe(3); const newItem = { userId: 10, userLogin: 'tester1', permission: 1, dashboardId: 1, }; - store.addStoreItem(newItem); - store.addStoreItem(newItem); - }); - - it('should return a validation error', () => { - expect(store.items.length).toBe(4); - expect(store.error).toBe('This permission exists already.'); - expect(backendSrv.post.mock.calls.length).toBe(1); - }); - }); - - describe('when duplicate team permissions are added', () => { - beforeEach(() => { - const newItem = { - teamId: 1, - teamName: 'testerteam', - permission: 1, - dashboardId: 1, - }; - store.addStoreItem(newItem); - store.addStoreItem(newItem); - }); - - it('should return a validation error', () => { - expect(store.items.length).toBe(4); - expect(store.error).toBe('This permission exists already.'); - expect(backendSrv.post.mock.calls.length).toBe(1); - }); - }); - - describe('when duplicate role permissions are added', () => { - beforeEach(() => { - const newItem = { - team: 'MyTestTeam', - teamId: 1, - permission: 1, - dashboardId: 1, - }; - store.addStoreItem(newItem); - store.addStoreItem(newItem); + store.setNewType(aclTypeValues.USER.value); + store.newItem.setUser(newItem.userId, newItem.userLogin); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); + store.setNewType(aclTypeValues.USER.value); + store.newItem.setUser(newItem.userId, newItem.userLogin); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); }); it('should return a validation error', () => { @@ -131,20 +110,24 @@ describe('PermissionsStore', () => { describe('when one inherited and one not inherited team permission are added', () => { beforeEach(() => { - const teamItem = { + const overridingItemForChildDashboard = { team: 'MyTestTeam', dashboardId: 1, teamId: 1, permission: 2, }; - store.addStoreItem(teamItem); + + store.resetNewType(); + store.newItem.setTeam(overridingItemForChildDashboard.teamId, overridingItemForChildDashboard.team); + store.newItem.setPermission(overridingItemForChildDashboard.permission); + store.addStoreItem(); }); - it('should not throw a validation error', () => { + it('should allowing overriding the inherited permission and not throw a validation error', () => { expect(store.error).toBe(null); }); - it('should add both permissions', () => { + it('should add new overriding permission', () => { expect(store.items.length).toBe(4); }); }); diff --git a/public/app/stores/PermissionsStore/PermissionsStore.ts b/public/app/stores/PermissionsStore/PermissionsStore.ts index cb9d8170303..9db76c4e124 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.ts @@ -1,4 +1,4 @@ -import { types, getEnv, flow } from 'mobx-state-tree'; +import { types, getEnv, flow } from 'mobx-state-tree'; import { PermissionsStoreItem } from './PermissionsStoreItem'; const duplicateError = 'This permission exists already.'; @@ -13,15 +13,62 @@ export const permissionOptions = [ }, ]; -export const aclTypes = [ - { value: 'Group', text: 'Team' }, - { value: 'User', text: 'User' }, - { value: 'Viewer', text: 'Everyone With Viewer Role' }, - { value: 'Editor', text: 'Everyone With Editor Role' }, -]; +export const aclTypeValues = { + GROUP: { value: 'Group', text: 'Team' }, + USER: { value: 'User', text: 'User' }, + VIEWER: { value: 'Viewer', text: 'Everyone With Viewer Role' }, + EDITOR: { value: 'Editor', text: 'Everyone With Editor Role' }, +}; + +export const aclTypes = Object.keys(aclTypeValues).map(item => aclTypeValues[item]); const defaultNewType = aclTypes[0].value; +export const NewPermissionsItem = types + .model('NewPermissionsItem', { + type: types.optional( + types.enumeration(Object.keys(aclTypeValues).map(item => aclTypeValues[item].value)), + defaultNewType + ), + userId: types.maybe(types.number), + userLogin: types.maybe(types.string), + teamId: types.maybe(types.number), + team: types.maybe(types.string), + permission: types.optional(types.number, 1), + }) + .views(self => ({ + isValid: () => { + switch (self.type) { + case aclTypeValues.GROUP.value: + return self.teamId && self.team; + case aclTypeValues.USER.value: + return !!self.userId && !!self.userLogin; + case aclTypeValues.VIEWER.value: + case aclTypeValues.EDITOR.value: + return true; + default: + return false; + } + }, + })) + .actions(self => ({ + setUser(userId: number, userLogin: string) { + self.userId = userId; + self.userLogin = userLogin; + self.teamId = null; + self.team = null; + }, + setTeam(teamId: number, team: string) { + self.userId = null; + self.userLogin = null; + self.teamId = teamId; + self.team = team; + }, + setPermission(permission: number) { + self.permission = permission; + }, + })); + export const PermissionsStore = types .model('PermissionsStore', { fetching: types.boolean, @@ -31,6 +78,8 @@ export const PermissionsStore = types error: types.maybe(types.string), originalItems: types.optional(types.array(PermissionsStoreItem), []), newType: types.optional(types.string, defaultNewType), + newItem: types.maybe(NewPermissionsItem), + isAddPermissionsVisible: types.optional(types.boolean, false), isInRoot: types.maybe(types.boolean), }) .views(self => ({ @@ -38,7 +87,6 @@ export const PermissionsStore = types const dupe = self.items.find(it => { return isDuplicate(it, item); }); - if (dupe) { self.error = duplicateError; return false; @@ -47,50 +95,94 @@ export const PermissionsStore = types return true; }, })) - .actions(self => ({ - load: flow(function* load(dashboardId: number, isFolder: boolean, isInRoot: boolean) { - const backendSrv = getEnv(self).backendSrv; - self.fetching = true; - self.isFolder = isFolder; - self.isInRoot = isInRoot; - self.dashboardId = dashboardId; - const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`); - const items = prepareServerResponse(res, dashboardId, isFolder, isInRoot); - self.items = items; - self.originalItems = items; - self.fetching = false; + .actions(self => { + const resetNewType = () => { self.error = null; - }), - addStoreItem: flow(function* addStoreItem(item) { - self.error = null; - if (!self.isValid(item)) { - return undefined; - } + self.newItem = NewPermissionsItem.create(); + }; - self.items.push(prepareItem(item, self.dashboardId, self.isFolder, self.isInRoot)); - return updateItems(self); - }), - removeStoreItem: flow(function* removeStoreItem(idx: number) { - self.error = null; - self.items.splice(idx, 1); - return updateItems(self); - }), - updatePermissionOnIndex: flow(function* updatePermissionOnIndex( - idx: number, - permission: number, - permissionName: string - ) { - self.error = null; - self.items[idx].updatePermission(permission, permissionName); - return updateItems(self); - }), - setNewType(newType: string) { - self.newType = newType; - }, - resetNewType() { - self.newType = defaultNewType; - }, - })); + return { + load: flow(function* load(dashboardId: number, isFolder: boolean, isInRoot: boolean) { + const backendSrv = getEnv(self).backendSrv; + self.fetching = true; + self.isFolder = isFolder; + self.isInRoot = isInRoot; + self.dashboardId = dashboardId; + const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`); + const items = prepareServerResponse(res, dashboardId, isFolder, isInRoot); + self.items = items; + self.originalItems = items; + self.fetching = false; + self.error = null; + }), + addStoreItem: flow(function* addStoreItem() { + self.error = null; + let item = { + type: self.newItem.type, + permission: self.newItem.permission, + dashboardId: self.dashboardId, + team: undefined, + teamId: undefined, + userLogin: undefined, + userId: undefined, + role: undefined, + }; + switch (self.newItem.type) { + case aclTypeValues.GROUP.value: + item.team = self.newItem.team; + item.teamId = self.newItem.teamId; + break; + case aclTypeValues.USER.value: + item.userLogin = self.newItem.userLogin; + item.userId = self.newItem.userId; + break; + case aclTypeValues.VIEWER.value: + case aclTypeValues.EDITOR.value: + item.role = self.newItem.type; + break; + default: + throw Error('Unknown type: ' + self.newItem.type); + } + + if (!self.isValid(item)) { + return undefined; + } + + self.items.push(prepareItem(item, self.dashboardId, self.isFolder, self.isInRoot)); + resetNewType(); + return updateItems(self); + }), + removeStoreItem: flow(function* removeStoreItem(idx: number) { + self.error = null; + self.items.splice(idx, 1); + return updateItems(self); + }), + updatePermissionOnIndex: flow(function* updatePermissionOnIndex( + idx: number, + permission: number, + permissionName: string + ) { + self.error = null; + self.items[idx].updatePermission(permission, permissionName); + return updateItems(self); + }), + setNewType(newType: string) { + self.newItem = NewPermissionsItem.create({ type: newType }); + }, + resetNewType() { + resetNewType(); + }, + toggleAddPermissions() { + self.isAddPermissionsVisible = !self.isAddPermissionsVisible; + }, + showAddPermissions() { + self.isAddPermissionsVisible = true; + }, + hideAddPermissions() { + self.isAddPermissionsVisible = false; + }, + }; + }); const updateItems = self => { self.error = null; @@ -116,7 +208,7 @@ const updateItems = self => { items: updated, }); } catch (error) { - console.error(error); + self.error = error; } return res; diff --git a/public/sass/components/_buttons.scss b/public/sass/components/_buttons.scss index 4c9b197c3d0..c21ed30b0f4 100644 --- a/public/sass/components/_buttons.scss +++ b/public/sass/components/_buttons.scss @@ -113,6 +113,10 @@ //border: 1px solid $tight-form-func-highlight-bg; } +.btn-transparent { + background-color: transparent; +} + .btn-outline-primary { @include button-outline-variant($btn-primary-bg); } diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 2113bbae43c..6603cfa072b 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -274,6 +274,10 @@ $input-border: 1px solid $input-border-color; } } + .gf-form-input { + margin-right: 0; + } + select.gf-form-input { text-indent: 0.01px; text-overflow: ''; @@ -392,3 +396,17 @@ select.gf-form-input ~ .gf-form-help-icon { top: 10px; color: $text-muted; } + +.cta-form { + position: relative; + padding: 1rem; + background-color: $empty-list-cta-bg; + margin-bottom: 1rem; + border-top: 3px solid $green; +} + +.cta-form__close { + position: absolute; + right: 0; + top: 0; +} diff --git a/yarn.lock b/yarn.lock index 5bb00023d7c..e95968736ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1604,6 +1604,10 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" +chain-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc" + chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.0, chalk@~1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2801,6 +2805,10 @@ dom-converter@~0.1: dependencies: utila "~0.3" +dom-helpers@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" + dom-serialize@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" @@ -8318,6 +8326,17 @@ react-test-renderer@^16.0.0, react-test-renderer@^16.0.0-0: object-assign "^4.1.1" prop-types "^15.6.0" +react-transition-group@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.1.tgz#e9fb677b79e6455fd391b03823afe84849df4a10" + dependencies: + chain-function "^1.0.0" + classnames "^2.2.5" + dom-helpers "^3.2.0" + loose-envify "^1.3.1" + prop-types "^15.5.8" + warning "^3.0.0" + react@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" @@ -10355,6 +10374,12 @@ walker@~1.0.5: dependencies: makeerror "1.0.x" +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + dependencies: + loose-envify "^1.0.0" + watch@~0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986"