0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-21 21:49:51 +01:00

[WIP] New UI (#2114)

Co-authored-by: Michael Matloka <dev@twixes.com>
This commit is contained in:
Paolo D'Amico 2020-11-05 12:55:33 +00:00 committed by GitHub
parent eb4817b8f3
commit cd251df713
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
103 changed files with 2091 additions and 6049 deletions

View File

@ -5,7 +5,6 @@ module.exports = {
'@babel/plugin-transform-react-jsx',
'@babel/plugin-proposal-class-properties',
'react-hot-loader/babel',
['import', { libraryName: 'antd', libraryDirectory: 'es', style: 'css' }],
['babel-plugin-kea', { path: './frontend/src' }],
],
presets: ['@babel/preset-env', '@babel/typescript'],

View File

@ -10,17 +10,17 @@ describe('Actions', () => {
it('Click on an action', () => {
cy.get('[data-attr=action-link-0]').click()
cy.get('h1').should('contain', 'Edit action')
cy.get('h1').should('contain', 'Editing action')
})
it('Create action', () => {
cy.get('[data-attr=create-action]').click()
cy.get('.ant-card-head-title').should('contain', 'event or pageview')
cy.get('[data-attr=new-action-pageview]').click()
cy.get('h1').should('contain', 'New Action')
cy.get('h1').should('contain', 'Creating action')
cy.get('[data-attr=edit-action-input]').type(Cypress._.random(0, 1e6))
cy.get('[data-attr=action-step-pageview]').click()
cy.get('.ant-radio-group > :nth-child(3)').click()
cy.get('[data-attr=edit-action-url-input]').type(Cypress.config().baseUrl)
cy.get('[data-attr=save-action-button]').click()

View File

@ -9,10 +9,10 @@ describe('Cohorts', () => {
// go to create a new cohort
cy.get('[data-attr="create-cohort"]').click()
cy.get('form.card-body > .form-control').type('Test Cohort')
cy.get('[data-attr="cohort-name"]').type('Test Cohort')
// select "add filter" and "property"
cy.get('[data-attr="cohort-group-property"]').click()
cy.get('.ant-radio-group > :nth-child(2)').click()
cy.get('[data-attr="new-prop-filter-cohort_0"]').click()
// select the first property

View File

@ -7,13 +7,8 @@ describe('People', () => {
cy.get('h1').should('contain', 'Persons')
})
it('Go to new cohort from people screen', () => {
cy.get('[data-attr=create-cohort]').click()
cy.get('span').should('contain', 'New Cohort')
})
it('All tabs work', () => {
cy.get('.form-control').type('has:email').type('{enter}').should('have.value', 'has:email')
cy.get('[data-attr=persons-search]').type('has:email').type('{enter}').should('have.value', 'has:email')
cy.wait(200)
cy.get('.ant-tabs-nav-list > :nth-child(2)').click()
cy.get('[data-row-key="100"] > :nth-child(2) > .ph-no-capture').should('contain', '@')
@ -24,8 +19,9 @@ describe('People', () => {
it('All people route works', () => {
cy.get('[data-attr=menu-item-people-cohorts]').click()
cy.get('[data-attr=menu-item-people-persons]').click()
cy.get('h1').should('contain', 'Cohorts')
cy.get('[data-attr=menu-item-people-persons]').click()
cy.get('h1').should('contain', 'Persons')
})
})

View File

@ -27,8 +27,13 @@ describe('Trends actions & events', () => {
it('Show property select dynamically', () => {
cy.get('[data-attr=math-property-selector-0]').should('not.exist')
cy.get('[data-attr=math-selector-0]').click()
cy.get('[data-attr=math-avg-0]').click()
// Test that the math selector dropdown is shown on hover
cy.get('[data-attr=math-selector-0]').trigger('mouseover')
cy.get('[data-attr=math-total-0]').should('be.visible')
// Use `force = true` because clicking the element without dragging the mouse makes the dropdown disappear
cy.get('[data-attr=math-avg-0]').click({ force: true })
cy.get('[data-attr=math-property-selector-0]').should('exist')
})

20
frontend/src/antd.less Normal file
View File

@ -0,0 +1,20 @@
/* This file sets theming configuration on Ant Design for PostHog. Ant uses LESS which is incompatible
with SASS, which is why configuration is duplicated. To change any variable here, please update vars.scss too */
@import 'antd/lib/style/themes/default.less';
@import 'antd/dist/antd.less';
@text-color: #2d2d2d;
@text-muted: #d9d9d9;
@primary-color: #5375ff;
@link-color: #5375ff;
@success-color: #77b96c;
@warning-color: #f7a501;
@error-color: #f96132;
@font-size-base: 14px;
@heading-color: @text-color;
@text-color-secondary: rgba(0, 0, 0, 0.45);
@disabled-color: @text-muted;
@border-radius-base: 2px;
@border-color-base: #d9d9d9;
@body-background:  #f2f2f2;
@layout-body-background: #fff;

334
frontend/src/global.scss Normal file
View File

@ -0,0 +1,334 @@
/* Only styles that are shared across multiple components (i.e. global) should go here, trying to keep this file
nimble to simplify maintenance. We separate variables and mixins in vars.scss to be able to import those into local
style files without adding already imported styles. */
// Global components
@import 'node_modules/react-toastify/dist/ReactToastify';
@import 'node_modules/react-datepicker/dist/react-datepicker';
@import './vars';
:root {
--primary: #{$primary};
--success: #{$success};
--danger: #{$danger};
--warning: #{$warning};
--bg-menu: #{$bg_menu};
--bg-mid: #{$bg_mid};
// Used for graph series
--blue: #{$blue_500};
--purple: #{purple_500};
--salmon: #ff906e;
--yellow: #ffc035;
--green: #{$success};
--indigo: #{$purple_700};
--cyan: #17a2b8;
--pink: #e83e8c;
--white: #f4f6ff;
}
// Text styles
.text-default {
color: $text_default;
font-size: 14px;
line-height: 20px;
font-weight: 400;
}
.text-small {
@extend .text-default;
font-size: 12px;
}
.text-extra-small {
@extend .text-default;
font-size: 10px;
}
.page-title {
font-size: 28px;
line-height: 34px;
margin-top: 32px;
font-weight: 700;
color: $text_default;
}
.page-caption {
@extend .text-default;
max-width: 640px;
margin-bottom: 32px;
}
.subtitle {
margin-top: 24px;
font-size: 22px;
line-height: 26px;
font-weight: 700;
}
.l3 {
/* Level 3 title (ideally H3) */
font-size: 16px;
font-weight: 700;
line-height: 19px;
}
.text-right {
text-align: right;
}
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-muted {
color: $text_muted;
}
// Spacing & layout
.mb {
margin-bottom: $default_spacing;
}
.mt {
margin-top: $default_spacing;
}
.mb-05 {
margin-bottom: $default_spacing * 0.5;
}
.mt-05 {
margin-top: $default_spacing * 0.5;
}
.mr {
margin-right: $default_spacing;
}
.ml {
margin-left: $default_spacing;
}
.pa {
// Padding all
padding: $default_spacing;
}
.pb {
padding-bottom: $default_spacing;
}
.pt {
padding-top: $default_spacing;
}
.pr {
padding-right: $default_spacing;
}
.pl {
padding-left: $default_spacing;
}
.full-width {
width: 100%;
}
.float-right {
float: right;
}
.float-left {
float: left;
}
.main-app-content {
padding: $default_spacing $default_spacing * 3;
@media (min-width: 480px) and (max-width: 639px) {
padding: $default_spacing $default_spacing * 2 !important;
}
@media (max-width: 480px) {
padding: $default_spacing $default_spacing !important;
}
}
// Color styles
.bg-primary {
background-color: $primary;
}
.text-danger {
color: $danger !important;
}
// Random general styles
.cursor-pointer {
cursor: pointer;
}
// Toasts
.Toastify__toast-container {
opacity: 1;
transform: none;
}
.Toastify__toast {
padding: 16px;
border-radius: $radius;
color: $text_default;
font-family: inherit;
background-color: $bg_light;
}
.Toastify__toast-body {
@extend .l3;
color: $success;
p {
@extend .text-default;
color: $text_default;
}
}
.Toastify__progress-bar--default {
background: $success;
}
.Toastify__toast--error {
h1 {
color: $danger;
}
.Toastify__progress-bar {
background: $danger;
}
.error-details {
font-style: italic;
}
}
// Table styles
.table-bordered td {
border: 1px solid $border;
}
// Card styles
.card-elevated {
@extend .mixin-elevated;
}
// Form & input styles
.input-set {
padding-bottom: $default_spacing;
color: $text_default;
label {
font-weight: bold;
@extend .text-default;
}
.caption {
color: $text_muted;
@extend .text-small;
}
&.errored {
.caption {
color: $danger;
}
.ant-input-password,
input[type='text'] {
border-color: $danger !important;
}
}
}
// Button styles
.btn-close {
color: $text_muted;
}
.ant-btn-sm {
font-size: 12px !important;
}
.ant-btn-md {
// Size between `small` & `default`
font-size: 13px !important;
height: 28px !important;
padding: 0px 10px !important;
}
.info-indicator {
color: $primary !important;
cursor: pointer;
margin-left: 5px;
}
// Overlays styles
#bottom-notice {
z-index: 1000000000;
display: flex;
flex-direction: row;
position: fixed;
width: 100%;
bottom: 0;
left: 0;
background: #000;
color: #fff;
font-size: 0.75rem;
line-height: 1.5rem;
&.warning div {
height: auto;
background: $danger;
}
div:nth-child(1) {
background: $purple_700;
}
div:nth-child(2) {
background: $purple_500;
}
div:nth-child(3) {
background: $purple_300;
}
div {
flex-basis: 0;
flex-grow: 1;
height: 1.5rem;
text-align: center;
}
span {
display: none;
}
button {
border: none;
background: transparent;
width: 1.5rem;
height: 1.5rem;
padding: 0;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
}
@media screen and (min-width: 750px) {
font-size: 1rem;
line-height: 2rem;
div {
height: 2rem;
}
span {
display: inline;
}
button {
width: 2rem;
height: 2rem;
font-size: 1.25rem;
}
}
}

View File

@ -1,4 +1,6 @@
import './style.scss'
import '~/global.scss' /* Contains PostHog's main styling configurations */
import '~/antd.less' /* Imports Ant Design's components */
import './style.scss' /* DEPRECATED */
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'

View File

@ -5,6 +5,7 @@ import { loadersPlugin } from 'kea-loaders'
import { windowValuesPlugin } from 'kea-window-values'
import { toast } from 'react-toastify'
import React from 'react'
import { identifierToHuman } from 'lib/utils'
export function initKea(): void {
resetContext({
@ -16,9 +17,11 @@ export function initKea(): void {
onFailure({ error, reducerKey, actionKey }) {
toast.error(
<div>
<h1>Error loading "{reducerKey}".</h1>
<p className="info">Action "{actionKey}" responded with</p>
<p className="error-message">"{error.message || error.detail}"</p>
<h1>Error on {identifierToHuman(reducerKey)}.</h1>
<p>
Attempting to {identifierToHuman(actionKey, false)} returned an error:{' '}
<span className="error-details">{error.detail}</span>
</p>
</div>
)
window['Sentry'] ? window['Sentry'].captureException(error) : console.error(error)

View File

@ -6,6 +6,8 @@
<title>PostHog</title>
{% include "head.html" %}
<%= htmlWebpackPlugin.tags.headTags %><%/* This adds the main.css file! */%>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet"
crossorigin="anonymous" />
<style>
html,
body {
@ -98,7 +100,7 @@ input:not(:placeholder-shown) + label,
input:focus + label {
transform: translate(0, 0) scale(1);
cursor: pointer;
color: var(--blue)
color: var(--primary)
}
</style>
{% block head %}

View File

@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'
import { router } from 'kea-router'
import { useValues } from 'kea'
import { JSSnippet } from 'lib/components/JSSnippet'
import './SendEventsOverlay.scss'
export function SendEventsOverlay() {
const overlay = useRef()

View File

@ -20,6 +20,7 @@ import {
ClockCircleOutlined,
MessageOutlined,
ProjectOutlined,
SettingOutlined,
LockOutlined,
WalletOutlined,
ApiOutlined,
@ -41,11 +42,8 @@ const itemStyle = { display: 'flex', alignItems: 'center' }
function Logo() {
return (
<div
className="row logo-row d-flex align-items-center justify-content-center"
style={{ margin: 16, height: 42, whiteSpace: 'nowrap', width: 168, overflow: 'hidden' }}
>
<img className="logo posthog-logo" src={whiteLogo} style={{ maxHeight: '100%' }} />
<div className="sidebar-logo">
<img src={whiteLogo} style={{ maxHeight: '100%' }} />
</div>
)
}
@ -53,7 +51,7 @@ function Logo() {
// to show the right page in the sidebar
const sceneOverride = {
action: 'actions',
person: 'people',
person: 'persons',
dashboard: 'dashboards',
featureFlags: 'experiments',
}
@ -63,7 +61,7 @@ const submenuOverride = {
actions: 'events',
liveActions: 'events',
sessions: 'events',
cohorts: 'people',
cohorts: 'persons',
projectSettings: 'project',
plugins: 'project',
organizationSettings: 'organization',
@ -111,16 +109,17 @@ function _Sidebar({ user, sidebarCollapsed, setSidebarCollapsed }) {
<Layout.Sider
breakpoint="lg"
collapsedWidth="0"
className="bg-dark"
collapsed={sidebarCollapsed}
onCollapse={(sidebarCollapsed) => {
setSidebarCollapsed(sidebarCollapsed)
triggerResizeAfterADelay()
}}
style={{ backgroundColor: 'var(--bg-menu)' }}
>
<Menu
className="h-100 bg-dark"
className="h-100"
theme="dark"
style={{ backgroundColor: 'var(--bg-menu)' }}
selectedKeys={[activeScene]}
openKeys={[openSubmenu]}
mode="inline"
@ -205,7 +204,7 @@ function _Sidebar({ user, sidebarCollapsed, setSidebarCollapsed }) {
</Menu.SubMenu>
<Menu.SubMenu
key="people"
key="persons"
title={
<span style={itemStyle} data-attr="menu-item-people">
<UserOutlined />
@ -214,18 +213,18 @@ function _Sidebar({ user, sidebarCollapsed, setSidebarCollapsed }) {
}
onTitleClick={() => {
collapseSidebar()
location.pathname !== '/people/persons' && push('/people/persons')
location.pathname !== '/persons' && push('/persons')
}}
>
<Menu.Item key="people" style={itemStyle} data-attr="menu-item-people-persons">
<Menu.Item key="persons" style={itemStyle} data-attr="menu-item-people-persons">
<UserOutlined />
<span className="sidebar-label">Persons</span>
<Link to={'/people/persons'} onClick={collapseSidebar} />
<Link to={'/persons'} onClick={collapseSidebar} />
</Menu.Item>
<Menu.Item key="cohorts" style={itemStyle} data-attr="menu-item-people-cohorts">
<UsergroupAddOutlined />
<span className="sidebar-label">Cohorts</span>
<Link to={'/people/cohorts'} onClick={collapseSidebar} />
<Link to={'/cohorts'} onClick={collapseSidebar} />
</Menu.Item>
</Menu.SubMenu>
@ -245,7 +244,7 @@ function _Sidebar({ user, sidebarCollapsed, setSidebarCollapsed }) {
key="project"
title={
<span style={itemStyle} data-attr="menu-item-project">
<DeploymentUnitOutlined />
<ProjectOutlined />
<span className="sidebar-label">Project</span>
</span>
}
@ -255,7 +254,7 @@ function _Sidebar({ user, sidebarCollapsed, setSidebarCollapsed }) {
}}
>
<Menu.Item key="projectSettings" style={itemStyle} data-attr="menu-item-project-settings">
<ProjectOutlined />
<SettingOutlined />
<span className="sidebar-label">Settings</span>
<Link to={'/project/settings'} onClick={collapseSidebar} />
</Menu.Item>

View File

@ -1,3 +1,14 @@
.sidebar-logo {
margin: 16px;
height: 42px;
white-space: nowrap;
width: 168px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
@media (max-width: 991px) {
#root {
height: 100%;

View File

@ -3,20 +3,21 @@ import { useActions, useValues } from 'kea'
import { commandPaletteLogic } from 'lib/components/CommandPalette/commandPaletteLogic'
import { SearchOutlined } from '@ant-design/icons'
import { platformCommandControlKey } from 'lib/utils'
import { Button } from 'antd'
export function CommandPaletteButton(): JSX.Element {
const { isPaletteShown } = useValues(commandPaletteLogic)
const { showPalette } = useActions(commandPaletteLogic)
return (
<span
<Button
data-attr="command-palette-toggle"
className="btn btn-sm btn-light btn-top hide-when-small"
className="hide-when-small"
onClick={showPalette}
title={isPaletteShown ? 'Hide Command Palette' : 'Show Command Palette'}
icon={<SearchOutlined />}
>
<SearchOutlined size={1} style={{ marginRight: '0.5rem' }} />
{platformCommandControlKey('K')}
</span>
</Button>
)
}

View File

@ -18,7 +18,7 @@ export function LatestVersion() {
{latestVersion ? (
<span>
{isApp ? (
<Button onClick={() => setChangelogOpen(true)} type="link" style={{ color: 'var(--green)' }}>
<Button onClick={() => setChangelogOpen(true)} type="link" style={{ color: 'var(--success)' }}>
New features
</Button>
) : (
@ -27,7 +27,7 @@ export function LatestVersion() {
<Button
onClick={() => setChangelogOpen(true)}
type="link"
style={{ color: 'var(--green)' }}
style={{ color: 'var(--success)' }}
>
<span className="hide-when-small">
<CheckOutlined /> PostHog up-to-date
@ -41,7 +41,7 @@ export function LatestVersion() {
<Button
type="link"
onClick={() => setChangelogOpen(true)}
style={{ color: 'hsla(42, 90%, 37%, 1)' }}
style={{ color: 'var(--warning)' }}
>
<span className="hide-when-small">
<BulbOutlined /> New version available

View File

@ -1,6 +1,6 @@
import React, { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react'
import { useValues, useActions } from 'kea'
import { Alert, Dropdown, Input, Menu, Modal } from 'antd'
import { Alert, Button, Dropdown, Input, Menu, Modal } from 'antd'
import {
ProjectOutlined,
SmileOutlined,
@ -29,7 +29,7 @@ export function User(): JSX.Element {
<Menu>
<Menu.Item key="user-email">
<Link to="/me/settings" title="My Settings">
<SettingOutlined size={1} style={{ marginRight: '0.5rem' }} />
<SettingOutlined style={{ marginRight: '0.5rem' }} />
{user ? user.email : <i>loading</i>}
</Link>
</Menu.Item>
@ -38,17 +38,16 @@ export function User(): JSX.Element {
</Menu.Item>
<Menu.Item key="user-logout">
<a href="#" onClick={logout} data-attr="user-options-logout" style={{ color: red.primary }}>
<LogoutOutlined color={red.primary} size={1} style={{ marginRight: '0.5rem' }} />
<LogoutOutlined color={red.primary} style={{ marginRight: '0.5rem' }} />
Logout
</a>
</Menu.Item>
</Menu>
}
>
<div data-attr="user-options-dropdown" className="btn btn-sm btn-light btn-top" title="Me">
<SmileOutlined size={1} style={{ marginRight: '0.5rem' }} />
<Button data-attr="user-options-dropdown" icon={<SmileOutlined />} style={{ fontWeight: 500 }}>
{user ? user.name || user.email : <i>loading</i>}
</div>
</Button>
</Dropdown>
)
}
@ -262,22 +261,20 @@ export function Projects(): JSX.Element {
)
}}
>
<PlusOutlined size={1} style={{ marginRight: '0.5rem' }} />
<PlusOutlined style={{ marginRight: '0.5rem' }} />
<i>New Project</i>
</a>
</Menu.Item>
</Menu>
}
>
<div
<Button
data-attr="user-project-dropdown"
className="btn btn-sm btn-light btn-top"
style={{ marginRight: '0.75rem' }}
title="Current Projects"
style={{ marginRight: '0.75rem', fontWeight: 500 }}
icon={<ProjectOutlined />}
>
<ProjectOutlined size={1} style={{ marginRight: '0.5rem' }} />
{!user ? <i>loading</i> : user.team ? user.team.name : <i>none yet</i>}
</div>
</Button>
</Dropdown>
</>
)

View File

@ -13,7 +13,7 @@ export function TopContent(): JSX.Element {
const { backTo } = useValues(topContentLogic)
return (
<div className="content py-3 layout-top-content">
<div className="main-app-content layout-top-content" style={{ paddingTop: 16 }}>
<div
className="layout-top-content"
style={{

View File

@ -1,17 +1,5 @@
const lightColors = [
'blue',
'orange',
'green',
'red',
'purple',
'gray',
'indigo',
'pink',
'yellow',
'teal',
'cyan',
'gray-dark',
]
const lightColors = ['blue', 'purple', 'green', 'salmon', 'yellow', 'indigo', 'cyan', 'pink']
const getColorVar = (variable: string): string => getComputedStyle(document.body).getPropertyValue('--' + variable)
export const darkWhites = [

View File

@ -6,23 +6,23 @@ import { appEditorUrl } from './utils'
import { teamLogic } from 'scenes/teamLogic'
import { Modal, Button } from 'antd'
export function AppEditorLink({ actionId, style, className, children }) {
export function AppEditorLink({ actionId, style, children }) {
const [modalOpen, setModalOpen] = useState(false)
const { currentTeam } = useValues(teamLogic)
return (
<>
<a
<Button
href={appEditorUrl(actionId, currentTeam?.appUrls?.[0])}
style={style}
className={className}
size="small"
onClick={(e) => {
e.preventDefault()
setModalOpen(true)
}}
>
{children}
</a>
</Button>
<Modal
visible={modalOpen}
title={

View File

@ -2,15 +2,16 @@ import React from 'react'
import { useValues } from 'kea'
import { userLogic } from 'scenes/userLogic'
import { WarningOutlined, ToolFilled } from '@ant-design/icons'
import { Button } from 'antd'
import { Button, Card } from 'antd'
export function BillingToolbar(): JSX.Element {
const { user } = useValues(userLogic)
return (
<>
{user?.billing?.should_setup_billing && user?.billing.subscription_url && (
<div className="card">
<div className="card-body" style={{ display: 'flex' }}>
<Card>
<div style={{ display: 'flex' }}>
<div style={{ flexGrow: 1, display: 'flex', alignItems: 'center' }}>
<WarningOutlined className="text-warning" style={{ paddingRight: 8 }} />
{user?.billing?.plan?.custom_setup_billing_message ||
@ -22,7 +23,7 @@ export function BillingToolbar(): JSX.Element {
</Button>
</div>
</div>
</div>
</Card>
)}
</>
)

View File

@ -13,13 +13,8 @@ export function ChartFilter(props) {
(!filters.display ||
filters.display === ACTIONS_LINE_GRAPH_LINEAR ||
filters.display === ACTIONS_LINE_GRAPH_CUMULATIVE) && (
<Tooltip
key="1"
getPopupContainer={(trigger) => trigger.parentElement}
placement="right"
title="Click on a point to see users related to the datapoint"
>
<InfoCircleOutlined className="info" style={{ color: '#007bff' }} />
<Tooltip key="1" placement="right" title="Click on a point to see users related to the datapoint">
<InfoCircleOutlined className="info-indicator" />
</Tooltip>
),

View File

@ -0,0 +1,10 @@
import { CloseOutlined } from '@ant-design/icons'
import React from 'react'
export function CloseButton(props: Record<string, any>): JSX.Element {
return (
<span {...props} className={'btn-close cursor-pointer ' + props.className} style={{ ...props.style }}>
<CloseOutlined />
</span>
)
}

View File

@ -463,17 +463,17 @@ export const commandPaletteLogic = kea<
},
{
icon: UserOutlined,
display: 'Go to People',
display: 'Go to Persons',
synonyms: ['people'],
executor: () => {
push('/people/persons')
push('/persons')
},
},
{
icon: UsergroupAddOutlined,
display: 'Go to Cohorts',
executor: () => {
push('/people/cohorts')
push('/cohorts')
},
},
{

View File

@ -1,3 +1,5 @@
@import '~/vars';
.palette__overlay {
z-index: 2147483021;
position: fixed;
@ -18,7 +20,11 @@
width: 36rem;
max-width: 100%;
max-height: 60%;
color: #fff;
color: $text_light;
border-radius: $radius;
background-color: $bg_menu;
@extend .mixin-elevated;
@media (max-width: 500px) {
top: 10%;
max-height: 80%;
@ -79,7 +85,7 @@
}
.palette__result--focused {
background: rgba(0, 0, 0, 0.35);
background: darken($bg_menu, 10%);
&::before,
&::after {
content: '';
@ -98,7 +104,7 @@
}
.palette__result--executable::after {
background: #1890ff;
background: $primary;
}
.palette__scope {
@ -110,5 +116,4 @@
display: flex;
align-items: center;
width: 1rem;
height: 100%;
}

View File

@ -52,7 +52,7 @@ export function CommandPalette(): JSX.Element | null {
return !user || !isPaletteShown ? null : (
<div className="palette__overlay">
<div className="palette__box card bg-dark" ref={boxRef}>
<div className="palette__box" ref={boxRef}>
{(!activeFlow || activeFlow.instruction) && <CommandInput />}
{!commandSearchResults.length && !activeFlow ? null : <CommandResults executeResult={executeResult} />}
</div>

View File

@ -125,7 +125,7 @@ function DatePickerDropdown(props) {
}, [calendarOpen])
return (
<div className="dropdown" ref={dropdownRef}>
<div ref={dropdownRef}>
<a
style={{
margin: '0 1rem',

View File

@ -1,57 +0,0 @@
import React, { useState } from 'react'
import { DownOutlined } from '@ant-design/icons'
export function Dropdown({
className,
style,
'data-attr': dataAttr,
buttonStyle,
children,
buttonClassName,
title,
titleEmpty,
}) {
const [menuOpen, setMenuOpen] = useState(false)
const isEmpty = !(children && (!Array.isArray(children) || children.length))
function close(e) {
if (e.target.closest('.dropdown-no-close') || e.target.closest('.react-datepicker')) return
setMenuOpen(false)
document.removeEventListener('click', close)
}
function open(e) {
e.preventDefault()
setMenuOpen(true)
document.addEventListener('click', close)
}
return (
<div
className={'dropdown ' + className}
style={{
display: 'inline',
marginTop: -6,
...style,
}}
data-attr={dataAttr}
>
<a className={'cursor-pointer ' + buttonClassName} style={{ ...buttonStyle }} onClick={open} href="#">
{isEmpty && titleEmpty ? titleEmpty : title}
{!isEmpty && <DownOutlined style={{ marginLeft: '3px', color: 'rgba(0, 0, 0, 0.25)' }} />}
</a>
{!isEmpty && (
<div
className={'dropdown-menu ' + (menuOpen && 'show')}
style={{
borderRadius: 2,
}}
aria-labelledby="dropdownMenuButton"
>
{children}
</div>
)}
</div>
)
}

View File

@ -0,0 +1,15 @@
import React from 'react'
interface PageHeaderProps {
title: string | JSX.Element
caption?: string | JSX.Element
}
export function PageHeader({ title, caption }: PageHeaderProps): JSX.Element {
return (
<>
<h1 className="page-title">{title}</h1>
{caption && <div className="page-caption">{caption}</div>}
</>
)
}

View File

@ -1,5 +1,5 @@
import React, { useCallback, useState } from 'react'
import { Select, Tabs } from 'antd'
import { Col, Row, Select, Tabs } from 'antd'
import { operatorMap, isOperatorFlag } from 'lib/utils'
import { PropertyValue } from './PropertyValue'
import { PropertyKeyInfo, keyMapping } from 'lib/components/PropertyKeyInfo'
@ -22,130 +22,135 @@ function PropertyPaneContents({
}) {
return (
<>
<div className={displayOperatorAndValue ? 'col-4 pl-0' : 'col p-0'}>
<Select
className={rrwebBlockClass}
showSearch
autoFocus={!propkey}
defaultOpen={!propkey}
placeholder="Property key"
data-attr="property-filter-dropdown"
labelInValue
value={
type === 'cohort'
? { value: '' }
: {
value: propkey,
label:
keyMapping[type === 'element' ? 'element' : 'event'][propkey]?.label || propkey,
}
}
filterOption={(input, option) => option.value?.toLowerCase().indexOf(input.toLowerCase()) >= 0}
onChange={(_, newKey) =>
setThisFilter(
newKey.value.replace(/^(event_|person_|element_)/gi, ''),
undefined,
operator,
newKey.type
)
}
style={{ width: '100%' }}
virtual={false}
>
{eventProperties.length > 0 && (
<Select.OptGroup key="event properties" label="Event properties">
{eventProperties.map((item, index) => (
<Select.Option
key={'event_' + item.value}
value={'event_' + item.value}
type="event"
data-attr={'prop-filter-event-' + index}
>
<PropertyKeyInfo value={item.value} />
</Select.Option>
))}
</Select.OptGroup>
)}
{personProperties && (
<Select.OptGroup key="user properties" label="User properties">
{personProperties.map((item, index) => (
<Select.Option
key={'person_' + item.value}
value={'person_' + item.value}
type="person"
data-attr={'prop-filter-person-' + index}
>
<PropertyKeyInfo value={item.value} />
</Select.Option>
))}
</Select.OptGroup>
)}
{eventProperties.length > 0 && (
<Select.OptGroup key="elements" label="Elements">
{['tag_name', 'text', 'href', 'selector'].map((item, index) => (
<Select.Option
key={'element_' + item}
value={'element_' + item}
type="element"
data-attr={'prop-filter-element-' + index}
>
<PropertyKeyInfo value={item} type="element" />
</Select.Option>
))}
</Select.OptGroup>
)}
</Select>
</div>
{displayOperatorAndValue && (
<div className={isOperatorFlag(operator) ? 'col-8 p-0' : 'col-4 pl-0'}>
<Row gutter={8} className="full-width">
<Col flex={1}>
<Select
style={{ width: '100%' }}
defaultActiveFirstOption
labelInValue
value={{
value: operator || '=',
label: operatorMap[operator || 'exact'],
}}
className={rrwebBlockClass}
showSearch
autoFocus={!propkey}
defaultOpen={!propkey}
placeholder="Property key"
onChange={(_, newOperator) => {
let newValue = value
if (isOperatorFlag(newOperator.value)) {
// change value to induce reload
newValue = newOperator.value
onComplete()
} else {
// clear value if switching from nonparametric (flag) to parametric
if (isOperatorFlag(operator)) newValue = undefined
}
setThisFilter(propkey, newValue, newOperator.value, type)
}}
data-attr="property-filter-dropdown"
labelInValue
value={
type === 'cohort'
? { value: '' }
: {
value: propkey,
label:
keyMapping[type === 'element' ? 'element' : 'event'][propkey]?.label ||
propkey,
}
}
filterOption={(input, option) => option.value?.toLowerCase().indexOf(input.toLowerCase()) >= 0}
onChange={(_, newKey) =>
setThisFilter(
newKey.value.replace(/^(event_|person_|element_)/gi, ''),
undefined,
operator,
newKey.type
)
}
style={{ width: '100%' }}
virtual={false}
>
{Object.keys(operatorMap).map((operator) => (
<Select.Option key={operator} value={operator}>
{operatorMap[operator || 'exact']}
</Select.Option>
))}
{eventProperties.length > 0 && (
<Select.OptGroup key="event properties" label="Event properties">
{eventProperties.map((item, index) => (
<Select.Option
key={'event_' + item.value}
value={'event_' + item.value}
type="event"
data-attr={'prop-filter-event-' + index}
>
<PropertyKeyInfo value={item.value} />
</Select.Option>
))}
</Select.OptGroup>
)}
{personProperties && (
<Select.OptGroup key="user properties" label="User properties">
{personProperties.map((item, index) => (
<Select.Option
key={'person_' + item.value}
value={'person_' + item.value}
type="person"
data-attr={'prop-filter-person-' + index}
>
<PropertyKeyInfo value={item.value} />
</Select.Option>
))}
</Select.OptGroup>
)}
{eventProperties.length > 0 && (
<Select.OptGroup key="elements" label="Elements">
{['tag_name', 'text', 'href', 'selector'].map((item, index) => (
<Select.Option
key={'element_' + item}
value={'element_' + item}
type="element"
data-attr={'prop-filter-element-' + index}
>
<PropertyKeyInfo value={item} type="element" />
</Select.Option>
))}
</Select.OptGroup>
)}
</Select>
</div>
)}
{displayOperatorAndValue && !isOperatorFlag(operator) && (
<div className="col-4 p-0">
<PropertyValue
type={type}
key={propkey}
propertyKey={propkey}
operator={operator}
value={value}
onSet={(value) => {
onComplete()
setThisFilter(propkey, value, operator, type)
}}
/>
{(operator === 'gt' || operator === 'lt') && isNaN(value) && (
<p className="text-danger">Value needs to be a number. Try "equals" or "contains" instead.</p>
)}
</div>
)}
</Col>
{displayOperatorAndValue && (
<Col flex={1}>
<Select
style={{ width: '100%' }}
defaultActiveFirstOption
labelInValue
value={{
value: operator || '=',
label: operatorMap[operator || 'exact'],
}}
placeholder="Property key"
onChange={(_, newOperator) => {
let newValue = value
if (isOperatorFlag(newOperator.value)) {
// change value to induce reload
newValue = newOperator.value
onComplete()
} else {
// clear value if switching from nonparametric (flag) to parametric
if (isOperatorFlag(operator)) newValue = undefined
}
setThisFilter(propkey, newValue, newOperator.value, type)
}}
>
{Object.keys(operatorMap).map((operator) => (
<Select.Option key={operator} value={operator}>
{operatorMap[operator || 'exact']}
</Select.Option>
))}
</Select>
</Col>
)}
{displayOperatorAndValue && !isOperatorFlag(operator) && (
<Col flex={1}>
<PropertyValue
type={type}
key={propkey}
propertyKey={propkey}
operator={operator}
value={value}
onSet={(value) => {
onComplete()
setThisFilter(propkey, value, operator, type)
}}
/>
{(operator === 'gt' || operator === 'lt') && isNaN(value) && (
<p className="text-danger">
Value needs to be a number. Try "equals" or "contains" instead.
</p>
)}
</Col>
)}
</Row>
</>
)
}

View File

@ -6,7 +6,8 @@ import { propertyFilterLogic } from './propertyFilterLogic'
import { cohortsModel } from '../../../models/cohortsModel'
import { keyMapping } from 'lib/components/PropertyKeyInfo'
import { Popover, Row } from 'antd'
import { CloseButton, formatPropertyLabel } from 'lib/utils'
import { formatPropertyLabel } from 'lib/utils'
import { CloseButton } from 'lib/components/CloseButton'
import '../../../scenes/actions/Actions.scss'
const FilterRow = React.memo(function FilterRow({
@ -31,7 +32,7 @@ const FilterRow = React.memo(function FilterRow({
}
return (
<Row align="middle" className="mt-2 mb-2">
<Row align="middle" className="mt-05 mb-05">
<Popover
trigger="click"
onVisibleChange={handleVisibleChange}
@ -85,7 +86,7 @@ export function PropertyFilters({
const { cohorts } = useValues(cohortsModel)
return (
<div className="column" style={{ marginBottom: '15px' }}>
<div className="mb">
{filters &&
filters.map((item, index) => {
return (

View File

@ -118,7 +118,6 @@ export function SaveToDashboardModal({
name="name"
required
type="text"
className="form-control"
placeholder="Users who did x"
autoFocus={!name}
value={name}

View File

@ -90,28 +90,6 @@ export function SceneLoading(): JSX.Element {
)
}
export function CloseButton(props: Record<string, any>): JSX.Element {
return (
<span {...props} className={'close cursor-pointer ' + props.className} style={{ ...props.style }}>
<span aria-hidden="true">&times;</span>
</span>
)
}
export function Card(props: Record<string, any>): JSX.Element {
return (
<div
{...props}
className={'card' + (props.className ? ` ${props.className}` : '')}
style={props.style}
title=""
>
{props.title && <div className="card-header">{props.title}</div>}
{props.children}
</div>
)
}
export function deleteWithUndo({ undo = false, ...props }: Record<string, any>): void {
api.update('api/' + props.endpoint + '/' + props.object.id, {
...props.object,
@ -536,6 +514,19 @@ export function sampleSingle<T>(items: T[]): T[] {
return [items[Math.floor(Math.random() * items.length)]]
}
export function identifierToHuman(input: string, capitalize: boolean = true): string | null {
/* Converts a camelCase, PascalCase or snake_case string to a human-friendly string.
(e.g. `feature_flags` or `featureFlags` becomes "Feature Flags") */
const match = input.match(/[A-Za-z][a-z]*/g)
if (!match) return null
return match
.map((group) => {
return capitalize ? group[0].toUpperCase() + group.substr(1).toLowerCase() : group.toLowerCase()
})
.join(' ')
}
export function parseGithubRepoURL(url: string): Record<string, string> {
const match = url.match(/^https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/?$/)
if (!match) {

View File

@ -1,5 +1,3 @@
import 'react-toastify/dist/ReactToastify.css'
import 'react-datepicker/dist/react-datepicker.css'
import { hot } from 'react-hot-loader/root'
import React, { useState, useEffect } from 'react'
@ -28,6 +26,10 @@ const darkerScenes: Record<string, boolean> = {
paths: true,
}
const Toast = (): JSX.Element => {
return <ToastContainer autoClose={8000} transition={Slide} position="top-right" />
}
export const App = hot(_App)
function _App(): JSX.Element {
const { user } = useValues(userLogic)
@ -55,9 +57,9 @@ function _App(): JSX.Element {
if (!user) {
return unauthenticatedRoutes.includes(scene) ? (
<>
<Scene {...params} /> <ToastContainer autoClose={8000} transition={Slide} position="bottom-center" />
</>
<Layout>
<Scene {...params} /> <Toast />
</Layout>
) : (
<div />
)
@ -67,7 +69,7 @@ function _App(): JSX.Element {
return (
<>
<Scene user={user} {...params} />
<ToastContainer autoClose={8000} transition={Slide} position="bottom-center" />
<Toast />
</>
)
}
@ -75,16 +77,14 @@ function _App(): JSX.Element {
return (
<>
<UpgradeModal />
<Layout className="bg-white">
<Layout>
<Sidebar user={user} sidebarCollapsed={sidebarCollapsed} setSidebarCollapsed={setSidebarCollapsed} />
<Layout
className={`${darkerScenes[scene] ? 'bg-dashboard' : 'bg-white'}${
!sidebarCollapsed ? ' with-open-sidebar' : ''
}`}
className={`${darkerScenes[scene] && 'bg-mid'}${!sidebarCollapsed ? ' with-open-sidebar' : ''}`}
style={{ minHeight: '100vh' }}
>
<TopContent />
<Layout.Content className="pl-5 pr-5 pt-3 pb-5" data-attr="layout-content">
<Layout.Content className="main-app-content" data-attr="layout-content">
<BillingToolbar />
{currentTeam &&
!currentTeam.ingested_event &&
@ -93,7 +93,7 @@ function _App(): JSX.Element {
) : (
<Scene user={user} {...params} />
)}
<ToastContainer autoClose={8000} transition={Slide} position="bottom-center" />
<Toast />
</Layout.Content>
</Layout>
</Layout>

View File

@ -14,6 +14,7 @@ import {
} from '@ant-design/icons'
import { volcano, green, red, grey, blue } from '@ant-design/colors'
import { router } from 'kea-router'
import { PageHeader } from 'lib/components/PageHeader'
function PreflightItem({ name, status, caption, failedState }) {
/*
@ -108,20 +109,17 @@ function PreflightCheck() {
return (
<>
<Space direction="vertical" className="space-top" style={{ width: '100%' }}>
<h1 className="title text-center" style={{ marginBottom: 0 }}>
Welcome to PostHog!
</h1>
<div className="page-caption text-center">Understand your users. Build a better product.</div>
<Space direction="vertical" className="space-top" style={{ width: '100%', paddingLeft: 32 }}>
<PageHeader title="Welcome to PostHog!" caption="Understand your users. Build a better product." />
</Space>
<Col xs={24} style={{ margin: '32px 16px' }}>
<Col xs={24} style={{ margin: '0 16px' }}>
<h2 className="subtitle text-center space-top">
We're&nbsp;glad to&nbsp;have you&nbsp;here! Let's&nbsp;get&nbsp;you started with&nbsp;PostHog.
We're glad to have you here! Let's get you started with PostHog.
</h2>
</Col>
<Row style={{ display: 'flex', justifyContent: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', flexDirection: 'column' }}>
<img src={hedgehogBlue} style={{ maxHeight: '100%', width: 380 }} />
<img src={hedgehogBlue} style={{ maxHeight: '100%', width: 320 }} />
<p>Got any PostHog questions?</p>
<Button type="default" data-attr="support" data-source="preflight">
<a href="https://posthog.com/support" target="_blank" rel="noreferrer">
@ -224,7 +222,7 @@ function PreflightCheck() {
<b>Checks in progress</b>
)}
</div>
<div className="space-top text-center" style={{ marginBottom: 64 }}>
<div className="text-center" style={{ marginBottom: 64 }}>
<Button
type="primary"
data-attr="preflight-complete"

View File

@ -80,31 +80,7 @@ function Signup() {
}}
>
<form onSubmit={handleSubmit}>
<div className="ph-input-group">
<label htmlFor="signupFirstName">First Name</label>
<Input
placeholder="Jane"
autoFocus
value={formState.firstName.value}
onChange={(e) => updateForm('firstName', e.target)}
required
disabled={accountLoading}
id="signupFirstName"
/>
</div>
<div className="ph-input-group">
<label htmlFor="signupCompanyName">Company or Project</label>
<Input
placeholder="Hogflix Movies"
value={formState.companyName.value}
onChange={(e) => updateForm('companyName', e.target)}
disabled={accountLoading}
id="signupCompanyName"
/>
</div>
<div className="ph-input-group">
<div className="input-set">
<label htmlFor="signupEmail">Email</label>
<Input
placeholder="jane@hogflix.io"
@ -117,11 +93,7 @@ function Signup() {
/>
</div>
<div
className={`ph-input-group ${
state.submitted && !formState.password.valid ? 'errored' : ''
}`}
>
<div className={`input-set ${state.submitted && !formState.password.valid ? 'errored' : ''}`}>
<label htmlFor="signupPassword">Password</label>
<Input.Password
value={formState.password.value}
@ -139,6 +111,30 @@ function Signup() {
)}
</div>
<div className="input-set">
<label htmlFor="signupFirstName">First Name</label>
<Input
placeholder="Jane"
autoFocus
value={formState.firstName.value}
onChange={(e) => updateForm('firstName', e.target)}
required
disabled={accountLoading}
id="signupFirstName"
/>
</div>
<div className="input-set">
<label htmlFor="signupCompanyName">Company or Project</label>
<Input
placeholder="Hogflix Movies"
value={formState.companyName.value}
onChange={(e) => updateForm('companyName', e.target)}
disabled={accountLoading}
id="signupCompanyName"
/>
</div>
<div>
<Checkbox
checked={formState.emailOptIn.value}

View File

@ -11,6 +11,7 @@ import { kea } from 'kea'
import { Spin } from 'antd'
import { hot } from 'react-hot-loader/root'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { PageHeader } from 'lib/components/PageHeader'
let actionLogic = kea({
key: (props) => props.id || 'new',
@ -86,14 +87,10 @@ function _Action({ id }) {
const { fetchEvents } = useActions(eventsTableLogic({ fixedFilters }))
const { isComplete } = useValues(actionLogic({ id, onComplete: fetchEvents }))
const { loadAction } = useActions(actionLogic({ id, onComplete: fetchEvents }))
const { featureFlags } = useValues(featureFlagLogic)
return (
<div>
{!featureFlags['actions-ux-201012'] && <h1>{id ? 'Edit action' : 'New Action'}</h1>}
{featureFlags['actions-ux-201012'] && (
<h1 className="page-header">{id ? 'Editing action' : 'Creating action'}</h1>
)}
<PageHeader title={id ? 'Editing action' : 'Creating action'} />
<EditComponent
apiURL=""
@ -108,7 +105,7 @@ function _Action({ id }) {
/>
{id && !isComplete && (
<div style={{ marginBottom: '10rem' }}>
<h1 className="page-header">Events</h1>
<h2 className="subtitle">Events</h2>
<Spin style={{ marginRight: 12 }} />
Calculating action, please hold on.
</div>

View File

@ -3,12 +3,12 @@ import { uuid, Loading } from 'lib/utils'
import { Link } from 'lib/components/Link'
import { useValues, useActions } from 'kea'
import { actionEditLogic } from './actionEditLogic'
import { ActionStep } from './ActionStep'
import { Input } from 'antd'
import { Button, Card, Input } from 'antd'
import { SaveOutlined } from '@ant-design/icons'
// TODO: isEditor === false always
export function ActionEdit({ actionId, apiURL, onSave, user, isEditor, simmer, showNewActionButton, temporaryToken }) {
export function ActionEdit({ actionId, apiURL, onSave, user, isEditor, simmer, temporaryToken }) {
let logic = actionEditLogic({
id: actionId,
apiURL,
@ -16,7 +16,7 @@ export function ActionEdit({ actionId, apiURL, onSave, user, isEditor, simmer, s
temporaryToken,
})
const { action, actionLoading, errorActionId } = useValues(logic)
const { setAction, saveAction, setCreateNew } = useActions(logic)
const { setAction, saveAction } = useActions(logic)
const [edited, setEdited] = useState(false)
const slackEnabled = user?.team?.slack_incoming_webhook
@ -24,30 +24,20 @@ export function ActionEdit({ actionId, apiURL, onSave, user, isEditor, simmer, s
if (actionLoading || !action) return <Loading />
const addGroup = (
<button
type="button"
className="btn btn-outline-success btn-sm"
<Button
onClick={() => {
setAction({ ...action, steps: [...action.steps, { isNew: uuid() }] })
}}
>
Add another match group
</button>
</Button>
)
return (
<div className={isEditor ? '' : 'card'} style={{ marginTop: isEditor ? 8 : '' }}>
<form
className={isEditor ? '' : 'card-body'}
onSubmit={(e) => {
e.preventDefault()
if (isEditor && showNewActionButton) setCreateNew(true)
saveAction()
}}
>
<input
<Card style={{ marginTop: isEditor ? 8 : '' }}>
<div className="mt">
<Input
required
className="form-control"
placeholder="For example: user signed up"
value={action.name}
onChange={(e) => {
@ -56,135 +46,134 @@ export function ActionEdit({ actionId, apiURL, onSave, user, isEditor, simmer, s
}}
data-attr="edit-action-input"
/>
</div>
{action.count > -1 && (
<div>
<small className="text-muted">Matches {action.count} events</small>
</div>
)}
{!isEditor && <br />}
{action.steps.map((step, index) => (
<Fragment key={index}>
{index > 0 ? (
<div
style={{
textAlign: 'center',
fontSize: 13,
letterSpacing: 1,
opacity: 0.7,
margin: 8,
}}
>
OR
</div>
) : null}
<ActionStep
key={step.id || step.isNew}
step={step}
isEditor={isEditor}
actionId={action.id}
simmer={simmer}
isOnlyStep={action.steps.length === 1}
onDelete={() => {
setAction({ ...action, steps: action.steps.filter((s) => s.id != step.id) })
setEdited(true)
}}
onChange={(newStep) => {
setAction({
...action,
steps: action.steps.map((s) =>
(step.id && s.id == step.id) || (step.isNew && s.isNew === step.isNew)
? {
id: step.id,
isNew: step.isNew,
...newStep,
}
: s
),
})
setEdited(true)
}}
/>
</Fragment>
))}
{!isEditor ? (
<div>
<div style={{ margin: '1rem 0 0.5rem' }}>
<input
id="webhook-checkbox"
type="checkbox"
onChange={(e) => {
setAction({ ...action, post_to_slack: e.target.checked })
setEdited(true)
}}
checked={!!action.post_to_slack}
disabled={!slackEnabled}
/>
<label
className={slackEnabled ? '' : 'disabled'}
style={{ marginLeft: '0.5rem', marginBottom: '0.5rem' }}
htmlFor="webhook-checkbox"
>
Post to Slack/Teams when this action is triggered.
</label>{' '}
<Link to="/project/settings#webhook">
{slackEnabled ? 'Configure' : 'Enable'} this integration in Setup.
</Link>
{action.post_to_slack && (
<>
<Input
addonBefore="Message format (optional)"
placeholder="[action.name] triggered by [user.name]"
value={action.slack_message_format}
onChange={(e) => {
setAction({ ...action, slack_message_format: e.target.value })
setEdited(true)
}}
disabled={!slackEnabled || !action.post_to_slack}
data-attr="edit-slack-message-format"
/>
<small>
<a
href="https://posthog.com/docs/integrations/message-formatting/"
target="_blank"
rel="noopener noreferrer"
>
See documentation on how to format webhook messages.
</a>
</small>
</>
)}
</div>
</div>
) : (
<br />
)}
{errorActionId && (
<p className="text-danger">
Action with this name already exists.{' '}
<a href={apiURL + 'action/' + errorActionId}>Click here to edit.</a>
</p>
)}
{isEditor ? <div style={{ marginBottom: 20 }}>{addGroup}</div> : null}
<div className={isEditor ? 'btn-group save-buttons' : ''}>
{!isEditor ? addGroup : null}
<button
disabled={!edited}
data-attr="save-action-button"
className={
edited ? 'btn-success btn btn-sm float-right' : 'btn-secondary btn btn-sm float-right'
}
>
Save action
</button>
{action.count > -1 && (
<div>
<small className="text-muted">Matches {action.count} events</small>
</div>
</form>
</div>
)}
{!isEditor && <br />}
{action.steps.map((step, index) => (
<Fragment key={index}>
{index > 0 ? (
<div
style={{
textAlign: 'center',
fontSize: 13,
letterSpacing: 1,
opacity: 0.7,
margin: 8,
}}
>
OR
</div>
) : null}
<ActionStep
key={step.id || step.isNew}
step={step}
isEditor={isEditor}
actionId={action.id}
simmer={simmer}
isOnlyStep={action.steps.length === 1}
onDelete={() => {
setAction({ ...action, steps: action.steps.filter((s) => s.id != step.id) })
setEdited(true)
}}
onChange={(newStep) => {
setAction({
...action,
steps: action.steps.map((s) =>
(step.id && s.id == step.id) || (step.isNew && s.isNew === step.isNew)
? {
id: step.id,
isNew: step.isNew,
...newStep,
}
: s
),
})
setEdited(true)
}}
/>
</Fragment>
))}
{!isEditor ? (
<div>
<div style={{ margin: '1rem 0 0.5rem' }}>
<input
id="webhook-checkbox"
type="checkbox"
onChange={(e) => {
setAction({ ...action, post_to_slack: e.target.checked })
setEdited(true)
}}
checked={!!action.post_to_slack}
disabled={!slackEnabled}
/>
<label
className={slackEnabled ? '' : 'disabled'}
style={{ marginLeft: '0.5rem', marginBottom: '0.5rem' }}
htmlFor="webhook-checkbox"
>
Post to Slack/Teams when this action is triggered.
</label>{' '}
<Link to="/project/settings#webhook">
{slackEnabled ? 'Configure' : 'Enable'} this integration in Setup.
</Link>
{action.post_to_slack && (
<>
<Input
addonBefore="Message format (optional)"
placeholder="[action.name] triggered by [user.name]"
value={action.slack_message_format}
onChange={(e) => {
setAction({ ...action, slack_message_format: e.target.value })
setEdited(true)
}}
disabled={!slackEnabled || !action.post_to_slack}
data-attr="edit-slack-message-format"
/>
<small>
<a
href="https://posthog.com/docs/integrations/message-formatting/"
target="_blank"
rel="noopener noreferrer"
>
See documentation on how to format webhook messages.
</a>
</small>
</>
)}
</div>
</div>
) : (
<br />
)}
{errorActionId && (
<p className="text-danger">
Action with this name already exists.{' '}
<a href={apiURL + 'action/' + errorActionId}>Click here to edit.</a>
</p>
)}
<div>
{addGroup}
<Button
disabled={!edited}
data-attr="save-action-button"
className="float-right"
type="primary"
icon={<SaveOutlined />}
onClick={saveAction}
>
Save action
</Button>
</div>
</Card>
)
}

View File

@ -7,8 +7,8 @@ import { useValues, useActions } from 'kea'
import { actionEditLogic } from './actionEditLogic'
import './Actions.scss'
import { ActionStep } from './ActionStepV2'
import { Col, Input, Row } from 'antd'
import { InfoCircleOutlined, PlusOutlined } from '@ant-design/icons'
import { Button, Col, Input, Row } from 'antd'
import { InfoCircleOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons'
export function ActionEdit({ actionId, apiURL, onSave, user, simmer, temporaryToken }) {
let logic = actionEditLogic({
@ -30,9 +30,9 @@ export function ActionEdit({ actionId, apiURL, onSave, user, simmer, temporaryTo
}
const addGroup = (
<button type="button" className="btn btn-outline-success btn-sm" onClick={newAction}>
<Button onClick={newAction} size="small">
Add another match group
</button>
</Button>
)
return (
@ -44,9 +44,8 @@ export function ActionEdit({ actionId, apiURL, onSave, user, simmer, temporaryTo
}}
>
<label>Action name:</label>
<input
<Input
required
className="form-control"
placeholder="e.g. user account created, purchase completed, movie watched"
value={action.name}
onChange={(e) => {
@ -61,8 +60,8 @@ export function ActionEdit({ actionId, apiURL, onSave, user, simmer, temporaryTo
</div>
)}
<div className="match-group-section card" style={{ overflow: 'visible' }}>
<h3>Match groups</h3>
<div className="match-group-section" style={{ overflow: 'visible' }}>
<h2 className="subtitle">Match groups</h2>
<div>
Your action will be triggered whenever <b>any of your match groups</b> are received.{' '}
<a href="https://posthog.com/docs/features/actions" target="_blank">
@ -169,15 +168,16 @@ export function ActionEdit({ actionId, apiURL, onSave, user, simmer, temporaryTo
</p>
)}
<div>
<button
<Button
disabled={!edited}
data-attr="save-action-button"
className={
edited ? 'btn-success btn btn-sm float-right' : 'btn-secondary btn btn-sm float-right'
}
className="float-right"
type="primary"
icon={<SaveOutlined />}
onClick={saveAction}
>
Save action
</button>
</Button>
</div>
</form>
</div>

View File

@ -4,6 +4,8 @@ import { AppEditorLink } from 'lib/components/AppEditorLink/AppEditorLink'
import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters'
import PropTypes from 'prop-types'
import { URL_MATCHING_HINTS } from 'scenes/actions/hints'
import { ExportOutlined } from '@ant-design/icons'
import { Button, Card, Checkbox, Input, Radio } from 'antd'
let getSafeText = (el) => {
if (!el.childNodes || !el.childNodes.length) return
@ -132,15 +134,14 @@ export class ActionStep extends Component {
selectorError = true
}
return (
<div className={'form-group ' + (this.state.selection.indexOf(props.item) > -1 && 'selected')}>
<div className={'mb ' + (this.state.selection.indexOf(props.item) > -1 && 'selected')}>
{props.selector && this.props.isEditor && (
<small className={'form-text float-right ' + (selectorError ? 'text-danger' : 'text-muted')}>
{selectorError ? 'Invalid selector' : `Matches ${matches} elements`}
</small>
)}
<label>
<input
type="checkbox"
<Checkbox
name="selection"
checked={this.state.selection.indexOf(props.item) > -1}
value={props.item}
@ -157,11 +158,10 @@ export class ActionStep extends Component {
{props.label} {props.extra_options}
</label>
{props.item === 'selector' ? (
<textarea className="form-control" onChange={onChange} value={this.props.step[props.item] || ''} />
<Input.TextArea onChange={onChange} value={this.props.step[props.item] || ''} />
) : (
<input
<Input
data-attr="edit-action-url-input"
className="form-control"
onChange={onChange}
value={this.props.step[props.item] || ''}
/>
@ -171,61 +171,45 @@ export class ActionStep extends Component {
}
TypeSwitcher = () => {
let { step, isEditor } = this.props
const handleChange = (e) => {
const type = e.target.value
if (type === '$autocapture') {
this.setState(
{
selection: Object.keys(step).filter((key) => key !== 'id' && key !== 'isNew' && step[key]),
},
() => this.sendStep({ ...step, event: '$autocapture' })
)
} else if (type === '$pageview') {
this.setState({ selection: ['url'] }, () =>
this.sendStep({
...step,
event: '$pageview',
url: isEditor
? window.location.protocol + '//' + window.location.host + window.location.pathname
: step.url,
})
)
} else {
this.setState({ selection: [] }, () => this.sendStep({ ...step, event: '' }))
}
}
return (
<div>
<div className="btn-group">
<button
type="button"
onClick={() =>
this.setState(
{
selection: Object.keys(step).filter(
(key) => key !== 'id' && key !== 'isNew' && step[key]
),
},
() => this.sendStep({ ...step, event: '$autocapture' })
)
}
className={'btn ' + (step.event === '$autocapture' ? 'btn-secondary' : 'btn-light btn-action')}
>
Frontend element
</button>
<button
type="button"
onClick={() => this.setState({ selection: [] }, () => this.sendStep({ ...step, event: '' }))}
className={
'btn ' +
(typeof step.event !== 'undefined' &&
step.event !== '$autocapture' &&
step.event !== '$pageview'
? 'btn-secondary'
: 'btn-light btn-action')
}
>
Custom event
</button>
<button
type="button"
onClick={() => {
this.setState({ selection: ['url'] }, () =>
this.sendStep({
...step,
event: '$pageview',
url: isEditor
? window.location.protocol +
'//' +
window.location.host +
window.location.pathname
: step.url,
})
)
}}
className={'btn ' + (step.event === '$pageview' ? 'btn-secondary' : 'btn-light btn-action')}
data-attr="action-step-pageview"
>
Page view
</button>
</div>
<Radio.Group
buttonStyle="solid"
onChange={handleChange}
value={
step.event === '$autocapture' || step.event === '$pageview' || step.event === undefined
? step.event
: 'event'
}
>
<Radio.Button value="$autocapture">Frontend element</Radio.Button>
<Radio.Button value="event">Custom event</Radio.Button>
<Radio.Button value="$pageview">Page view</Radio.Button>
</Radio.Group>
</div>
)
}
@ -234,12 +218,8 @@ export class ActionStep extends Component {
<div>
{!isEditor && (
<span>
<AppEditorLink
actionId={actionId}
style={{ margin: '1rem 0' }}
className="btn btn-sm btn-light"
>
Select element on site <i className="fi flaticon-export" />
<AppEditorLink actionId={actionId} style={{ margin: '1rem 0' }}>
Select element on site <ExportOutlined />
</AppEditorLink>
<a
href="https://posthog.com/docs/features/actions"
@ -269,55 +249,44 @@ export class ActionStep extends Component {
</div>
)
}
URLMatching = ({ step, isEditor }) => {
URLMatching = ({ step }) => {
const handleURLMatchChange = (e) => {
this.sendStep({ ...step, url_matching: e.target.value })
}
return (
<div className="btn-group" style={{ margin: isEditor ? '4px 0 0 8px' : '0 0 0 8px' }}>
<button
onClick={() => this.sendStep({ ...step, url_matching: 'contains' })}
type="button"
className={
'btn btn-sm ' +
(!step.url_matching || step.url_matching === 'contains' ? 'btn-secondary' : 'btn-light')
}
>
contains
</button>
<button
onClick={() => this.sendStep({ ...step, url_matching: 'regex' })}
type="button"
className={'btn btn-sm ' + (step.url_matching === 'regex' ? 'btn-secondary' : 'btn-light')}
>
matches regex
</button>
<button
onClick={() => this.sendStep({ ...step, url_matching: 'exact' })}
type="button"
className={'btn btn-sm ' + (step.url_matching === 'exact' ? 'btn-secondary' : 'btn-light')}
>
matches exactly
</button>
</div>
<Radio.Group
buttonStyle="solid"
onChange={handleURLMatchChange}
value={step.url_matching || 'contains'}
size="small"
style={{ paddingBottom: 16 }}
>
<Radio.Button value="contains">contains</Radio.Button>
<Radio.Button value="regex">matches regex</Radio.Button>
<Radio.Button value="exact">matches exactly</Radio.Button>
</Radio.Group>
)
}
render() {
let { step, isEditor, actionId, isOnlyStep } = this.props
return (
<div
className={isEditor ? '' : 'card'}
<Card
style={{
marginBottom: 0,
background: isEditor ? 'rgba(0,0,0,0.05)' : '',
}}
>
<div className={isEditor ? '' : 'card-body'}>
<div>
{!isOnlyStep && (!isEditor || step.event === '$autocapture' || !step.event) && (
<button
style={{
margin: isEditor ? '12px 12px 0px 0px' : '-3px 0 0 0',
border: 0,
float: 'right',
color: 'hsl(0, 0%, 80%)',
}}
type="button"
className="close pull-right"
aria-label="Close"
onClick={this.props.onDelete}
>
@ -332,15 +301,14 @@ export class ActionStep extends Component {
}}
>
{isEditor && [
<button
<Button
key="inspect-button"
type="button"
className="btn btn-sm btn-secondary"
size="small"
style={{ margin: '10px 0px 10px 12px' }}
onClick={() => this.start()}
>
Inspect element
</button>,
</Button>,
this.state.inspecting && (
<p key="inspect-prompt" style={{ marginLeft: 10, marginRight: 10 }}>
Hover over and click on an element you want to create an action for
@ -394,7 +362,7 @@ export class ActionStep extends Component {
)}
</div>
</div>
</div>
</Card>
)
}
}

View File

@ -4,7 +4,8 @@ import { AppEditorLink } from 'lib/components/AppEditorLink/AppEditorLink'
import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters'
import PropTypes from 'prop-types'
import { URL_MATCHING_HINTS } from 'scenes/actions/hints'
import { Col } from 'antd'
import { Card, Checkbox, Col, Input, Radio } from 'antd'
import { ExportOutlined } from '@ant-design/icons'
let getSafeText = (el) => {
if (!el.childNodes || !el.childNodes.length) return
@ -128,11 +129,9 @@ export class ActionStep extends Component {
}
return (
<div className={'form-group ' + (this.state.selection.indexOf(props.item) > -1 && 'selected')}>
<div className={'mb ' + (this.state.selection.indexOf(props.item) > -1 && 'selected')}>
<label>
<input
type="checkbox"
name="selection"
<Checkbox
checked={this.state.selection.indexOf(props.item) > -1}
value={props.item}
onChange={(e) => {
@ -148,11 +147,10 @@ export class ActionStep extends Component {
{props.label} {props.extra_options}
</label>
{props.item === 'selector' ? (
<textarea className="form-control" onChange={onChange} value={this.props.step[props.item] || ''} />
<Input.TextArea onChange={onChange} value={this.props.step[props.item] || ''} />
) : (
<input
<Input
data-attr="edit-action-url-input"
className="form-control"
onChange={onChange}
value={this.props.step[props.item] || ''}
/>
@ -162,56 +160,43 @@ export class ActionStep extends Component {
}
TypeSwitcher = () => {
let { step } = this.props
const handleChange = (e) => {
const type = e.target.value
if (type === '$autocapture') {
this.setState(
{
selection: Object.keys(step).filter((key) => key !== 'id' && key !== 'isNew' && step[key]),
},
() => this.sendStep({ ...step, event: '$autocapture' })
)
} else if (type === 'event') {
this.setState({ selection: [] }, () => this.sendStep({ ...step, event: '' }))
} else if (type === '$pageview') {
this.setState({ selection: ['url'] }, () =>
this.sendStep({
...step,
event: '$pageview',
url: step.url,
})
)
}
}
return (
<div>
<div className="type-switcher btn-group">
<button
type="button"
onClick={() =>
this.setState(
{
selection: Object.keys(step).filter(
(key) => key !== 'id' && key !== 'isNew' && step[key]
),
},
() => this.sendStep({ ...step, event: '$autocapture' })
)
}
className={'btn ' + (step.event === '$autocapture' ? 'btn-secondary' : 'btn-light btn-action')}
>
Autocapture
</button>
<button
type="button"
onClick={() => this.setState({ selection: [] }, () => this.sendStep({ ...step, event: '' }))}
className={
'btn ' +
(typeof step.event !== 'undefined' &&
step.event !== '$autocapture' &&
step.event !== '$pageview'
? 'btn-secondary'
: 'btn-light btn-action')
}
>
Custom event
</button>
<button
type="button"
onClick={() => {
this.setState({ selection: ['url'] }, () =>
this.sendStep({
...step,
event: '$pageview',
url: step.url,
})
)
}}
className={'btn ' + (step.event === '$pageview' ? 'btn-secondary' : 'btn-light btn-action')}
data-attr="action-step-pageview"
>
Page view
</button>
</div>
<Radio.Group
buttonStyle="solid"
onChange={handleChange}
value={
step.event === '$autocapture' || step.event === '$pageview' || step.event === undefined
? step.event
: 'event'
}
>
<Radio.Button value="$autocapture">Autocapture</Radio.Button>
<Radio.Button value="event">Custom event</Radio.Button>
<Radio.Button value="$pageview">Page view</Radio.Button>
</Radio.Group>
</div>
)
}
@ -226,8 +211,8 @@ export class ActionStep extends Component {
return (
<div>
<span>
<AppEditorLink actionId={actionId} style={{ margin: '1rem 0' }} className="btn btn-sm btn-light">
Select element on site <i className="fi flaticon-export" />
<AppEditorLink actionId={actionId} style={{ margin: '1rem 0' }}>
Select element on site <ExportOutlined />
</AppEditorLink>
<a
href="https://posthog.com/docs/features/actions"
@ -258,33 +243,21 @@ export class ActionStep extends Component {
)
}
URLMatching = ({ step }) => {
const handleURLMatchChange = (e) => {
this.sendStep({ ...step, url_matching: e.target.value })
}
return (
<div className="btn-group" style={{ margin: '0 0 0 8px' }}>
<button
onClick={() => this.sendStep({ ...step, url_matching: 'contains' })}
type="button"
className={
'btn btn-sm ' +
(!step.url_matching || step.url_matching === 'contains' ? 'btn-secondary' : 'btn-light')
}
>
contains
</button>
<button
onClick={() => this.sendStep({ ...step, url_matching: 'regex' })}
type="button"
className={'btn btn-sm ' + (step.url_matching === 'regex' ? 'btn-secondary' : 'btn-light')}
>
matches regex
</button>
<button
onClick={() => this.sendStep({ ...step, url_matching: 'exact' })}
type="button"
className={'btn btn-sm ' + (step.url_matching === 'exact' ? 'btn-secondary' : 'btn-light')}
>
matches exactly
</button>
</div>
<Radio.Group
buttonStyle="solid"
onChange={handleURLMatchChange}
value={step.url_matching || 'contains'}
size="small"
style={{ paddingBottom: 16 }}
>
<Radio.Button value="contains">contains</Radio.Button>
<Radio.Button value="regex">matches regex</Radio.Button>
<Radio.Button value="exact">matches exactly</Radio.Button>
</Radio.Group>
)
}
render() {
@ -292,9 +265,9 @@ export class ActionStep extends Component {
return (
<Col span={24} md={12}>
<div className="action-step card" style={{ overflow: 'visible' }}>
<Card className="action-step" style={{ overflow: 'visible' }}>
{index > 0 && <div className="match-condition-badge mc-main mc-or">OR</div>}
<div className="card-body">
<div>
{!isOnlyStep && (
<div className="remove-wrapper">
<button type="button" aria-label="delete" onClick={onDelete}>
@ -344,9 +317,9 @@ export class ActionStep extends Component {
{step.event && (
<div className="property-filters">
<div className="section-title">Filters</div>
<h3 className="l3">Filters</h3>
{(!step.properties || step.properties.length === 0) && (
<div className="empty-state">This match group has no additional filters.</div>
<div className="text-muted">This match group has no additional filters.</div>
)}
<PropertyFilters
propertyFilters={step.properties}
@ -363,7 +336,7 @@ export class ActionStep extends Component {
)}
</div>
</div>
</div>
</Card>
</Col>
)
}

View File

@ -7,7 +7,7 @@
}
.t-element {
display: flex;
align-items: center;
align-items: flex-start;
border-right: 0.75px solid #c4c4c4;
padding-left: 32px;
@ -40,13 +40,8 @@
.action-edit-container {
margin-top: 32px;
label {
font-weight: bold;
}
.match-group-section {
margin-top: 32px;
padding: 16px;
}
.match-group-add-skeleton {
@ -99,16 +94,11 @@
}
.property-filters {
border-top: 1px solid rgba(#a7b7be, 0.5);
margin-top: 16px;
padding-top: 16px;
.section-title {
font-weight: bold;
}
.empty-state {
color: #a7b7be;
}
}
}

View File

@ -2,7 +2,7 @@ import React from 'react'
import './Actions.scss'
import { Link } from 'lib/components/Link'
import { Table } from 'antd'
import { QuestionCircleOutlined } from '@ant-design/icons'
import { QuestionCircleOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
import { DeleteWithUndo } from 'lib/utils'
import { useActions, useValues } from 'kea'
import { actionsModel } from '~/models/actionsModel'
@ -12,6 +12,7 @@ import moment from 'moment'
import imgGrouping from 'public/actions-tutorial-grouping.svg'
import imgStandardized from 'public/actions-tutorial-standardized.svg'
import imgRetroactive from 'public/actions-tutorial-retroactive.svg'
import { PageHeader } from 'lib/components/PageHeader'
export function ActionsTable() {
const { actions, actionsLoading } = useValues(actionsModel({ params: 'include_count=1' }))
@ -86,7 +87,7 @@ export function ActionsTable() {
return (
<span>
<Link to={'/action/' + action.id}>
<i className="fi flaticon-edit" />
<EditOutlined />
</Link>
<DeleteWithUndo
endpoint="action"
@ -95,7 +96,7 @@ export function ActionsTable() {
style={{ marginLeft: 8 }}
callback={loadActions}
>
<i className="fi flaticon-basket" />
<DeleteOutlined />
</DeleteWithUndo>
</span>
)
@ -103,12 +104,19 @@ export function ActionsTable() {
},
]
return (
<div>
<h1 className="page-header">Actions</h1>
{!featureFlags['actions-ux-201012'] && (
<p style={{ maxWidth: 600 }}>
<i>
const Caption = () => {
return (
<>
{featureFlags['actions-ux-201012'] && (
<div>
Actions can retroactively group one or more raw events to help provide consistent analytics.{' '}
<a href="https://posthog.com/docs/features/actions" target="_blank">
<QuestionCircleOutlined />
</a>
</div>
)}
{!featureFlags['actions-ux-201012'] && (
<div>
Actions are PostHogs way of easily cleaning up a large amount of Event data. Actions consist of
one or more events that you have decided to put into a manually-labelled bucket. They're used in
Funnels, Live actions and Trends.
@ -117,15 +125,17 @@ export function ActionsTable() {
<a href="https://posthog.com/docs/features/actions" target="_blank" rel="noopener noreferrer">
See documentation
</a>
</i>
</p>
)}
</div>
)}
</>
)
}
return (
<div>
<PageHeader title="Actions" caption={<Caption />} />
{featureFlags['actions-ux-201012'] && (
<div>
Actions can retroactively group one or more raw events to help provide consistent analytics.{' '}
<a href="https://posthog.com/docs/features/actions" target="_blank">
<QuestionCircleOutlined />
</a>
<div className="tutorial-container">
<div className="t-element">
<div>
@ -161,22 +171,24 @@ export function ActionsTable() {
</div>
</div>
)}
<div style={{ margin: '32px 0' }}>
<NewActionButton />
<div>
<div className="mb text-right">
<NewActionButton />
</div>
<Table
size="small"
columns={columns}
loading={actionsLoading}
rowKey={(action) => action.id}
pagination={{ pageSize: 100, hideOnSinglePage: true }}
dataSource={actions}
locale={
featureFlags['actions-ux-201012']
? { emptyText: 'The first step to standardized analytics is creating your first action.' }
: {}
}
/>
</div>
<Table
size="small"
columns={columns}
loading={actionsLoading}
rowKey={(action) => action.id}
pagination={{ pageSize: 100, hideOnSinglePage: true }}
dataSource={actions}
locale={
featureFlags['actions-ux-201012']
? { emptyText: 'The first step to standardized analytics is creating your first action.' }
: {}
}
/>
</div>
)
}

View File

@ -11,6 +11,8 @@ import { DeleteOutlined, RedoOutlined, ProjectOutlined, DeploymentUnitOutlined,
import { AnnotationScope, annotationScopeToName } from 'lib/constants'
import { userLogic } from 'scenes/userLogic'
import rrwebBlockClass from 'lib/utils/rrwebBlockClass'
import { PageHeader } from 'lib/components/PageHeader'
import { PlusOutlined } from '@ant-design/icons'
const { TextArea } = Input
@ -98,54 +100,58 @@ function _Annotations(): JSX.Element {
return (
<div>
<h1 className="page-header">Annotations</h1>
<p style={{ maxWidth: 600 }}>
<i>
Here you can add organization- and project-wide annotations.
<br />
Dashboard-specific ones can be added directly in the dashboard.
<br />
Edit any annotation by clicking on it below.
</i>
</p>
<Button className="mb-4" type="primary" data-attr="create-annotation" onClick={(): void => setOpen(true)}>
+ Create Annotation
</Button>
<Table
data-attr="annotations-table"
size="small"
rowKey={(item): string => item.id}
pagination={{ pageSize: 99999, hideOnSinglePage: true }}
rowClassName="cursor-pointer"
dataSource={annotations}
columns={columns}
loading={annotationsLoading}
onRow={(annotation): HTMLAttributes<HTMLElement> => ({
onClick: (): void => {
setSelected(annotation)
setOpen(true)
},
})}
<PageHeader
title="Annotations"
caption="Here you can add organization- and project-wide annotations. Dashboard-specific ones can be added directly in the dashboard."
/>
<div
style={{
visibility: next ? 'visible' : 'hidden',
margin: '2rem auto 5rem',
textAlign: 'center',
}}
>
{loadingNext ? (
<Spin />
) : (
<div>
<div className="mb text-right">
<Button
type="primary"
onClick={(): void => {
loadAnnotationsNext()
}}
data-attr="create-annotation"
onClick={(): void => setOpen(true)}
icon={<PlusOutlined />}
>
{'Load more annotations'}
Create Annotation
</Button>
)}
</div>
<Table
data-attr="annotations-table"
size="small"
rowKey={(item): string => item.id}
pagination={{ pageSize: 99999, hideOnSinglePage: true }}
rowClassName="cursor-pointer"
dataSource={annotations}
columns={columns}
loading={annotationsLoading}
onRow={(annotation): HTMLAttributes<HTMLElement> => ({
onClick: (): void => {
setSelected(annotation)
setOpen(true)
},
})}
/>
<div
style={{
visibility: next ? 'visible' : 'hidden',
margin: '2rem auto 5rem',
textAlign: 'center',
}}
>
{loadingNext ? (
<Spin />
) : (
<Button
type="primary"
onClick={(): void => {
loadAnnotationsNext()
}}
>
{'Load more annotations'}
</Button>
)}
</div>
</div>
<CreateAnnotationModal
visible={open}
@ -256,7 +262,7 @@ function CreateAnnotationModal(props: CreateAnnotationModalProps): JSX.Element {
</Menu>
}
>
<Button>
<Button style={{ marginLeft: 8, marginRight: 8 }}>
{annotationScopeToName.get(scope)} <DownOutlined />
</Button>
</Dropdown>{' '}
@ -267,7 +273,7 @@ function CreateAnnotationModal(props: CreateAnnotationModalProps): JSX.Element {
<span>Change existing annotation text</span>
{!props.annotation?.deleted ? (
<DeleteOutlined
className="button-border clickable"
className="text-danger"
onClick={(): void => {
props.onDelete()
}}
@ -287,7 +293,7 @@ function CreateAnnotationModal(props: CreateAnnotationModalProps): JSX.Element {
<div>
Date:
<DatePicker
className="mb-2 mt-2 ml-2"
style={{ marginTop: 16, marginLeft: 8, marginBottom: 16 }}
getPopupContainer={(trigger): HTMLElement => trigger.parentElement}
value={selectedDate}
onChange={(date): void => setDate(date)}

View File

@ -5,6 +5,7 @@ import { billingLogic } from './billingLogic'
import { Card, Progress, Row, Col, Button, Popconfirm, Spin } from 'antd'
import defaultImg from 'public/plan-default.svg'
import { PlanInterface } from '~/types'
import { PageHeader } from 'lib/components/PageHeader'
function Plan({ plan, onUpgrade }: { plan: PlanInterface; onUpgrade: (plan: PlanInterface) => void }): JSX.Element {
return (
@ -38,9 +39,13 @@ export function Billing(): JSX.Element {
return (
<>
<h1 className="page-header">
Billing &amp; usage information <span style={{ fontSize: 12, color: '#F7A501' }}>BETA</span>
</h1>
<PageHeader
title={
<>
Billing &amp; usage information <span style={{ fontSize: 12, color: '#F7A501' }}>BETA</span>
</>
}
/>
<div className="space-top" />
<Card title="Current usage">
{user?.billing?.current_usage && (

View File

@ -18,7 +18,7 @@ function _Dashboard({ id, shareToken }) {
const { dashboardsLoading } = useValues(dashboardsModel)
return (
<div>
<div style={{ marginTop: 32 }}>
{!shareToken && <DashboardHeader id={id} logic={logic} />}
{dashboardsLoading ? (

View File

@ -9,6 +9,7 @@ import { Table } from 'antd'
import { PushpinFilled, PushpinOutlined, DeleteOutlined, AppstoreAddOutlined } from '@ant-design/icons'
import { hot } from 'react-hot-loader/root'
import { NewDashboard } from 'scenes/dashboard/NewDashboard'
import { PageHeader } from 'lib/components/PageHeader'
export const Dashboards = hot(_Dashboards)
function _Dashboards(): JSX.Element {
@ -19,16 +20,16 @@ function _Dashboards(): JSX.Element {
return (
<div>
<div style={{ marginBottom: 20 }}>
<PageHeader title="Dashboards" />
<div className="mb text-right">
<Button
data-attr={'new-dashboard'}
onClick={() => setOpenNewDashboard(true)}
style={{ float: 'right' }}
type="primary"
icon={<PlusOutlined />}
>
<PlusOutlined style={{ verticalAlign: 'baseline' }} />
New Dashboard
</Button>
<h1 className="page-header">Dashboards</h1>
</div>
{openNewDashboard && (
@ -42,98 +43,99 @@ function _Dashboards(): JSX.Element {
<NewDashboard />
</Drawer>
)}
<Card>
{dashboardsLoading ? (
<Spin />
) : dashboards.length > 0 ? (
<Table
dataSource={dashboards}
rowKey="id"
size="small"
pagination={{ pageSize: 100, hideOnSinglePage: true }}
>
<Table.Column
title=""
width={24}
align="center"
render={({ id, pinned }) => (
<span
onClick={() => (pinned ? unpinDashboard(id) : pinDashboard(id))}
style={{ color: 'rgba(0, 0, 0, 0.85)', cursor: 'pointer' }}
>
{pinned ? <PushpinFilled /> : <PushpinOutlined />}
</span>
)}
/>
<Table.Column
title="Dashboard"
dataIndex="name"
key="name"
render={(name, { id }, index) => (
<Link data-attr={'dashboard-name-' + index} to={`/dashboard/${id}`}>
{name || 'Untitled'}
</Link>
)}
/>
<Table.Column
title="Actions"
align="center"
width={120}
render={({ id }) => (
<span
style={{ cursor: 'pointer' }}
onClick={() => deleteDashboard({ id, redirect: false })}
className="text-danger"
>
<DeleteOutlined /> Delete
</span>
)}
/>
</Table>
) : (
<div>
<p>Create your first dashboard:</p>
{dashboardsLoading ? (
<Spin />
) : dashboards.length > 0 ? (
<Table
dataSource={dashboards}
rowKey="id"
size="small"
pagination={{ pageSize: 100, hideOnSinglePage: true }}
>
<Table.Column
title=""
width={24}
align="center"
render={({ id, pinned }) => (
<span
onClick={() => (pinned ? unpinDashboard(id) : pinDashboard(id))}
style={{ color: 'rgba(0, 0, 0, 0.85)', cursor: 'pointer' }}
>
{pinned ? <PushpinFilled /> : <PushpinOutlined />}
</span>
)}
/>
<Table.Column
title="Dashboard"
dataIndex="name"
key="name"
render={(name, { id }, index) => (
<Link data-attr={'dashboard-name-' + index} to={`/dashboard/${id}`}>
{name || 'Untitled'}
</Link>
)}
/>
<Table.Column
title="Actions"
align="center"
width={120}
render={({ id }) => (
<span
style={{ cursor: 'pointer' }}
onClick={() => deleteDashboard({ id, redirect: false })}
className="text-danger"
>
<DeleteOutlined /> Delete
</span>
)}
/>
</Table>
) : (
<div>
<p>Create your first dashboard:</p>
<Row gutter={24}>
<Col xs={24} xl={6}>
<Card
title="Empty"
size="small"
style={{ cursor: 'pointer' }}
onClick={() =>
addDashboard({
name: 'New Dashboard',
show: true,
useTemplate: '',
})
}
>
<div style={{ textAlign: 'center', fontSize: 40 }}>
<AppstoreAddOutlined />
</div>
</Card>
</Col>
<Col xs={24} xl={6}>
<Card
title="App Default"
size="small"
style={{ cursor: 'pointer' }}
onClick={() =>
addDashboard({
name: 'Web App Dashboard',
show: true,
useTemplate: 'DEFAULT_APP',
})
}
>
<div style={{ textAlign: 'center', fontSize: 40 }}>
<AppstoreAddOutlined />
</div>
</Card>
</Col>
</Row>
</div>
)}
<Row gutter={24}>
<Col xs={24} xl={6}>
<Card
title="Empty"
size="small"
style={{ cursor: 'pointer' }}
onClick={() =>
addDashboard({
name: 'New Dashboard',
show: true,
useTemplate: '',
})
}
>
<div style={{ textAlign: 'center', fontSize: 40 }}>
<AppstoreAddOutlined />
</div>
</Card>
</Col>
<Col xs={24} xl={6}>
<Card
title="App Default"
size="small"
style={{ cursor: 'pointer' }}
onClick={() =>
addDashboard({
name: 'Web App Dashboard',
show: true,
useTemplate: 'DEFAULT_APP',
})
}
>
<div style={{ textAlign: 'center', fontSize: 40 }}>
<AppstoreAddOutlined />
</div>
</Card>
</Col>
</Row>
</div>
)}
</Card>
</div>
)
}

View File

@ -16,7 +16,7 @@ initKea()
let dashboard = window.__SHARED_DASHBOARD__
ReactDOM.render(
<Provider store={getContext().store}>
<div style={{ background: 'var(--gray-background)', minHeight: '100vh', top: 0 }}>
<div style={{ minHeight: '100vh', top: 0 }}>
<Row style={{ marginBottom: '1rem' }}>
<Col sm={7} xs={24} style={{ padding: '1rem' }}>
<a href="https://posthog.com" target="_blank" rel="noopener noreferrer">

View File

@ -18,7 +18,7 @@ export function EventElements({ event }) {
margin: 0,
padding: 0,
borderRadius: 0,
backgroundColor: index === elements.length - 1 ? 'var(--blue)' : undefined,
backgroundColor: index === elements.length - 1 ? 'var(--primary)' : undefined,
}}
>
{indent(index)}

View File

@ -11,10 +11,10 @@ import { router } from 'kea-router'
import { FilterPropertyLink } from 'lib/components/FilterPropertyLink'
import { Property } from 'lib/components/Property'
import { EventName } from 'scenes/actions/EventName'
import { PageHeader } from 'lib/components/PageHeader'
import { eventToName, toParams } from 'lib/utils'
import rrwebBlockClass from 'lib/utils/rrwebBlockClass'
import './EventsTable.scss'
export function EventsTable({
fixedFilters,
@ -161,15 +161,17 @@ export function EventsTable({
return (
<div className="events" data-attr="events-table">
<h1 className="page-header">
{isLiveActions
? 'Live Actions'
: isPersonPage
? ''
: !featureFlags['actions-ux-201012']
? 'Events'
: 'Raw Events Stream'}
</h1>
<PageHeader
title={
isLiveActions
? 'Live Actions'
: isPersonPage
? ''
: !featureFlags['actions-ux-201012']
? 'Events'
: 'Raw Events Stream'
}
/>
{filtersEnabled ? <PropertyFilters pageKey={isLiveActions ? 'LiveActionsTable' : 'EventsTable'} /> : null}
<Tooltip title="Up to 100,000 latest events.">
<Button
@ -186,50 +188,53 @@ export function EventsTable({
Export
</Button>
</Tooltip>
<Table
dataSource={eventsFormatted}
loading={isLoading}
columns={columns}
size="small"
className={rrwebBlockClass + ' ph-no-capture'}
locale={{
emptyText: (
<span>
You don't have any items here! If you haven't integrated PostHog yet,{' '}
<Link to="/project">click here to set PostHog up on your app</Link>.
</span>
),
}}
pagination={{ pageSize: 99999, hideOnSinglePage: true }}
rowKey={(row) => (row.event ? row.event.id + '-' + row.event.actionId : row.date_break)}
rowClassName={(row) => {
if (row.event) return 'event-row ' + (row.event.event === '$exception' && 'event-row-is-exception')
if (row.date_break) return 'event-day-separator'
if (row.new_events) return 'event-row-new'
}}
expandable={{
expandedRowRender: function renderExpand({ event }) {
return <EventDetails event={event} />
},
rowExpandable: ({ event }) => event,
expandRowByClick: true,
}}
onRow={(row) => ({
onClick: () => {
if (row.new_events) prependNewEvents(newEvents)
},
})}
/>
<div
style={{
visibility: hasNext || isLoadingNext ? 'visible' : 'hidden',
margin: '2rem auto 5rem',
textAlign: 'center',
}}
>
<Button type="primary" onClick={fetchNextEvents}>
{isLoadingNext ? <Spin /> : 'Load more events'}
</Button>
<div>
<Table
dataSource={eventsFormatted}
loading={isLoading}
columns={columns}
size="small"
className={rrwebBlockClass + ' ph-no-capture'}
locale={{
emptyText: (
<span>
You don't have any items here! If you haven't integrated PostHog yet,{' '}
<Link to="/project">click here to set PostHog up on your app</Link>.
</span>
),
}}
pagination={{ pageSize: 99999, hideOnSinglePage: true }}
rowKey={(row) => (row.event ? row.event.id + '-' + row.event.actionId : row.date_break)}
rowClassName={(row) => {
if (row.event)
return 'event-row ' + (row.event.event === '$exception' && 'event-row-is-exception')
if (row.date_break) return 'event-day-separator'
if (row.new_events) return 'event-row-new'
}}
expandable={{
expandedRowRender: function renderExpand({ event }) {
return <EventDetails event={event} />
},
rowExpandable: ({ event }) => event,
expandRowByClick: true,
}}
onRow={(row) => ({
onClick: () => {
if (row.new_events) prependNewEvents(newEvents)
},
})}
/>
<div
style={{
visibility: hasNext || isLoadingNext ? 'visible' : 'hidden',
margin: '2rem auto 5rem',
textAlign: 'center',
}}
>
<Button type="primary" onClick={fetchNextEvents}>
{isLoadingNext ? <Spin /> : 'Load more events'}
</Button>
</div>
</div>
<div style={{ marginTop: '5rem' }} />
</div>

View File

@ -1,6 +1,9 @@
@import '~/vars';
.events {
.event-day-separator {
background-color: $gray-300;
background-color: darken($bg_mid, 10%);
font-weight: bold;
text-align: center;
}
.event-row {
@ -10,7 +13,9 @@
}
.event-row-new,
.event-new-events {
background-color: $gray-300;
background-color: darken($bg_mid, 10%);
font-weight: bold;
transition: background-color 0.3s ease-out;
}
.event-row-is-exception {
background-color: lighten($danger, 40%);

View File

@ -7,6 +7,8 @@ import moment from 'moment'
import { EditFeatureFlag } from './EditFeatureFlag'
import rrwebBlockClass from 'lib/utils/rrwebBlockClass'
import { LinkButton } from 'lib/components/LinkButton'
import { PageHeader } from 'lib/components/PageHeader'
import { PlusOutlined } from '@ant-design/icons'
export const FeatureFlags = hot(_FeatureFlags)
function _FeatureFlags() {
@ -72,15 +74,20 @@ function _FeatureFlags() {
return (
<div className="feature_flags">
<h1 className="page-header">Feature Flags</h1>
<p style={{ maxWidth: 600 }}>
<i>Feature flags are a way of turning functionality in your app on or off, based on user properties.</i>
</p>
<Button type="primary" onClick={() => setOpenFeatureFlag('new')} data-attr="new-feature-flag">
+ New Feature Flag
</Button>
<br />
<br />
<PageHeader
title="Feature Flags"
caption="Feature flags are a way of turning functionality in your app on or off, based on user properties."
/>
<div className="mb text-right">
<Button
type="primary"
onClick={() => setOpenFeatureFlag('new')}
data-attr="new-feature-flag"
icon={<PlusOutlined />}
>
New Feature Flag
</Button>
</div>
<Table
dataSource={featureFlags}
columns={columns}

View File

@ -0,0 +1,18 @@
@import '~/vars';
.funnel-people {
.funnel-success {
background-color: lighten($primary, 20%);
}
.funnel-dropped {
background-color: $bg_mid;
}
table {
table-layout: fixed;
border-top: 0;
margin-bottom: 0;
td {
padding: 0 0.75rem;
}
}
}

View File

@ -4,6 +4,7 @@ import { Loading, humanFriendlyDuration } from 'lib/utils'
import PropTypes from 'prop-types'
import { useValues, useActions } from 'kea'
import { funnelVizLogic } from 'scenes/funnels/funnelVizLogic'
import './FunnelViz.scss'
export function FunnelViz({ steps: stepsParam, dashboardItemId, funnelId, cachedResults }) {
const container = useRef()
@ -25,7 +26,7 @@ export function FunnelViz({ steps: stepsParam, dashboardItemId, funnelId, cached
}`
),
values: steps.map((step) => step.count),
colors: ['#66b0ff', 'var(--blue)'],
colors: ['#66b0ff', 'var(--primary)'],
},
displayPercent: true,
})

View File

@ -0,0 +1,87 @@
@import '~/vars';
@import 'node_modules/funnel-graph-js/src/scss/main';
.svg-funnel-js {
.svg-funnel-js__container {
width: 100%;
height: 100%;
}
.svg-funnel-js__labels {
width: 100%;
box-sizing: border-box;
.svg-funnel-js__label {
flex: 1 1 0;
position: relative;
.label__value {
display: none;
}
.label__title {
margin-top: 1rem;
font-size: 12px;
font-weight: 300;
}
.label__percentage {
font-size: 16px;
font-weight: bold;
}
}
}
&:not(.svg-funnel-js--vertical) {
padding-top: 64px;
padding-bottom: 16px;
.svg-funnel-js__label {
padding-left: 24px;
&:not(:first-child) {
border-left: 1px solid $border;
}
}
}
&.svg-funnel-js--vertical {
padding-left: 120px;
padding-right: 16px;
.svg-funnel-js__label {
padding-top: 24px;
&:not(:first-child) {
border-top: 1px solid $border;
}
}
}
.svg-funnel-js__subLabels {
display: flex;
justify-content: center;
margin-top: 24px;
position: absolute;
width: 100%;
left: 0;
.svg-funnel-js__subLabel {
display: flex;
font-size: 12px;
color: $text_light;
line-height: 16px;
&:not(:first-child) {
margin-left: 16px;
}
.svg-funnel-js__subLabel--color {
width: 12px;
height: 12px;
border-radius: 50%;
margin: 2px 8px 2px 0;
}
}
}
}

View File

@ -2,20 +2,22 @@ import React from 'react'
import { useValues } from 'kea'
import { funnelLogic } from './funnelLogic'
import { Link } from 'lib/components/Link'
import { Card, percentage, Loading } from 'lib/utils'
import { percentage, Loading } from 'lib/utils'
import { EntityTypes } from 'scenes/insights/trendsLogic'
import './FunnelPeople.scss'
import { Card } from 'antd'
export function People() {
const { stepsWithCount, peopleSorted, peopleLoading } = useValues(funnelLogic)
return (
<Card title="Per user" style={{ boxShadow: 'none', marginBottom: 0 }}>
<Card title="Per user" className="funnel-people">
{peopleLoading && <Loading style={{ minHeight: 50 }} />}
{!peopleSorted && !peopleLoading && (
<div style={{ textAlign: 'center', margin: '3rem 0' }}>No users found for this funnel.</div>
)}
{peopleSorted && peopleSorted.length > 0 && (
<table className="table table-bordered table-fixed">
<table className="table-bordered full-width">
<tbody>
<tr>
<th />

View File

@ -33,16 +33,15 @@ export function CardContainer({
{`Step ${index + 1} ${totalSteps ? 'of' : ''} ${totalSteps ? totalSteps : ''}`}
</Row>
}
className="card"
style={{ width: '65vw', maxHeight: '70vh', overflow: 'auto' }}
>
{children}
</Card>
{nextButton && (
<Card
<div
data-attr="wizard-continue-button"
className="card big-button"
className="bg-primary"
role="button"
style={{
marginTop: 20,
@ -53,12 +52,11 @@ export function CardContainer({
justifyContent: 'center',
borderRadius: 5,
cursor: 'pointer',
backgroundColor: '#007bff',
}}
onClick={onSubmit}
>
<span style={{ fontWeight: 500, fontSize: 18, color: 'white' }}>Continue</span>
</Card>
</div>
)}
</Col>
)

View File

@ -31,7 +31,7 @@ export function ActionFilter({ setFilters, filters, typeKey, hideMathSelector, c
hideMathSelector={hideMathSelector}
/>
))}
<div style={!featureFlags['actions-ux-201012'] ? {} : { paddingTop: '0.5rem' }}>
<div className="mt">
<Button
type="primary"
onClick={() => addFilter()}

View File

@ -7,6 +7,7 @@ import { actionsModel } from '~/models/actionsModel'
import { ExportOutlined } from '@ant-design/icons'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { Link } from 'lib/components/Link'
import './ActionFilterDropdown.scss'
export function ActionFilterDropdown({ onClickOutside, logic }) {
const dropdownRef = useRef()
@ -69,9 +70,13 @@ export function ActionPanelContainer({ entityType, panelIndex, options, logic })
if (selectedFilter && selectedFilter.type === EntityTypes.ACTIONS && entityType === EntityTypes.ACTIONS) {
const action = entities[selectedFilter.type].filter((a) => a.id === selectedFilter.filter.id)[0]
return (
<a href={'/action/' + selectedFilter.filter.id} target="_blank" rel="noopener noreferrer">
Edit "{action.name}" <i className="fi flaticon-export" />
</a>
<div style={{ textAlign: 'right', paddingBottom: 8 }}>
<Link to={'/action/' + selectedFilter.filter.id} target="_blank">
<>
Edit "{action.name}" <ExportOutlined />
</>
</Link>
</div>
)
} else {
return null

View File

@ -1,13 +1,12 @@
import React, { useRef, useState } from 'react'
import { useActions, useValues } from 'kea'
import { EntityTypes } from '../trendsLogic'
import { CloseButton } from '~/lib/utils'
import { Dropdown } from '~/lib/components/Dropdown'
import { ActionFilterDropdown } from './ActionFilterDropdown'
import { Tooltip } from 'antd'
import { Button, Tooltip, Dropdown, Menu, Col, Row } from 'antd'
import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters'
import { userLogic } from 'scenes/userLogic'
import { DownOutlined } from '@ant-design/icons'
import { CloseButton } from 'lib/components/CloseButton'
const MATHS = {
total: {
@ -140,30 +139,32 @@ export function ActionFilterRow({ logic, filter, index, hideMathSelector }) {
value = entity.id || filter.id
}
return (
<div className="mt-2" style={{ paddingBottom: 16 }}>
<button
data-attr={'trend-element-subject-' + index}
ref={node}
className="filter-action btn btn-sm btn-light"
type="button"
onClick={onClick}
style={{
fontWeight: 500,
}}
>
{name || 'Select action'}
<DownOutlined style={{ marginLeft: '3px', color: 'rgba(0, 0, 0, 0.25)' }} />
</button>
{!hideMathSelector && (
<MathSelector
math={math}
index={index}
onMathSelect={onMathSelect}
areEventPropertiesNumericalAvailable={
eventPropertiesNumerical && eventPropertiesNumerical.length > 0
}
/>
)}
<div>
<Row gutter={8} className="mt">
<Col>
<Button
data-attr={'trend-element-subject-' + index}
ref={node}
onClick={onClick}
className="ant-btn-md"
>
{name || 'Select action'}
<DownOutlined style={{ fontSize: 10 }} />
</Button>
</Col>
<Col>
{!hideMathSelector && (
<MathSelector
math={math}
index={index}
onMathSelect={onMathSelect}
areEventPropertiesNumericalAvailable={
eventPropertiesNumerical && eventPropertiesNumerical.length > 0
}
/>
)}
</Col>
</Row>
{!hideMathSelector && MATHS[math]?.onProperty && (
<MathPropertySelector
name={name}
@ -175,27 +176,27 @@ export function ActionFilterRow({ logic, filter, index, hideMathSelector }) {
/>
)}
<div style={{ paddingTop: 6 }}>
<span style={{ color: '#C4C4C4', fontSize: 18, paddingLeft: 6 }}>&#8627;</span>
<div
className="btn btn-sm btn-light ml-2"
<span style={{ color: '#C4C4C4', fontSize: 18, paddingLeft: 6, paddingRight: 2 }}>&#8627;</span>
<Button
className="ant-btn-md"
onClick={() => setEntityFilterVisible(!entityFilterVisible)}
data-attr={'show-prop-filter-' + index}
>
{determineFilterLabel(entityFilterVisible, filter)}
</div>
</Button>
<CloseButton
className="ml-2"
onClick={onClose}
style={{
float: 'none',
position: 'absolute',
marginTop: 3,
marginLeft: 4,
}}
/>
</div>
{entityFilterVisible && (
<div className="ml-3">
<div className="ml">
<PropertyFilters
pageKey={`${index}-${value}-filter`}
properties={eventProperties}
@ -225,49 +226,46 @@ function MathSelector({ math, index, onMathSelect, areEventPropertiesNumericalAv
areEventPropertiesNumericalAvailable ? '' : ' None have been found yet!'
}`
return (
<Dropdown
title={MATHS[math || 'total']?.name}
buttonClassName="btn btn-sm btn-light ml-2"
data-attr={`math-selector-${index}`}
>
{MATH_ENTRIES.map(([key, { name, description, onProperty }]) => {
const disabled = onProperty && !areEventPropertiesNumericalAvailable
return (
<Tooltip
placement="right"
title={
onProperty ? (
<>
{description}
<br />
{numericalNotice}
</>
) : (
description
)
}
key={`math-${key}`}
>
<div>
<button
className="dropdown-item"
disabled={disabled}
onClick={
disabled
? undefined
: () => {
onMathSelect(index, key)
}
const overlay = () => {
return (
<Menu onClick={({ item }) => onMathSelect(index, item.props['data-value'])}>
{MATH_ENTRIES.map(([key, { name, description, onProperty }]) => {
const disabled = onProperty && !areEventPropertiesNumericalAvailable
return (
<Menu.Item
key={`math-${key}`}
data-value={key}
data-attr={`math-${key}-${index}`}
disabled={disabled}
>
<Tooltip
title={
onProperty ? (
<>
{description}
<br />
{numericalNotice}
</>
) : (
description
)
}
data-attr={`math-${key}-${index}`}
placement="right"
>
{name}
</button>
</div>
</Tooltip>
)
})}
</Tooltip>
</Menu.Item>
)
})}
</Menu>
)
}
return (
<Dropdown overlay={overlay}>
<Button className="ant-btn-md" data-attr={`math-selector-${index}`}>
{MATHS[math || 'total']?.name} <DownOutlined />
</Button>
</Dropdown>
)
}
@ -277,35 +275,40 @@ function MathPropertySelector(props) {
({ value }) => value[0] !== '$' && value !== 'distinct_id' && value !== 'token'
)
const overlay = () => {
return (
<Menu onClick={({ item }) => props.onMathPropertySelect(props.index, item.props['data-value'])}>
{applicableProperties.map(({ value, label }) => {
return (
<Menu.Item
key={`math-property-${value}-${props.index}`}
data-attr={`math-property-${value}-${props.index}`}
data-value={value}
>
<Tooltip
title={
<>
Calculate {MATHS[props.math].name.toLowerCase()} from property{' '}
<code>{label}</code>. Note that only {props.name} occurences where{' '}
<code>{label}</code> is set and a number will be taken into account.
</>
}
placement="right"
>
{label}
</Tooltip>
</Menu.Item>
)
})}
</Menu>
)
}
return (
<Dropdown
title={props.mathProperty || 'Select property'}
titleEmpty="No applicable properties"
buttonClassName="btn btn-sm btn-light ml-2"
data-attr={`math-property-selector-${props.index}`}
>
{applicableProperties.map(({ value, label }) => (
<Tooltip
placement="right"
title={
<>
Calculate {MATHS[props.math].name.toLowerCase()} from property <code>{label}</code>. Note
that only {props.name} occurences where <code>{label}</code> is set and a number will be
taken into account.
</>
}
key={`math-property-${value}-${props.index}`}
>
<a
href="#"
className="dropdown-item"
onClick={() => props.onMathPropertySelect(props.index, value)}
data-attr={`math-property-${value}-${props.index}`}
>
{label}
</a>
</Tooltip>
))}
<Dropdown overlay={overlay}>
<Button data-attr={`math-property-selector-${props.index}`} style={{ marginTop: 8 }}>
{props.mathProperty || 'Select property'} <DownOutlined />
</Button>
</Dropdown>
)
}

View File

@ -1,5 +1,5 @@
import { Card } from 'antd'
import React, { Component } from 'react'
import { Card } from '../../lib/utils'
export class ActionSelectInfo extends Component {
infoDiv = React.createRef()
@ -23,7 +23,7 @@ export class ActionSelectInfo extends Component {
entity.steps.map((step, index) => (
<div key={step.id}>
<Card key={step.id} style={{ marginBottom: 0 }}>
<div className="card-body">
<div>
<strong>
{step.event && step.event[0] == '$'
? step.event[1].toUpperCase() + step.event.slice(2)

View File

@ -65,7 +65,7 @@ export function FunnelTab(): JSX.Element {
<Row justify="space-between">
<Row justify="start">
<Button
className="mr-1"
style={{ marginRight: 4 }}
type="primary"
htmlType="submit"
disabled={isStepsEmpty}

View File

@ -6,7 +6,7 @@ import { entityFilterLogic } from '../ActionFilter/entityFilterLogic'
import { DownOutlined } from '@ant-design/icons'
import { retentionTableLogic, dateOptions } from 'scenes/retention/retentionTableLogic'
import { DatePicker, Select } from 'antd'
import { Button, DatePicker, Select } from 'antd'
export function RetentionTab(): JSX.Element {
const node = useRef()
@ -27,18 +27,10 @@ export function RetentionTab(): JSX.Element {
return (
<div data-attr="retention-tab">
<h4 className="secondary">Target Event</h4>
<button
ref={node}
className="filter-action btn btn-sm btn-light"
type="button"
onClick={(): void => setOpen(!open)}
style={{
fontWeight: 500,
}}
>
<Button ref={node} data-attr="retention-action" onClick={(): void => setOpen(!open)}>
{startEntity?.name || 'Select action'}
<DownOutlined style={{ marginLeft: '3px', color: 'rgba(0, 0, 0, 0.25)' }} />
</button>
<DownOutlined className="text-muted" style={{ marginRight: '-6px' }} />
</Button>
{open && (
<ActionFilterDropdown
logic={entityLogic}
@ -53,7 +45,6 @@ export function RetentionTab(): JSX.Element {
<hr />
<h4 className="secondary">Filters</h4>
<PropertyFilters pageKey="insight-retention" />
<>
<hr />
<h4 className="secondary">Current Date</h4>
@ -62,7 +53,7 @@ export function RetentionTab(): JSX.Element {
showTime={filters.period === 'h'}
use12Hours
format={filters.period === 'h' ? 'YYYY-MM-DD, h a' : 'YYYY-MM-DD'}
className="mb-2"
className="mb-05"
value={selectedDate}
onChange={(date): void => setFilters({ selectedDate: date })}
allowClear={false}

View File

@ -4,7 +4,7 @@ import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters'
import { ActionFilter } from '../../ActionFilter/ActionFilter'
import { Tooltip, Row } from 'antd'
import { BreakdownFilter } from '../../BreakdownFilter'
import { CloseButton } from 'lib/utils'
import { CloseButton } from 'lib/components/CloseButton'
import { ShownAsFilter } from '../../ShownAsFilter'
import { InfoCircleOutlined } from '@ant-design/icons'
import { trendsLogic } from '../../trendsLogic'
@ -34,7 +34,7 @@ export function TrendTab(): JSX.Element {
placement="right"
title="Use breakdown to see the volume of events for each variation of that property. For example, breaking down by $current_url will give you the event volume for each url your users have visited."
>
<InfoCircleOutlined className="info" style={{ color: '#007bff' }} />
<InfoCircleOutlined className="info-indicator" />
</Tooltip>
</h4>
<Row>
@ -59,7 +59,7 @@ export function TrendTab(): JSX.Element {
performed an action on Monday and again on Friday, it would be shown
as "2 days".'
>
<InfoCircleOutlined className="info" style={{ color: '#007bff' }} />
<InfoCircleOutlined className="info-indicator" />
</Tooltip>
</h4>
<ShownAsFilter filters={filters} onChange={(shown_as): void => setFilters({ shown_as })} />

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react'
import { useActions, useMountedLogic, useValues } from 'kea'
import { Card, Loading } from 'lib/utils'
import { Loading } from 'lib/utils'
import { SaveToDashboard } from 'lib/components/SaveToDashboard/SaveToDashboard'
import { DateFilter } from 'lib/components/DateFilter'
import { IntervalFilter } from 'lib/components/IntervalFilter/IntervalFilter'
@ -10,9 +10,10 @@ import { ActionsPie } from './ActionsPie'
import { ActionsTable } from './ActionsTable'
import { ActionsLineGraph } from './ActionsLineGraph'
import { PeopleModal } from './PeopleModal'
import { PageHeader } from 'lib/components/PageHeader'
import { ChartFilter } from 'lib/components/ChartFilter'
import { Tabs, Row, Col, Button, Drawer, Tooltip } from 'antd'
import { Tabs, Row, Col, Button, Drawer, Tooltip, Card } from 'antd'
import {
ACTIONS_LINE_GRAPH_LINEAR,
ACTIONS_LINE_GRAPH_CUMULATIVE,
@ -47,6 +48,8 @@ import { InfoCircleOutlined } from '@ant-design/icons'
import { userLogic } from 'scenes/userLogic'
import { insightCommandLogic } from './insightCommandLogic'
import './Insights.scss'
const { TabPane } = Tabs
const displayMap = {
@ -124,7 +127,7 @@ function _Insights() {
return (
user?.team && (
<div className="actions-graph">
<h1 className="page-header">Insights</h1>
<PageHeader title="Insights" />
<Row justify="space-between" align="middle">
<Tabs
size="large"
@ -154,8 +157,8 @@ function _Insights() {
</Row>
<Row gutter={16}>
<Col xs={24} xl={7}>
<Card className="mb-3" style={{ overflow: 'visible' }}>
<div className="card-body px-4 mb-0">
<Card className="" style={{ overflow: 'visible' }}>
<div>
{/*
These are insight specific filters.
They each have insight specific logics
@ -182,14 +185,13 @@ function _Insights() {
placement="right"
title="These consist of funnels by you and the rest of the team"
>
<InfoCircleOutlined className="info" style={{ color: '#007bff' }} />
<InfoCircleOutlined className="info-indicator" />
</Tooltip>
</Row>
}
style={{ marginTop: 16 }}
>
<div className="card-body px-4 mb-0">
<SavedFunnels />
</div>
<SavedFunnels />
</Card>
)}
</Col>
@ -200,7 +202,7 @@ function _Insights() {
*/}
<Card
title={
<div className="float-right pt-1 pb-1">
<div className="float-right">
{showIntervalFilter[activeView] && (
<IntervalFilter filters={allFilters} view={activeView} />
)}
@ -237,8 +239,9 @@ function _Insights() {
/>
</div>
}
headStyle={{ backgroundColor: 'rgba(0,0,0,.03)' }}
>
<div className="card-body card-body-graph">
<div>
{
{
[`${ViewType.TRENDS}`]: <TrendInsight view={ViewType.TRENDS} />,
@ -327,11 +330,7 @@ function FunnelInsight() {
function FunnelPeople() {
const { stepsWithCount } = useValues(funnelLogic)
if (stepsWithCount && stepsWithCount.length > 0) {
return (
<div className="funnel">
<People />
</div>
)
return <People />
}
return <></>
}

View File

@ -1,11 +1,3 @@
.filter-action-only {
display: none;
}
.filter-action:hover {
.filter-action-only {
display: inline;
}
}
.graph-container {
position: absolute;
width: 100%;

View File

@ -9,6 +9,7 @@ import { toast } from 'react-toastify'
import { Annotations, annotationsLogic, AnnotationMarker } from 'lib/components/Annotations'
import { useEscapeKey } from 'lib/hooks/useEscapeKey'
import moment from 'moment'
import './Insights.scss'
//--Chart Style Options--//
// Chart.defaults.global.defaultFontFamily = "'PT Sans', sans-serif"

View File

@ -5,7 +5,7 @@ import { licenseLogic } from './logic'
import { useValues, useActions } from 'kea'
import { humanFriendlyDetailedTime } from 'lib/utils'
import { CodeSnippet } from 'scenes/ingestion/frameworks/CodeSnippet'
import { userLogic } from 'scenes/userLogic'
import { PageHeader } from 'lib/components/PageHeader'
const columns = [
{
@ -43,21 +43,20 @@ function _Licenses(): JSX.Element {
const [form] = Form.useForm()
const { licenses, licensesLoading, error } = useValues(licenseLogic)
const { createLicense } = useActions(licenseLogic)
const { user } = useValues(userLogic)
return (
<div>
<h1 className="page-header">Organization Licenses {user?.organization.name}</h1>
<i>
<p>
Here you can add and manage your PostHog enterprise licenses.
<br />
Activate a license key and enterprise functionality will be enabled immediately.
</p>
<p>
Contact <a href="mailto:sales@posthog.com">sales@posthog.com</a> to buy a license.
</p>
</i>
<PageHeader
title="Licenses"
caption={
<>
Here you can add and manage your PostHog enterprise licenses. When you activate a license key,
enterprise functionality will be enabled immediately. Contact{' '}
<a href="mailto:sales@posthog.com">sales@posthog.com</a> to buy a license or if you have any
issues with a license.
</>
}
/>
{error && (
<Alert
message={

View File

@ -2,9 +2,10 @@ import './index.scss'
import React from 'react'
import { hot } from 'react-hot-loader/root'
import { Alert, Table, Tag } from 'antd'
import { Alert, Table, Tag, Card } from 'antd'
import { systemStatusLogic } from './systemStatusLogic'
import { useValues } from 'kea'
import { PageHeader } from 'lib/components/PageHeader'
const columns = [
{
@ -29,26 +30,27 @@ function _Status(): JSX.Element {
const { systemStatus, systemStatusLoading, error } = useValues(systemStatusLogic)
return (
<div className="system-status-scene">
<h1 className="page-header">System Status</h1>
<p style={{ maxWidth: 600 }}>
<i>Here you can find all the critical runtime details about your PostHog installation.</i>
</p>
<PageHeader
title="System Status"
caption="Here you can find all the critical runtime details about your PostHog installation."
/>
{error && (
<Alert
message={error || <span>Something went wrong. Please try again or contact us.</span>}
type="error"
/>
)}
<br />
<Table
className="system-status-table"
size="small"
rowKey={(item): string => item.metric}
pagination={{ pageSize: 99999, hideOnSinglePage: true }}
dataSource={systemStatus}
columns={columns}
loading={systemStatusLoading}
/>
<Card>
<Table
className="system-status-table"
size="small"
rowKey={(item): string => item.metric}
pagination={{ pageSize: 99999, hideOnSinglePage: true }}
dataSource={systemStatus}
columns={columns}
loading={systemStatusLoading}
/>
</Card>
</div>
)
}

View File

@ -31,7 +31,7 @@ export function ChangePassword(): JSX.Element {
<Form
onFinish={submit}
labelCol={{
span: 8,
span: 4,
}}
wrapperCol={{
span: 16,

View File

@ -1,6 +1,6 @@
import React from 'react'
import { useValues } from 'kea'
import { Divider } from 'antd'
import { Divider, Card } from 'antd'
import { useAnchor } from 'lib/hooks/useAnchor'
import { router } from 'kea-router'
import { hot } from 'react-hot-loader/root'
@ -8,30 +8,36 @@ import { UpdateEmailPreferences } from './UpdateEmailPreferences'
import { ChangePassword } from './ChangePassword'
import { PersonalAPIKeys } from 'lib/components/PersonalAPIKeys'
import { OptOutCapture } from './OptOutCapture'
import { userLogic } from 'scenes/userLogic'
import { PageHeader } from 'lib/components/PageHeader'
export const MySettings = hot(_MySettings)
function _MySettings(): JSX.Element {
const { location } = useValues(router)
const { user } = useValues(userLogic)
useAnchor(location.hash)
return (
<div>
<h1 className="page-header">My Settings {user?.name}</h1>
<Divider />
<h2 id="password">Change Password</h2>
<ChangePassword />
<Divider />
<h2 id="personal-api-keys">Personal API Keys</h2>
<PersonalAPIKeys />
<Divider />
<h2>Security and Feature Updates</h2>
<UpdateEmailPreferences />
<Divider />
<h2 id="optout">Anonymize Data Collection</h2>
<OptOutCapture />
<div style={{ marginBottom: 128 }}>
<PageHeader title="My Settings" />
<Card>
<h2 id="password" className="subtitle">
Change Password
</h2>
<ChangePassword />
<Divider />
<h2 id="personal-api-keys" className="subtitle">
Personal API Keys
</h2>
<PersonalAPIKeys />
<Divider />
<h2 className="subtitle">Security and Feature Updates</h2>
<UpdateEmailPreferences />
<Divider />
<h2 id="optout" className="subtitle">
Anonymize Data Collection
</h2>
<OptOutCapture />
</Card>
</div>
)
}

View File

@ -23,15 +23,17 @@ export function CreateOrgInviteModalWithButton(): JSX.Element {
return (
<>
<Button
type="primary"
data-attr="invite-teammate-button"
onClick={() => {
setIsVisible(true)
}}
>
+ Invite Teammate
</Button>
<div className="mb text-right">
<Button
type="primary"
data-attr="invite-teammate-button"
onClick={() => {
setIsVisible(true)
}}
>
+ Invite Teammate
</Button>
</div>
<Modal
title="Inviting Teammate"
okText="Create Invite Link"

View File

@ -8,6 +8,7 @@ import { humanFriendlyDetailedTime } from 'lib/utils'
import { hot } from 'react-hot-loader/root'
import { UserType } from '~/types'
import { CopyToClipboardInline } from 'lib/components/CopyToClipboard'
import { PageHeader } from 'lib/components/PageHeader'
export const Invites = hot(_Invites)
function _Invites({ user }: { user: UserType }): JSX.Element {
@ -87,12 +88,7 @@ function _Invites({ user }: { user: UserType }): JSX.Element {
return (
<>
<h1 className="page-header">Organization Invites {user.organization.name}</h1>
<div style={{ maxWidth: 672 }}>
<i>
<p>Create, send out, and delete organization invites.</p>
</i>
</div>
<PageHeader title="Organization Invites" caption="Create, send out, and delete organization invites." />
<CreateOrgInviteModalWithButton />
<Table
dataSource={invites}

View File

@ -9,6 +9,7 @@ import { CreateOrgInviteModalWithButton } from '../Invites/CreateOrgInviteModal'
import { OrganizationMembershipLevel, organizationMembershipLevelToName } from 'lib/constants'
import { UserType } from '~/types'
import { ColumnsType } from 'antd/lib/table'
import { PageHeader } from 'lib/components/PageHeader'
interface MembersProps {
user: UserType
@ -88,12 +89,10 @@ function _Members({ user }: MembersProps): JSX.Element {
return (
<>
<h1 className="page-header">Organization Members {user?.organization.name}</h1>
<div style={{ maxWidth: 672 }}>
<i>
<p>View and manage all organization members here. Build an even better product together.</p>
</i>
</div>
<PageHeader
title="Organization Members"
caption="View and manage all organization members here. Build an even better product together."
/>
<CreateOrgInviteModalWithButton />
<Table
dataSource={members}

View File

@ -5,6 +5,7 @@ import { useAnchor } from 'lib/hooks/useAnchor'
import { router } from 'kea-router'
import { hot } from 'react-hot-loader/root'
import { userLogic } from 'scenes/userLogic'
import { PageHeader } from 'lib/components/PageHeader'
export const Setup = hot(_Setup)
function _Setup() {
@ -15,7 +16,7 @@ function _Setup() {
return (
<div>
<h1 className="page-header">Organization Settings {user.organization.name}</h1>
<PageHeader title={`Organization Settings - ${user.organization.name}`} />
</div>
)
}

View File

@ -156,7 +156,7 @@ export function Paths() {
.selectAll('g')
.data(links)
.join('g')
.attr('stroke', () => 'var(--blue)')
.attr('stroke', () => 'var(--primary)')
.attr('opacity', 0.3)
.style('mix-blend-mode', 'multiply')

View File

@ -1,6 +1,6 @@
import React from 'react'
import { useActions, useValues } from 'kea'
import { Divider } from 'antd'
import { Card, Divider } from 'antd'
import { IPCapture } from './IPCapture'
import { JSSnippet } from 'lib/components/JSSnippet'
import { OptInSessionRecording } from './OptInSessionRecording'
@ -16,6 +16,7 @@ import { ToolbarSettings } from './ToolbarSettings'
import { CodeSnippet } from 'scenes/ingestion/frameworks/CodeSnippet'
import { teamLogic } from 'scenes/teamLogic'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { PageHeader } from 'lib/components/PageHeader'
export const Setup = hot(_Setup)
function _Setup() {
@ -28,81 +29,95 @@ function _Setup() {
useAnchor(location.hash)
return (
<div>
<h1 className="page-header">Project Settings {user.team.name}</h1>
<Divider />
<h2 id="snippet">Website Event Autocapture</h2>
To integrate PostHog into your webiste and get event autocapture with no additional work, include the
following snippet in your&nbsp;website's&nbsp;HTML. Ideally, put it just above the&nbsp;
<code>{'<head>'}</code>&nbsp;tag.
<br />
For more guidance, including on identying users,{' '}
<a href="https://posthog.com/docs/integrations/js-integration">see PostHog Docs</a>.
<JSSnippet />
<Divider />
<h2 id="custom-events">Send Custom Events</h2>
To send custom events <a href="https://posthog.com/docs/integrations">visit PostHog Docs</a> and integrate
the library for the specific language or platform you're using. We support Python, Ruby, Node, Go, PHP, iOS,
Android, and more.
<Divider />
<h2 id="project-api-key">Project API Key</h2>
You can use this write-only key in any one of{' '}
<a href="https://posthog.com/docs/integrations">our libraries</a>.
<CodeSnippet
actions={[
{
Icon: ReloadOutlined,
popconfirmProps: {
title: 'Reset project API key, invalidating the current one?',
okText: 'Reset Key',
okType: 'danger',
icon: <ReloadOutlined style={{ color: red.primary }} />,
placement: 'left',
<div style={{ marginBottom: 128 }}>
<PageHeader title={`Project Settings ${user.team.name}`} />
<Card>
<h2 id="snippet" className="subtitle">
Website Event Autocapture
</h2>
To integrate PostHog into your webiste and get event autocapture with no additional work, include the
following snippet in your&nbsp;website's&nbsp;HTML. Ideally, put it just above the&nbsp;
<code>{'<head>'}</code>&nbsp;tag.
<br />
For more guidance, including on identying users,{' '}
<a href="https://posthog.com/docs/integrations/js-integration">see PostHog Docs</a>.
<JSSnippet />
<Divider />
<h2 id="custom-events" className="subtitle">
Send Custom Events
</h2>
To send custom events <a href="https://posthog.com/docs/integrations">visit PostHog Docs</a> and
integrate the library for the specific language or platform you're using. We support Python, Ruby, Node,
Go, PHP, iOS, Android, and more.
<Divider />
<h2 id="project-api-key" className="subtitle">
Project API Key
</h2>
You can use this write-only key in any one of{' '}
<a href="https://posthog.com/docs/integrations">our libraries</a>.
<CodeSnippet
actions={[
{
Icon: ReloadOutlined,
popconfirmProps: {
title: 'Reset project API key, invalidating the current one?',
okText: 'Reset Key',
okType: 'danger',
icon: <ReloadOutlined style={{ color: red.primary }} />,
placement: 'left',
},
callback: resetToken,
},
callback: resetToken,
},
]}
>
{currentTeam?.api_token}
</CodeSnippet>
Write-only means it can only create new events. It can't read events or any of your other data stored with
PostHog, so it's safe to use in public apps.
<Divider />
<h2 id="urls">Permitted Domains/URLs</h2>
<p>
These are the domains and URLs where the Toolbar will automatically open if you're logged in. It's also
where you'll be able to create Actions.
</p>
<EditAppUrls />
<Divider />
<h2 id="webhook">Slack / Microsoft Teams Integration</h2>
<WebhookIntegration />
<h2 id="datacapture">Data Capture Configuration</h2>
<IPCapture />
<Divider />
<h2>PostHog Toolbar</h2>
<ToolbarSettings />
<Divider />
{(!user.is_multi_tenancy || featureFlags['session-recording-player']) && (
<>
<h2 id="sessionrecording">
Session recording <span style={{ fontSize: 16, color: '#F7A501' }}>BETA</span>
</h2>
<p>
Watch sessions replays to see how users interact with your app and find out what can be
improved.
</p>
<OptInSessionRecording />
<p>
This is a new feature of posthog. Please{' '}
<a href="https://github.com/PostHog/posthog/issues/new/choose" target="_blank">
share feedback
</a>{' '}
with us!
</p>
<Divider />
</>
)}
]}
>
{currentTeam?.api_token}
</CodeSnippet>
Write-only means it can only create new events. It can't read events or any of your other data stored
with PostHog, so it's safe to use in public apps.
<Divider />
<h2 id="urls" className="subtitle">
Permitted Domains/URLs
</h2>
<p>
These are the domains and URLs where the Toolbar will automatically open if you're logged in. It's
also where you'll be able to create Actions.
</p>
<EditAppUrls />
<Divider />
<h2 id="webhook" className="subtitle">
Slack / Microsoft Teams Integration
</h2>
<WebhookIntegration />
<Divider />
<h2 id="datacapture" className="subtitle">
Data Capture Configuration
</h2>
<IPCapture />
<Divider />
<h2 className="subtitle">PostHog Toolbar</h2>
<ToolbarSettings />
<Divider />
{(!user.is_multi_tenancy || featureFlags['session-recording-player']) && (
<>
<h2 id="sessionrecording" className="subtitle">
Session recording <span style={{ fontSize: 16, color: 'var(--warning)' }}>BETA</span>
</h2>
<p>
Watch sessions replays to see how users interact with your app and find out what can be
improved.
</p>
<OptInSessionRecording />
<br />
<p>
This is a new feature of PostHog. Please{' '}
<a href="https://github.com/PostHog/posthog/issues/new/choose" target="_blank">
share feedback
</a>{' '}
with us!
</p>
</>
)}
</Card>
</div>
)
}

View File

@ -4,6 +4,7 @@ import { Table, Modal, Button, Spin } from 'antd'
import { percentage } from 'lib/utils'
import { Link } from 'lib/components/Link'
import { retentionTableLogic } from './retentionTableLogic'
import './RetentionTable.scss'
import moment from 'moment'
export function RetentionTable() {
@ -96,7 +97,7 @@ export function RetentionTable() {
<span>No users during this period.</span>
) : (
<div>
<table className="table table-bordered table-fixed">
<table className="table-bordered full-width">
<tbody>
<tr>
<th />

View File

@ -1,3 +1,5 @@
@import '~/vars';
.retention-table {
.ant-table-tbody .ant-table-cell {
padding: 0px !important;
@ -9,8 +11,9 @@
}
.retention-success {
background-color: lighten($blue, 20%);
border-radius: $radius;
background-color: lighten($primary, 20%);
}
.retention-dropped {
background-color: $gray-200;
background-color: $bg_mid;
}

View File

@ -15,7 +15,7 @@ export const scenes = {
events: () => import(/* webpackChunkName: 'events' */ './events/Events'),
sessions: () => import(/* webpackChunkName: 'sessions' */ './sessions/Sessions'),
person: () => import(/* webpackChunkName: 'person' */ './users/Person'),
people: () => import(/* webpackChunkName: 'people' */ './users/People'),
persons: () => import(/* webpackChunkName: 'persons' */ './users/People'),
actions: () => import(/* webpackChunkName: 'actions' */ './actions/Actions'),
action: () => import(/* webpackChunkName: 'action' */ './actions/Action'),
liveActions: () => import(/* webpackChunkName: 'liveActions' */ './actions/LiveActions'),
@ -35,7 +35,7 @@ export const scenes = {
plugins: () => import(/* webpackChunkName: 'plugins' */ './plugins/Plugins'),
}
/* List of routes that do not require authentication (N.B. add to posthog.urls too) */
/* List of routes that do not require authentication (N.B. add to posthog/urls.py too) */
export const unauthenticatedRoutes = ['preflightCheck', 'signup']
export const redirects = {
@ -55,9 +55,9 @@ export const routes = {
'/sessions': 'sessions',
'/person_by_id/:id': 'person',
'/person/*': 'person',
'/people/persons': 'people',
'/people/new_cohort': 'people',
'/people/cohorts': 'cohorts',
'/persons': 'persons',
'/cohorts/new': 'persons',
'/cohorts': 'cohorts',
'/feature_flags': 'featureFlags',
'/annotations': 'annotations',
'/project/settings': 'projectSettings',

View File

@ -15,6 +15,7 @@ import rrwebBlockClass from 'lib/utils/rrwebBlockClass'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import SessionsPlayerDrawer from 'scenes/sessions/SessionsPlayerDrawer'
import { userLogic } from 'scenes/userLogic'
import { PageHeader } from 'lib/components/PageHeader'
interface SessionsTableProps {
personIds?: string[]
@ -111,8 +112,8 @@ export function SessionsTable({ personIds, isPersonPage = false }: SessionsTable
return (
<div className="events" data-attr="events-table">
{!isPersonPage && <h1 className="page-header">Sessions By Day</h1>}
<Space className="mb-2">
{!isPersonPage && <PageHeader title="Sessions By Day" />}
<Space className="mb-05">
<Button onClick={previousDay} icon={<CaretLeftOutlined />} />
<DatePicker
value={selectedDate}

View File

@ -1,9 +1,9 @@
import React from 'react'
import { Card, CloseButton, fromParams } from 'lib/utils'
import { fromParams } from 'lib/utils'
import { CloseButton } from 'lib/components/CloseButton'
import { CohortGroup } from './CohortGroup'
import { cohortLogic } from './cohortLogic'
import { Button } from 'antd'
import { Button, Card, Input } from 'antd'
import { useValues, useActions } from 'kea'
const isSubmitDisabled = (cohorts) => {
@ -16,90 +16,84 @@ export function Cohort({ onChange }) {
const { personProperties, cohort } = useValues(cohortLogic({ onChange, id: fromParams()['cohort'] }))
if (!cohort) return null
return cohort.groups.length === 0 ? (
<Button
style={{ marginBottom: '1rem', marginRight: 12 }}
onClick={() => setCohort({ groups: [{}] })}
type="primary"
data-attr="create-cohort"
>
+ New Cohort
</Button>
) : (
<div style={{ maxWidth: 750 }}>
<Card
title={
<span>
<CloseButton
className="float-right"
onClick={() => {
setCohort({ id: false, groups: [] })
onChange()
}}
/>
{cohort.name || 'New Cohort'}
</span>
}
>
<form
className="card-body"
onSubmit={(e) => {
e.preventDefault()
saveCohort(cohort)
}}
>
<input
style={{ marginBottom: '1rem' }}
required
className="form-control"
autoFocus
placeholder="Cohort name..."
value={cohort.name}
onChange={(e) => setCohort({ ...cohort, name: e.target.value })}
/>
{cohort.groups
.map((group, index) => (
<CohortGroup
key={index}
group={group}
properties={personProperties}
index={index}
onRemove={() => {
cohort.groups.splice(index, 1)
setCohort({ ...cohort })
}}
onChange={(group) => {
cohort.groups[index] = group
setCohort({ ...cohort })
return (
cohort.groups.length > 0 && (
<div style={{ maxWidth: 750 }} className="mb">
<Card
title={
<span>
<CloseButton
style={{ float: 'right' }}
onClick={() => {
setCohort({ id: false, groups: [] })
onChange()
}}
/>
))
.reduce((prev, curr, index) => [
prev,
<div key={index} className="secondary" style={{ textAlign: 'center', margin: 8 }}>
{' '}
OR{' '}
</div>,
curr,
])}
<Button
type="primary"
htmlType="submit"
disabled={isSubmitDisabled(cohort)}
data-attr="save-cohort"
style={{ marginTop: '1rem' }}
{cohort.name || 'New Cohort'}
</span>
}
>
<form
onSubmit={(e) => {
e.preventDefault()
saveCohort(cohort)
}}
>
Save cohort
</Button>
<Button
style={{ marginTop: '1rem', marginLeft: 12 }}
onClick={() => setCohort({ ...cohort, groups: [...cohort.groups, {}] })}
>
New group
</Button>
</form>
</Card>
</div>
<div className="mb">
<Input
required
autoFocus
placeholder="Cohort name..."
value={cohort.name}
data-attr="cohort-name"
onChange={(e) => setCohort({ ...cohort, name: e.target.value })}
/>
</div>
{cohort.groups
.map((group, index) => (
<CohortGroup
key={index}
group={group}
properties={personProperties}
index={index}
onRemove={() => {
cohort.groups.splice(index, 1)
setCohort({ ...cohort })
}}
onChange={(group) => {
cohort.groups[index] = group
setCohort({ ...cohort })
}}
/>
))
.reduce((prev, curr, index) => [
prev,
<div key={index} className="secondary" style={{ textAlign: 'center', margin: 8 }}>
{' '}
OR{' '}
</div>,
curr,
])}
<div className="mt">
<Button
type="primary"
htmlType="submit"
disabled={isSubmitDisabled(cohort)}
data-attr="save-cohort"
style={{ marginTop: '1rem' }}
>
Save cohort
</Button>
<Button
style={{ marginTop: '1rem', marginLeft: 12 }}
onClick={() => setCohort({ ...cohort, groups: [...cohort.groups, {}] })}
>
New group
</Button>
</div>
</form>
</Card>
</div>
)
)
}

View File

@ -1,126 +1,111 @@
import React, { useState } from 'react'
import { Card, CloseButton } from '../../lib/utils'
import { CloseButton } from 'lib/components/CloseButton'
import { PropertyFilters } from '../../lib/components/PropertyFilters/PropertyFilters'
import { Select } from 'antd'
import { Select, Card, Radio } from 'antd'
import { actionsModel } from '~/models/actionsModel'
import { useValues } from 'kea'
function DayChoice({ days, name, group, onChange }) {
return (
<button
onClick={() =>
onChange({
action_id: group.action_id,
days,
})
}
type="button"
className={'btn btn-sm ' + (group.days == days ? 'btn-secondary' : 'btn-light')}
>
{name}
</button>
)
}
export function CohortGroup({ onChange, onRemove, group, index }) {
const { actionsGrouped } = useValues(actionsModel)
const [selected, setSelected] = useState((group.action_id && 'action') || (group.properties && 'property'))
return (
<Card title={false} style={{ margin: 0 }}>
<div className="card-body">
{index > 0 && <CloseButton className="float-right" onClick={onRemove} />}
<div style={{ height: 32 }}>
User has
{selected == 'action' && ' done '}
<div className="btn-group" style={{ margin: '0 8px' }}>
<button
onClick={() => {
setSelected('action')
onChange({})
}}
type="button"
data-attr="cohort-group-action"
className={'btn btn-sm ' + (selected == 'action' ? 'btn-secondary' : 'btn-light')}
>
<Card title={false} style={{ margin: 0 }} className="card-elevated">
{index > 0 && <CloseButton className="float-right" onClick={onRemove} />}
<div style={{ height: 32 }}>
<span style={{ paddingRight: selected !== 'action' ? 40 : 0 }}>User has</span>
{selected === 'action' && ' done '}
<span style={{ paddingRight: 8 }}>
<Radio.Group
buttonStyle="solid"
onChange={(e) => {
setSelected(e.target.value)
onChange({})
}}
size="small"
value={selected}
>
<Radio.Button value="action" data-attr="cohort-group-action">
action
</button>
<button
onClick={() => {
setSelected('property')
onChange({})
}}
type="button"
data-attr="cohort-group-property"
className={'btn btn-sm ' + (selected == 'property' ? 'btn-secondary' : 'btn-light')}
>
</Radio.Button>
<Radio.Button value="property" data-attr="cohort-group-property">
property
</button>
</div>
{selected == 'action' && (
<span>
in the last
<div className="btn-group" style={{ margin: '0 8px' }}>
<DayChoice days={1} name="day" group={group} onChange={onChange} />
<DayChoice days={7} name="7 days" group={group} onChange={onChange} />
<DayChoice days={30} name="month" group={group} onChange={onChange} />
</div>
</span>
)}
</div>
{selected && (
<div style={{ marginLeft: '2rem', minHeight: 38 }}>
{selected == 'property' && (
<PropertyFilters
endpoint="person"
pageKey={'cohort_' + index}
className=" "
onChange={(properties) => {
onChange(
properties.length
? {
properties: properties,
days: group.days,
}
: {}
)
}}
propertyFilters={group.properties || {}}
style={{ margin: '1rem 0 0' }}
/>
)}
{selected == 'action' && (
<div style={{ marginTop: '1rem', width: 350 }}>
<Select
showSearch
placeholder="Select action..."
style={{ width: '100%' }}
onChange={(value) => onChange({ action_id: value })}
filterOption={(input, option) =>
option.children &&
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
value={group.action_id}
>
{actionsGrouped.map((typeGroup) => {
if (typeGroup['options'].length > 0) {
return (
<Select.OptGroup key={typeGroup['label']} label={typeGroup['label']}>
{typeGroup['options'].map((item) => (
<Select.Option key={item.value} value={item.value}>
{item.label}
</Select.Option>
))}
</Select.OptGroup>
)
}
})}
</Select>
</div>
)}
</div>
</Radio.Button>
</Radio.Group>
</span>
{selected == 'action' && (
<span>
in the last
<Radio.Group
buttonStyle="solid"
onChange={(e) =>
onChange({
action_id: group.action_id,
days: e.target.value,
})
}
size="small"
value={group.days}
style={{ paddingLeft: 8 }}
>
<Radio.Button value="1">day</Radio.Button>
<Radio.Button value="7">7 days</Radio.Button>
<Radio.Button value="30">month</Radio.Button>
</Radio.Group>
</span>
)}
</div>
{selected && (
<div style={{ minHeight: 38 }}>
{selected == 'property' && (
<PropertyFilters
endpoint="person"
pageKey={'cohort_' + index}
className=" "
onChange={(properties) => {
onChange(
properties.length
? {
properties: properties,
days: group.days,
}
: {}
)
}}
propertyFilters={group.properties || {}}
style={{ margin: '1rem 0 0' }}
/>
)}
{selected == 'action' && (
<div style={{ marginTop: '1rem', width: 350 }}>
<Select
showSearch
placeholder="Select action..."
style={{ width: '100%' }}
onChange={(value) => onChange({ action_id: value })}
filterOption={(input, option) =>
option.children && option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
value={group.action_id}
>
{actionsGrouped.map((typeGroup) => {
if (typeGroup['options'].length > 0) {
return (
<Select.OptGroup key={typeGroup['label']} label={typeGroup['label']}>
{typeGroup['options'].map((item) => (
<Select.Option key={item.value} value={item.value}>
{item.label}
</Select.Option>
))}
</Select.OptGroup>
)
}
})}
</Select>
</div>
)}
</div>
)}
</Card>
)
}

View File

@ -9,6 +9,8 @@ import { cohortsModel } from '../../models/cohortsModel'
import { useValues, useActions } from 'kea'
import { hot } from 'react-hot-loader/root'
import rrwebBlockClass from 'lib/utils/rrwebBlockClass'
import { PageHeader } from 'lib/components/PageHeader'
import { PlusOutlined } from '@ant-design/icons'
export const Cohorts = hot(_Cohorts)
function _Cohorts() {
@ -21,7 +23,7 @@ function _Cohorts() {
key: 'name',
render: function RenderName(_, cohort) {
return (
<Link className={rrwebBlockClass} to={'/people/persons?cohort=' + cohort.id}>
<Link className={rrwebBlockClass} to={'/persons?cohort=' + cohort.id}>
{cohort.name}
</Link>
)
@ -91,20 +93,26 @@ function _Cohorts() {
return (
<div>
<h1 className="page-header">Cohorts</h1>
<LinkButton to={'/people/new_cohort'} type="primary" data-attr="create-cohort">
+ New Cohort
</LinkButton>
<br />
<br />
<Table
size="small"
columns={columns}
loading={!cohorts && cohortsLoading}
rowKey={(cohort) => cohort.id}
pagination={{ pageSize: 100, hideOnSinglePage: true }}
dataSource={cohorts}
<PageHeader
title="Cohorts"
caption="Create lists of users who have something in common to use in analytics or feature flags."
/>
<div>
<div className="mb text-right">
<LinkButton to={'/cohorts/new'} type="primary" data-attr="create-cohort" icon={<PlusOutlined />}>
New Cohort
</LinkButton>
</div>
<Table
size="small"
columns={columns}
loading={!cohorts && cohortsLoading}
rowKey={(cohort) => cohort.id}
pagination={{ pageSize: 100, hideOnSinglePage: true }}
dataSource={cohorts}
/>
</div>
</div>
)
}

View File

@ -4,10 +4,10 @@ import { router } from 'kea-router'
import api from 'lib/api'
import { Cohort } from './Cohort'
import { PeopleTable } from './PeopleTable'
import { Button, Tabs } from 'antd'
import { Button, Tabs, Input } from 'antd'
import { ExportOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons'
import { hot } from 'react-hot-loader/root'
import { PageHeader } from 'lib/components/PageHeader'
const { TabPane } = Tabs
const ALLOWED_CATEGORIES = ['all', 'identified', 'anonymous']
@ -64,7 +64,7 @@ function _People() {
}, [cohortId])
useEffect(() => {
if (!ALLOWED_CATEGORIES.includes(categoryRaw)) push('/people/persons', { category, cohort: cohortId })
if (!ALLOWED_CATEGORIES.includes(categoryRaw)) push('/persons', { category, cohort: cohortId })
}, [categoryRaw])
const exampleEmail =
@ -72,10 +72,10 @@ function _People() {
return (
<div>
<h1 className="page-header">Persons</h1>
<PageHeader title="Persons" />
<Cohort
onChange={(cohortId) => {
push('/people/persons', { category, cohort: cohortId })
push('/persons', { category, cohort: cohortId })
}}
/>
<Button
@ -86,23 +86,22 @@ function _People() {
>
Export
</Button>
<input
className="form-control"
name="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => e.keyCode === 13 && fetchPeople()}
placeholder={people && 'Try ' + exampleEmail + ' or has:email'}
style={{ maxWidth: 400 }}
/>
<br />
<div className="mb">
<Input
data-attr="persons-search"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && fetchPeople()}
placeholder={people && 'Try ' + exampleEmail + ' or has:email'}
style={{ maxWidth: 400 }}
/>
</div>
<Tabs
defaultActiveKey={category}
onChange={(category) => {
push('/people/persons', { category, cohort: cohortId })
push('/persons', { category, cohort: cohortId })
fetchPeople(undefined, undefined, category)
}}
type="card"
>
<TabPane tab={<span data-attr="people-types-tab">All</span>} key="all" data-attr="people-types-tab" />
<TabPane
@ -116,23 +115,26 @@ function _People() {
data-attr="people-types-tab"
/>
</Tabs>
<PeopleTable people={people} loading={isLoading} actions={true} onChange={() => fetchPeople()} />
<div style={{ margin: '3rem auto 10rem', width: 200 }}>
<Button
type="link"
disabled={!tabHasPagination('previous')}
onClick={() => fetchPeople(pagination[category].previous, true)}
>
<LeftOutlined style={{ verticalAlign: 'initial' }} /> Previous
</Button>
<Button
type="link"
disabled={!tabHasPagination('next')}
onClick={() => fetchPeople(pagination[category].next, true)}
>
Next <RightOutlined style={{ verticalAlign: 'initial' }} />
</Button>
<div>
<PeopleTable people={people} loading={isLoading} actions={true} onChange={() => fetchPeople()} />
<div style={{ margin: '3rem auto 10rem', width: 200 }}>
<Button
type="link"
disabled={!tabHasPagination('previous')}
onClick={() => fetchPeople(pagination[category].previous, true)}
>
<LeftOutlined style={{ verticalAlign: 'initial' }} /> Previous
</Button>
<Button
type="link"
disabled={!tabHasPagination('next')}
onClick={() => fetchPeople(pagination[category].next, true)}
>
Next <RightOutlined style={{ verticalAlign: 'initial' }} />
</Button>
</div>
</div>
</div>
)

View File

@ -12,6 +12,7 @@ import { hot } from 'react-hot-loader/root'
import { SessionsTable } from '../sessions/SessionsTable'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { userLogic } from 'scenes/userLogic'
import { PageHeader } from 'lib/components/PageHeader'
const { TabPane } = Tabs
@ -95,7 +96,7 @@ function _Person({ _: distinctId, id }) {
<Button
className="float-right"
danger
onClick={() => deletePersonData(person, () => history.push('/people/persons'))}
onClick={() => deletePersonData(person, () => history.push('/persons'))}
>
{isScreenSmall ? <DeleteOutlined /> : 'Delete all data on this person'}
</Button>
@ -107,10 +108,7 @@ function _Person({ _: distinctId, id }) {
>
Save updated data
</Button>
<h1 className="page-header">
{'name' in person.properties ? person.properties.name.first : person.name}{' '}
{person.properties.name ? person.properties.name.last : ''}
</h1>
<PageHeader title={`Person ${person.properties.name?.first || person.name || person.properties.email}`} />
<div style={{ maxWidth: 750 }}>
<PersonTable
properties={{

View File

@ -97,7 +97,7 @@ export const cohortLogic = kea({
const cohort = await api.get('api/cohort/' + props.id)
return actions.setCohort(cohort)
}
actions.setCohort({ groups: router.values.location.pathname.indexOf('new_cohort') > -1 ? [{}] : [] })
actions.setCohort({ groups: router.values.location.pathname.indexOf('cohorts/new') > -1 ? [{}] : [] })
},
beforeUnmount: () => {
clearTimeout(values.pollTimeout)

View File

@ -1,109 +1,9 @@
/*! Font generated by flaticon.com.
Under CC: Gregor Cresnar*/
@import 'node_modules/bootstrap/scss/_functions';
@import 'node_modules/bootstrap/scss/_variables';
@import 'node_modules/bootstrap/scss/_mixins';
$blue: #007bff;
$indigo: #6610f2;
$purple: #6f42c1;
$pink: #e83e8c;
$red: #dc3545;
$orange: #fd7e14;
$yellow: #ffc107;
$green: #28a745;
$teal: #20c997;
$cyan: #17a2b8;
$badge-font-weight: 400;
$body-bg: #fffefc;
$body-color: #37352f;
:root {
--gray-background: hsla(210, 6%, 95%, 1);
// make antd sidebar just a tad bit darker
.bg-dark {
background-color: hsla(210, 10%, 19%, 1) !important;
}
}
// bootstrap components
@import 'node_modules/bootstrap/scss/_root';
@import 'node_modules/bootstrap/scss/utilities';
@import 'node_modules/bootstrap/scss/_reboot';
@import 'node_modules/bootstrap/scss/_buttons';
@import 'node_modules/bootstrap/scss/_button-group';
@import 'node_modules/bootstrap/scss/_input-group';
@import 'node_modules/bootstrap/scss/_tables';
@import 'node_modules/bootstrap/scss/_forms';
@import 'node_modules/bootstrap/scss/_nav';
@import 'node_modules/bootstrap/scss/_badge';
@import 'node_modules/bootstrap/scss/_grid';
@import 'node_modules/bootstrap/scss/_list-group';
@import 'node_modules/bootstrap/scss/_close';
@import 'node_modules/bootstrap/scss/_card';
@import 'node_modules/bootstrap/scss/_modal';
@import 'node_modules/bootstrap/scss/_dropdown';
@import 'node_modules/bootstrap/scss/_alert';
// // funnel
@import 'node_modules/funnel-graph-js/src/scss/main.scss';
// icons
@import 'style/font/_flaticon';
// our components
@import 'style/events';
@import 'style/funnel';
@import 'style/toast';
@import 'style/trends';
@import 'style/send-events-overlay';
@import 'style/action-filter-dropdown';
@import 'style/retention-table';
.posthog-title {
font-weight: 500;
font-size: 20px;
color: #fff;
padding-left: 15px;
margin-top: 8px;
}
/* DEPRECATED in favor of global.scss */
.layout-container {
max-width: 100vw;
}
.cursor-pointer {
cursor: pointer;
}
.badge .close {
text-shadow: none;
margin: -6px 0 -8px 8px;
font-size: 1.3rem;
}
.card {
box-shadow: 0px 0px 13px rgba(0, 0, 0, 0.1);
border: 0;
margin-bottom: 3rem;
overflow: hidden;
.card-header {
font-weight: 500;
font-size: 15px;
border-bottom: 0;
}
}
.right-align {
float: right;
.fi {
font-size: 20px;
margin-right: 12px;
}
}
@keyframes rotation {
from {
transform: rotate(0deg);
@ -137,10 +37,6 @@ $body-color: #37352f;
}
}
.content {
padding: 0 45px;
}
.code-container {
position: relative;
.action-icon-container {
@ -182,6 +78,7 @@ pre.code {
text-overflow: ellipsis;
max-width: 0;
}
.paths {
height: 720px;
svg {
@ -189,6 +86,7 @@ pre.code {
width: 100%;
}
}
.secondary {
font-size: 13px;
letter-spacing: 1px;
@ -198,38 +96,11 @@ pre.code {
border: 0;
background: none;
}
hr {
border: 1px solid rgba(0, 0, 0, 0.1);
}
.close {
color: hsl(0, 0%, 80%);
}
h5.modal-title {
font-size: 120%;
}
h1.page-header {
font-size: 24px;
}
h1.title {
// New implementation of h1.page-hader
font-size: 32px;
font-weight: bold;
}
h2.subtitle {
font-size: 18px;
font-weight: bold;
}
.page-caption {
color: rgba(0, 0, 0, 0.6);
font-size: 14px;
}
button.no-style {
background: none;
border: none;
@ -240,58 +111,14 @@ button.no-style {
padding: 0.25rem;
}
.select-with-close {
display: inline-block;
.close {
margin-left: 10px;
}
}
.modal {
overflow: scroll;
}
.Toastify__toast--error {
background: white !important;
h1 {
font-size: 16px !important;
color: red;
}
p {
font-size: 14px;
margin-bottom: 8px;
}
p.info {
margin-bottom: 0;
}
p.error-message {
font-style: italic;
}
.Toastify__progress-bar {
background: #ffbcbc;
}
}
.Toastify__toast--success {
background: hsl(111 88% 33%);
color: white;
}
label.disabled {
color: #888;
}
.info {
color: $blue;
cursor: pointer;
margin-left: 5px;
}
.ant-layout.bg-dashboard {
background: var(--gray-background);
}
.ant-modal-mask {
z-index: 1050 !important;
}
@ -304,29 +131,11 @@ label.disabled {
.ant-select-dropdown {
z-index: 1065 !important;
}
.anticon {
// not sure why this doesn't just work..
vertical-align: 0.125em !important;
}
.ant-select-arrow .anticon {
vertical-align: top !important;
}
// less content padding in low resolutions
.ant-layout .ant-layout-content.pl-5.pr-5.pt-3.pb-5,
.ant-layout .content.py-3.layout-top-content {
@media (min-width: 480px) and (max-width: 639px) {
padding-left: 2rem !important;
padding-right: 2rem !important;
padding-bottom: 2rem !important;
}
@media (max-width: 479px) {
padding-left: 1rem !important;
padding-right: 1rem !important;
padding-bottom: 1rem !important;
}
}
.property-key-info {
display: flex;
align-items: center;
@ -346,72 +155,6 @@ label.disabled {
vertical-align: text-bottom;
}
.Toastify__progress-bar--default {
background: var(--success) !important;
}
#bottom-notice {
z-index: 1000000000;
display: flex;
flex-direction: row;
position: fixed;
width: 100%;
bottom: 0;
left: 0;
background: #000;
color: #fff;
font-size: 0.75rem;
line-height: 1.5rem;
&.warning div {
height: auto;
background: #f00;
}
&.tricolor {
div:nth-child(1) {
background: #1d4aff;
}
div:nth-child(2) {
background: #f54e00;
}
div:nth-child(3) {
background: #f9bd2b;
}
}
div {
flex-basis: 0;
flex-grow: 1;
height: 1.5rem;
text-align: center;
}
span {
display: none;
}
button {
border: none;
background: transparent;
width: 1.5rem;
height: 1.5rem;
padding: 0;
font-size: 1rem;
font-weight: bold;
}
@media screen and (min-width: 750px) {
font-size: 1rem;
line-height: 2rem;
div {
height: 2rem;
}
span {
display: inline;
}
button {
width: 2rem;
height: 2rem;
font-size: 1.25rem;
}
}
}
.button-border {
padding: 8px;
border-radius: 4px;
@ -436,49 +179,6 @@ label.disabled {
}
}
.ph-input-group {
padding-bottom: 16px;
color: #000000;
label {
font-weight: bold;
font-size: 16px;
}
.caption {
color: #666666;
font-size: 10px;
}
&.errored {
.caption {
color: $red;
}
.ant-input-password {
border-color: $red;
}
}
}
.btn-action {
background-color: #f0f0f0 !important;
}
.btn-sm {
line-height: 20px;
}
.btn-top {
font-weight: 500;
}
@media screen and (max-width: 480px) {
h1.title {
line-height: 1.1em;
font-size: 28px;
}
}
@media screen and (max-width: 480px) {
.signup-form {
h1.title {

Binary file not shown.

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

View File

@ -1,422 +0,0 @@
/*
Flaticon icon font: Flaticon
Creation date: 22/06/2016 15:44
*/
@font-face {
font-family: 'Flaticon';
src: url('./style/font/Flaticon.eot');
src: url('./style/font/Flaticon.eot?#iefix') format('embedded-opentype'),
url('./style/font/Flaticon.woff') format('woff'), url('./style/font/Flaticon.ttf') format('truetype'),
url('./style/font/Flaticon.svg#Flaticon') format('svg');
font-weight: normal;
font-style: normal;
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
@font-face {
font-family: 'Flaticon';
src: url('./style/font/Flaticon.svg#Flaticon') format('svg');
}
}
.fi:before {
display: inline-block;
font-family: 'Flaticon';
font-style: normal;
font-weight: normal;
font-variant: normal;
line-height: 1;
text-decoration: inherit;
text-rendering: optimizeLegibility;
text-transform: none;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-smoothing: antialiased;
}
.flaticon-add:before {
content: '\f100';
}
.flaticon-basket:before {
content: '\f101';
}
.flaticon-bookmark:before {
content: '\f102';
}
.flaticon-broken-link:before {
content: '\f103';
}
.flaticon-checked:before {
content: '\f104';
}
.flaticon-checked-1:before {
content: '\f105';
}
.flaticon-checked-2:before {
content: '\f106';
}
.flaticon-click:before {
content: '\f107';
}
.flaticon-close:before {
content: '\f108';
}
.flaticon-close-1:before {
content: '\f109';
}
.flaticon-coin:before {
content: '\f10a';
}
.flaticon-coins:before {
content: '\f10b';
}
.flaticon-compress:before {
content: '\f10c';
}
.flaticon-cursor:before {
content: '\f10d';
}
.flaticon-cursor-1:before {
content: '\f10e';
}
.flaticon-cursor-2:before {
content: '\f10f';
}
.flaticon-down-arrow:before {
content: '\f110';
}
.flaticon-down-arrow-1:before {
content: '\f111';
}
.flaticon-download:before {
content: '\f112';
}
.flaticon-download-1:before {
content: '\f113';
}
.flaticon-download-2:before {
content: '\f114';
}
.flaticon-download-3:before {
content: '\f115';
}
.flaticon-download-4:before {
content: '\f116';
}
.flaticon-edit:before {
content: '\f117';
}
.flaticon-expand:before {
content: '\f118';
}
.flaticon-export:before {
content: '\f119';
}
.flaticon-folder:before {
content: '\f11a';
}
.flaticon-forbidden:before {
content: '\f11b';
}
.flaticon-head:before {
content: '\f11c';
}
.flaticon-headphones:before {
content: '\f11d';
}
.flaticon-home:before {
content: '\f11e';
}
.flaticon-inbox:before {
content: '\f11f';
}
.flaticon-left-arrow:before {
content: '\f120';
}
.flaticon-left-arrow-1:before {
content: '\f121';
}
.flaticon-left-arrow-2:before {
content: '\f122';
}
.flaticon-levels:before {
content: '\f123';
}
.flaticon-levels-1:before {
content: '\f124';
}
.flaticon-link:before {
content: '\f125';
}
.flaticon-list:before {
content: '\f126';
}
.flaticon-login:before {
content: '\f127';
}
.flaticon-login-1:before {
content: '\f128';
}
.flaticon-mail:before {
content: '\f129';
}
.flaticon-move:before {
content: '\f12a';
}
.flaticon-musical-note:before {
content: '\f12b';
}
.flaticon-muted:before {
content: '\f12c';
}
.flaticon-next:before {
content: '\f12d';
}
.flaticon-next-1:before {
content: '\f12e';
}
.flaticon-next-2:before {
content: '\f12f';
}
.flaticon-next-3:before {
content: '\f130';
}
.flaticon-padlock:before {
content: '\f131';
}
.flaticon-padlock-1:before {
content: '\f132';
}
.flaticon-paper-clip:before {
content: '\f133';
}
.flaticon-previous:before {
content: '\f134';
}
.flaticon-previous-1:before {
content: '\f135';
}
.flaticon-previous-2:before {
content: '\f136';
}
.flaticon-push-pin:before {
content: '\f137';
}
.flaticon-refresh:before {
content: '\f138';
}
.flaticon-reload:before {
content: '\f139';
}
.flaticon-repeat:before {
content: '\f13a';
}
.flaticon-repeat-1:before {
content: '\f13b';
}
.flaticon-repeat-2:before {
content: '\f13c';
}
.flaticon-repeat-3:before {
content: '\f13d';
}
.flaticon-right-arrow:before {
content: '\f13e';
}
.flaticon-right-arrow-1:before {
content: '\f13f';
}
.flaticon-search:before {
content: '\f140';
}
.flaticon-send:before {
content: '\f141';
}
.flaticon-settings:before {
content: '\f142';
}
.flaticon-share:before {
content: '\f143';
}
.flaticon-shield:before {
content: '\f144';
}
.flaticon-shuffle:before {
content: '\f145';
}
.flaticon-shuffle-1:before {
content: '\f146';
}
.flaticon-sort:before {
content: '\f147';
}
.flaticon-speaker:before {
content: '\f148';
}
.flaticon-speech-bubble:before {
content: '\f149';
}
.flaticon-speech-bubble-1:before {
content: '\f14a';
}
.flaticon-speech-bubble-2:before {
content: '\f14b';
}
.flaticon-speech-bubble-3:before {
content: '\f14c';
}
.flaticon-sticker:before {
content: '\f14d';
}
.flaticon-target:before {
content: '\f14e';
}
.flaticon-telephone:before {
content: '\f14f';
}
.flaticon-telephone-1:before {
content: '\f150';
}
.flaticon-transfer:before {
content: '\f151';
}
.flaticon-transfer-1:before {
content: '\f152';
}
.flaticon-transfer-2:before {
content: '\f153';
}
.flaticon-up-arrow:before {
content: '\f154';
}
.flaticon-up-arrow-1:before {
content: '\f155';
}
.flaticon-up-arrow-2:before {
content: '\f156';
}
.flaticon-upload:before {
content: '\f157';
}
.flaticon-upload-1:before {
content: '\f158';
}
.flaticon-upload-2:before {
content: '\f159';
}
.flaticon-upload-3:before {
content: '\f15a';
}
.flaticon-user:before {
content: '\f15b';
}
.flaticon-user-1:before {
content: '\f15c';
}
.flaticon-wrench:before {
content: '\f15d';
}
.flaticon-zoom-in:before {
content: '\f15e';
}
.flaticon-zoom-out:before {
content: '\f15f';
}
$font-Flaticon-add: '\f100';
$font-Flaticon-basket: '\f101';
$font-Flaticon-bookmark: '\f102';
$font-Flaticon-broken-link: '\f103';
$font-Flaticon-checked: '\f104';
$font-Flaticon-checked-1: '\f105';
$font-Flaticon-checked-2: '\f106';
$font-Flaticon-click: '\f107';
$font-Flaticon-close: '\f108';
$font-Flaticon-close-1: '\f109';
$font-Flaticon-coin: '\f10a';
$font-Flaticon-coins: '\f10b';
$font-Flaticon-compress: '\f10c';
$font-Flaticon-cursor: '\f10d';
$font-Flaticon-cursor-1: '\f10e';
$font-Flaticon-cursor-2: '\f10f';
$font-Flaticon-down-arrow: '\f110';
$font-Flaticon-down-arrow-1: '\f111';
$font-Flaticon-download: '\f112';
$font-Flaticon-download-1: '\f113';
$font-Flaticon-download-2: '\f114';
$font-Flaticon-download-3: '\f115';
$font-Flaticon-download-4: '\f116';
$font-Flaticon-edit: '\f117';
$font-Flaticon-expand: '\f118';
$font-Flaticon-export: '\f119';
$font-Flaticon-folder: '\f11a';
$font-Flaticon-forbidden: '\f11b';
$font-Flaticon-head: '\f11c';
$font-Flaticon-headphones: '\f11d';
$font-Flaticon-home: '\f11e';
$font-Flaticon-inbox: '\f11f';
$font-Flaticon-left-arrow: '\f120';
$font-Flaticon-left-arrow-1: '\f121';
$font-Flaticon-left-arrow-2: '\f122';
$font-Flaticon-levels: '\f123';
$font-Flaticon-levels-1: '\f124';
$font-Flaticon-link: '\f125';
$font-Flaticon-list: '\f126';
$font-Flaticon-login: '\f127';
$font-Flaticon-login-1: '\f128';
$font-Flaticon-mail: '\f129';
$font-Flaticon-move: '\f12a';
$font-Flaticon-musical-note: '\f12b';
$font-Flaticon-muted: '\f12c';
$font-Flaticon-next: '\f12d';
$font-Flaticon-next-1: '\f12e';
$font-Flaticon-next-2: '\f12f';
$font-Flaticon-next-3: '\f130';
$font-Flaticon-padlock: '\f131';
$font-Flaticon-padlock-1: '\f132';
$font-Flaticon-paper-clip: '\f133';
$font-Flaticon-previous: '\f134';
$font-Flaticon-previous-1: '\f135';
$font-Flaticon-previous-2: '\f136';
$font-Flaticon-push-pin: '\f137';
$font-Flaticon-refresh: '\f138';
$font-Flaticon-reload: '\f139';
$font-Flaticon-repeat: '\f13a';
$font-Flaticon-repeat-1: '\f13b';
$font-Flaticon-repeat-2: '\f13c';
$font-Flaticon-repeat-3: '\f13d';
$font-Flaticon-right-arrow: '\f13e';
$font-Flaticon-right-arrow-1: '\f13f';
$font-Flaticon-search: '\f140';
$font-Flaticon-send: '\f141';
$font-Flaticon-settings: '\f142';
$font-Flaticon-share: '\f143';
$font-Flaticon-shield: '\f144';
$font-Flaticon-shuffle: '\f145';
$font-Flaticon-shuffle-1: '\f146';
$font-Flaticon-sort: '\f147';
$font-Flaticon-speaker: '\f148';
$font-Flaticon-speech-bubble: '\f149';
$font-Flaticon-speech-bubble-1: '\f14a';
$font-Flaticon-speech-bubble-2: '\f14b';
$font-Flaticon-speech-bubble-3: '\f14c';
$font-Flaticon-sticker: '\f14d';
$font-Flaticon-target: '\f14e';
$font-Flaticon-telephone: '\f14f';
$font-Flaticon-telephone-1: '\f150';
$font-Flaticon-transfer: '\f151';
$font-Flaticon-transfer-1: '\f152';
$font-Flaticon-transfer-2: '\f153';
$font-Flaticon-up-arrow: '\f154';
$font-Flaticon-up-arrow-1: '\f155';
$font-Flaticon-up-arrow-2: '\f156';
$font-Flaticon-upload: '\f157';
$font-Flaticon-upload-1: '\f158';
$font-Flaticon-upload-2: '\f159';
$font-Flaticon-upload-3: '\f15a';
$font-Flaticon-user: '\f15b';
$font-Flaticon-user-1: '\f15c';
$font-Flaticon-wrench: '\f15d';
$font-Flaticon-zoom-in: '\f15e';
$font-Flaticon-zoom-out: '\f15f';

View File

@ -1,319 +0,0 @@
/*
Flaticon icon font: Flaticon
Creation date: 22/06/2016 15:44
*/
@font-face {
font-family: 'Flaticon';
src: url('./Flaticon.eot');
src: url('./Flaticon.eot?#iefix') format('embedded-opentype'), url('./Flaticon.woff') format('woff'),
url('./Flaticon.ttf') format('truetype'), url('./Flaticon.svg#Flaticon') format('svg');
font-weight: normal;
font-style: normal;
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
@font-face {
font-family: 'Flaticon';
src: url('./Flaticon.svg#Flaticon') format('svg');
}
}
[class^='flaticon-']:before,
[class*=' flaticon-']:before,
[class^='flaticon-']:after,
[class*=' flaticon-']:after {
font-family: Flaticon;
font-size: 20px;
font-style: normal;
margin-left: 20px;
}
.flaticon-add:before {
content: '\f100';
}
.flaticon-basket:before {
content: '\f101';
}
.flaticon-bookmark:before {
content: '\f102';
}
.flaticon-broken-link:before {
content: '\f103';
}
.flaticon-checked:before {
content: '\f104';
}
.flaticon-checked-1:before {
content: '\f105';
}
.flaticon-checked-2:before {
content: '\f106';
}
.flaticon-click:before {
content: '\f107';
}
.flaticon-close:before {
content: '\f108';
}
.flaticon-close-1:before {
content: '\f109';
}
.flaticon-coin:before {
content: '\f10a';
}
.flaticon-coins:before {
content: '\f10b';
}
.flaticon-compress:before {
content: '\f10c';
}
.flaticon-cursor:before {
content: '\f10d';
}
.flaticon-cursor-1:before {
content: '\f10e';
}
.flaticon-cursor-2:before {
content: '\f10f';
}
.flaticon-down-arrow:before {
content: '\f110';
}
.flaticon-down-arrow-1:before {
content: '\f111';
}
.flaticon-download:before {
content: '\f112';
}
.flaticon-download-1:before {
content: '\f113';
}
.flaticon-download-2:before {
content: '\f114';
}
.flaticon-download-3:before {
content: '\f115';
}
.flaticon-download-4:before {
content: '\f116';
}
.flaticon-edit:before {
content: '\f117';
}
.flaticon-expand:before {
content: '\f118';
}
.flaticon-export:before {
content: '\f119';
}
.flaticon-folder:before {
content: '\f11a';
}
.flaticon-forbidden:before {
content: '\f11b';
}
.flaticon-head:before {
content: '\f11c';
}
.flaticon-headphones:before {
content: '\f11d';
}
.flaticon-home:before {
content: '\f11e';
}
.flaticon-inbox:before {
content: '\f11f';
}
.flaticon-left-arrow:before {
content: '\f120';
}
.flaticon-left-arrow-1:before {
content: '\f121';
}
.flaticon-left-arrow-2:before {
content: '\f122';
}
.flaticon-levels:before {
content: '\f123';
}
.flaticon-levels-1:before {
content: '\f124';
}
.flaticon-link:before {
content: '\f125';
}
.flaticon-list:before {
content: '\f126';
}
.flaticon-login:before {
content: '\f127';
}
.flaticon-login-1:before {
content: '\f128';
}
.flaticon-mail:before {
content: '\f129';
}
.flaticon-move:before {
content: '\f12a';
}
.flaticon-musical-note:before {
content: '\f12b';
}
.flaticon-muted:before {
content: '\f12c';
}
.flaticon-next:before {
content: '\f12d';
}
.flaticon-next-1:before {
content: '\f12e';
}
.flaticon-next-2:before {
content: '\f12f';
}
.flaticon-next-3:before {
content: '\f130';
}
.flaticon-padlock:before {
content: '\f131';
}
.flaticon-padlock-1:before {
content: '\f132';
}
.flaticon-paper-clip:before {
content: '\f133';
}
.flaticon-previous:before {
content: '\f134';
}
.flaticon-previous-1:before {
content: '\f135';
}
.flaticon-previous-2:before {
content: '\f136';
}
.flaticon-push-pin:before {
content: '\f137';
}
.flaticon-refresh:before {
content: '\f138';
}
.flaticon-reload:before {
content: '\f139';
}
.flaticon-repeat:before {
content: '\f13a';
}
.flaticon-repeat-1:before {
content: '\f13b';
}
.flaticon-repeat-2:before {
content: '\f13c';
}
.flaticon-repeat-3:before {
content: '\f13d';
}
.flaticon-right-arrow:before {
content: '\f13e';
}
.flaticon-right-arrow-1:before {
content: '\f13f';
}
.flaticon-search:before {
content: '\f140';
}
.flaticon-send:before {
content: '\f141';
}
.flaticon-settings:before {
content: '\f142';
}
.flaticon-share:before {
content: '\f143';
}
.flaticon-shield:before {
content: '\f144';
}
.flaticon-shuffle:before {
content: '\f145';
}
.flaticon-shuffle-1:before {
content: '\f146';
}
.flaticon-sort:before {
content: '\f147';
}
.flaticon-speaker:before {
content: '\f148';
}
.flaticon-speech-bubble:before {
content: '\f149';
}
.flaticon-speech-bubble-1:before {
content: '\f14a';
}
.flaticon-speech-bubble-2:before {
content: '\f14b';
}
.flaticon-speech-bubble-3:before {
content: '\f14c';
}
.flaticon-sticker:before {
content: '\f14d';
}
.flaticon-target:before {
content: '\f14e';
}
.flaticon-telephone:before {
content: '\f14f';
}
.flaticon-telephone-1:before {
content: '\f150';
}
.flaticon-transfer:before {
content: '\f151';
}
.flaticon-transfer-1:before {
content: '\f152';
}
.flaticon-transfer-2:before {
content: '\f153';
}
.flaticon-up-arrow:before {
content: '\f154';
}
.flaticon-up-arrow-1:before {
content: '\f155';
}
.flaticon-up-arrow-2:before {
content: '\f156';
}
.flaticon-upload:before {
content: '\f157';
}
.flaticon-upload-1:before {
content: '\f158';
}
.flaticon-upload-2:before {
content: '\f159';
}
.flaticon-upload-3:before {
content: '\f15a';
}
.flaticon-user:before {
content: '\f15b';
}
.flaticon-user-1:before {
content: '\f15c';
}
.flaticon-wrench:before {
content: '\f15d';
}
.flaticon-zoom-in:before {
content: '\f15e';
}
.flaticon-zoom-out:before {
content: '\f15f';
}

View File

@ -1,935 +0,0 @@
<!DOCTYPE html>
<!--
Flaticon icon font: Flaticon
Creation date: 22/06/2016 15:44
-->
<html>
<!DOCTYPE html>
<html>
<head>
<title>Flaticon WebFont</title>
<link href="http://fonts.googleapis.com/css?family=Varela+Round" rel="stylesheet" type="text/css" />
<link rel="stylesheet" type="text/css" href="flaticon.css">
<meta charset="UTF-8">
<style>
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
body {
font-family: 'Varela Round', Helvetica, Arial, sans-serif;
font-size: 16px;
color: #222;
}
a {
color: #333;
border-bottom: 1px solid #a9fd00;
font-weight: bold;
text-decoration: none;
}
* {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
}
[class^="flaticon-"]:before, [class*=" flaticon-"]:before, [class^="flaticon-"]:after, [class*=" flaticon-"]:after {
font-family: Flaticon;
font-size: 30px;
font-style: normal;
margin-left: 20px;
color: #333;
}
.wrapper {
max-width: 600px;
margin: auto;
padding: 0 1em;
}
.title {
font-size: 1.25em;
text-align: center;
margin-bottom: 1em;
text-transform: uppercase;
}
header {
text-align: center;
background-color: #222;
color: #fff;
padding: 1em;
}
header .logo {
width: 210px;
height: 38px;
display: inline-block;
vertical-align: middle;
margin-right: 1em;
border: none;
}
header strong {
font-size: 1.95em;
font-weight: bold;
vertical-align: middle;
margin-top: 5px;
display: inline-block;
}
.demo {
margin: 2em auto;
line-height: 1.25em;
}
.demo ul li {
margin-bottom: 1em;
}
.demo ul li .num {
color: #222;
border-radius: 20px;
display: inline-block;
width: 26px;
padding: 3px;
height: 26px;
text-align: center;
margin-right: 0.5em;
border: 1px solid #222;
}
.demo ul li code {
background-color: #222;
border-radius: 4px;
padding: 0.25em 0.5em;
display: inline-block;
color: #fff;
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
font-weight: lighter;
margin-top: 1em;
font-size: 0.8em;
word-break: break-all;
}
.demo ul li code.big {
padding: 1em;
font-size: 0.9em;
}
.demo ul li code .red {
color: #EF3159;
}
.demo ul li code .green {
color: #ACFF65;
}
.demo ul li code .yellow {
color: #FFFF99;
}
.demo ul li code .blue {
color: #99D3FF;
}
.demo ul li code .purple {
color: #A295FF;
}
.demo ul li code .dots {
margin-top: 0.5em;
display: block;
}
#glyphs {
border-bottom: 1px solid #ccc;
padding: 2em 0;
text-align: center;
}
.glyph {
display: inline-block;
width: 9em;
margin: 1em;
text-align: center;
vertical-align: top;
background: #FFF;
}
.glyph .glyph-icon {
padding: 10px;
display: block;
font-family:"Flaticon";
font-size: 64px;
line-height: 1;
}
.glyph .glyph-icon:before {
font-size: 64px;
color: #222;
margin-left: 0;
}
.class-name {
font-size: 0.65em;
background-color: #222;
color: #fff;
border-radius: 4px 4px 0 0;
padding: 0.5em;
color: #FFFF99;
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
}
.author-name {
font-size: 0.6em;
background-color: #fcfcfd;
border: 1px solid #DEDEE4;
border-top: 0;
border-radius: 0 0 4px 4px;
padding: 0.5em;
}
.class-name:last-child {
font-size: 10px;
color:#888;
}
.class-name:last-child a {
font-size: 10px;
color:#555;
}
.class-name:last-child a:hover {
color:#a9fd00;
}
.glyph > input {
display: block;
width: 100px;
margin: 5px auto;
text-align: center;
font-size: 12px;
cursor: text;
}
.glyph > input.icon-input {
font-family:"Flaticon";
font-size: 16px;
margin-bottom: 10px;
}
.attribution .title {
margin-top: 2em;
}
.attribution textarea {
background-color: #fcfcfd;
padding: 1em;
border: none;
box-shadow: none;
border: 1px solid #DEDEE4;
border-radius: 4px;
resize: none;
width: 100%;
height: 150px;
font-size: 0.8em;
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
-webkit-appearance: none;
}
.iconsuse {
margin: 2em auto;
text-align: center;
max-width: 1200px;
}
.iconsuse:after {
content: '';
display: table;
clear: both;
}
.iconsuse .image {
float: left;
width: 25%;
padding: 0 1em;
}
.iconsuse .image p {
margin-bottom: 1em;
}
.iconsuse .image span {
display: block;
font-size: 0.65em;
background-color: #222;
color: #fff;
border-radius: 4px;
padding: 0.5em;
color: #FFFF99;
margin-top: 1em;
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
}
#footer {
text-align: center;
background-color: #4C5B5C;
color: #7c9192;
padding: 1em;
}
#footer a {
border: none;
color: #a9fd00;
font-weight: normal;
}
@media (max-width: 960px) {
.iconsuse .image {
width: 50%;
}
}
@media (max-width: 560px) {
.iconsuse .image {
width: 100%;
}
}
</style>
</head>
<body class="characters-off">
<header>
<a href="http://www.flaticon.com" target="_blank" class="logo">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/" viewBox="0 0 560.875 102.036" enable-background="new 0 0 560.875 102.036" xml:space="preserve">
<defs>
</defs>
<g>
<g class="letters">
<path fill="#ffffff" d="M141.596,29.675c0-3.777,2.985-6.767,6.764-6.767h34.438c3.426,0,6.15,2.728,6.15,6.15
c0,3.43-2.724,6.149-6.15,6.149h-27.674v13.091h23.719c3.429,0,6.151,2.724,6.151,6.15c0,3.43-2.723,6.149-6.151,6.149h-23.719
v17.574c0,3.773-2.986,6.761-6.764,6.761c-3.779,0-6.764-2.989-6.764-6.761V29.675z"></path>
<path fill="#ffffff" d="M193.844,29.149c0-3.781,2.985-6.767,6.764-6.767c3.776,0,6.763,2.985,6.763,6.767v42.957h25.039
c3.426,0,6.149,2.726,6.149,6.153c0,3.425-2.723,6.15-6.149,6.15h-31.802c-3.779,0-6.764-2.986-6.764-6.768V29.149z"></path>
<path fill="#ffffff" d="M241.891,75.71l21.438-48.407c1.492-3.341,4.215-5.357,7.906-5.357h0.792
c3.686,0,6.323,2.017,7.815,5.357l21.439,48.407c0.436,0.967,0.701,1.845,0.701,2.723c0,3.602-2.809,6.501-6.414,6.501
c-3.161,0-5.269-1.845-6.499-4.655l-4.132-9.661h-27.059l-4.301,10.102c-1.144,2.631-3.426,4.214-6.237,4.214
c-3.517,0-6.24-2.81-6.24-6.325C241.1,77.64,241.451,76.677,241.891,75.71z M279.932,58.666l-8.521-20.297l-8.526,20.297H279.932
z"></path>
<path fill="#ffffff" d="M314.864,35.387H301.86c-3.429,0-6.239-2.813-6.239-6.238c0-3.429,2.811-6.24,6.239-6.24h39.533
c3.426,0,6.237,2.811,6.237,6.24c0,3.425-2.811,6.238-6.237,6.238h-13.001v42.785c0,3.773-2.99,6.761-6.764,6.761
c-3.779,0-6.764-2.989-6.764-6.761V35.387z"></path>
<path fill="#A9FD00" d="M352.615,29.149c0-3.781,2.985-6.767,6.767-6.767c3.774,0,6.761,2.985,6.761,6.767v49.024
c0,3.773-2.987,6.761-6.761,6.761c-3.781,0-6.767-2.989-6.767-6.761V29.149z"></path>
<path fill="#A9FD00" d="M374.132,53.836v-0.179c0-17.481,13.178-31.801,32.065-31.801c9.22,0,15.459,2.458,20.557,6.238
c1.402,1.054,2.637,2.985,2.637,5.357c0,3.692-2.985,6.59-6.681,6.59c-1.845,0-3.071-0.702-4.044-1.319
c-3.776-2.813-7.729-4.393-12.562-4.393c-10.364,0-17.831,8.611-17.831,19.154v0.173c0,10.542,7.291,19.329,17.831,19.329
c5.715,0,9.492-1.756,13.359-4.834c1.049-0.874,2.458-1.491,4.039-1.491c3.429,0,6.325,2.813,6.325,6.236
c0,2.106-1.056,3.78-2.282,4.834c-5.539,4.834-12.036,7.733-21.878,7.733C387.572,85.464,374.132,71.493,374.132,53.836z"></path>
<path fill="#A9FD00" d="M433.009,53.836v-0.179c0-17.481,13.79-31.801,32.766-31.801c18.981,0,32.592,14.143,32.592,31.628v0.173
c0,17.483-13.785,31.807-32.769,31.807C446.625,85.464,433.009,71.32,433.009,53.836z M484.224,53.836v-0.179
c0-10.539-7.725-19.326-18.626-19.326c-10.893,0-18.449,8.611-18.449,19.154v0.173c0,10.542,7.73,19.329,18.626,19.329
C476.676,72.986,484.224,64.378,484.224,53.836z"></path>
<path fill="#A9FD00" d="M506.233,29.321c0-3.774,2.99-6.763,6.767-6.763h1.401c3.252,0,5.183,1.583,7.029,3.953l26.093,34.265
V29.059c0-3.692,2.99-6.677,6.681-6.677c3.683,0,6.671,2.985,6.671,6.677v48.934c0,3.78-2.987,6.765-6.764,6.765h-0.436
c-3.257,0-5.188-1.581-7.034-3.953l-27.056-35.492v32.944c0,3.687-2.985,6.676-6.678,6.676c-3.683,0-6.673-2.989-6.673-6.676
V29.321z"></path>
</g>
<g class="insignia">
<path fill="#ffffff" d="M48.372,56.137h12.517l11.156-18.537H37.186L25.688,18.539h57.825L94.668,0H9.271
C5.925,0,2.842,1.801,1.198,4.716c-1.644,2.907-1.593,6.482,0.134,9.343l50.38,83.501c1.678,2.781,4.689,4.476,7.938,4.476
c3.246,0,6.257-1.695,7.935-4.476l2.898-4.804L48.372,56.137z"></path>
<g class="i">
<path fill="#A9FD00" d="M93.575,18.539h0.031v0.004l21.652,0.004l2.705-4.488c1.727-2.861,1.778-6.436,0.133-9.343
C116.454,1.801,113.371,0,110.026,0h-5.294L93.575,18.539z"></path>
<polygon fill="#A9FD00" points="88.291,27.356 64.725,66.486 75.519,84.404 109.942,27.356"></polygon>
</g>
</g>
</g>
</svg>
</a>
<strong>Font Demo</strong>
</header>
<section class="demo wrapper">
<p class="title">Instructions</p>
<ul>
<li>
<span class="num">1</span>Copy the "Fonts" files and CSS files to your website CSS folder.
</li>
<li>
<span class="num">2</span>Add the CSS link to your website source code on header.
<code class="big">
&lt;<span class="red">head</span>&gt;
<br/><span class="dots">...</span>
<br/>&lt;<span class="red">link</span> <span class="green">rel</span>=<span class="yellow">"stylesheet"</span> <span class="green">type</span>=<span class="yellow">"text/css"</span> <span class="green">href</span>=<span class="yellow">"your_website_domain/css_root/flaticon.css"</span>&gt;
<br/><span class="dots">...</span>
<br/>&lt;/<span class="red">head</span>&gt;
</code>
</li>
<li>
<p>
<span class="num">3</span>Use the icon class on <code>"<span class="blue">display</span>:<span class="purple"> inline</span>"</code> elements:
<br />
Use example: <code>&lt;<span class="red">i</span> <span class="green">class</span>=<span class="yellow">&quot;flaticon-airplane49&quot;</span>&gt;&lt;/<span class="red">i</span>&gt;</code> or <code>&lt;<span class="red">span</span> <span class="green">class</span>=<span class="yellow">&quot;flaticon-airplane49&quot;</span>&gt;&lt;/<span class="red">span</span>&gt;</code>
</li>
</ul>
</section>
<section id="glyphs">
<div class="glyph"><div class="glyph-icon flaticon-add"></div>
<div class="class-name">.flaticon-add</div>
<div class="author-name">Author: <a data-file="add" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-basket"></div>
<div class="class-name">.flaticon-basket</div>
<div class="author-name">Author: <a data-file="basket" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-bookmark"></div>
<div class="class-name">.flaticon-bookmark</div>
<div class="author-name">Author: <a data-file="bookmark" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-broken-link"></div>
<div class="class-name">.flaticon-broken-link</div>
<div class="author-name">Author: <a data-file="broken-link" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-checked"></div>
<div class="class-name">.flaticon-checked</div>
<div class="author-name">Author: <a data-file="checked" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-checked-1"></div>
<div class="class-name">.flaticon-checked-1</div>
<div class="author-name">Author: <a data-file="checked-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-checked-2"></div>
<div class="class-name">.flaticon-checked-2</div>
<div class="author-name">Author: <a data-file="checked-2" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-click"></div>
<div class="class-name">.flaticon-click</div>
<div class="author-name">Author: <a data-file="click" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-close"></div>
<div class="class-name">.flaticon-close</div>
<div class="author-name">Author: <a data-file="close" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-close-1"></div>
<div class="class-name">.flaticon-close-1</div>
<div class="author-name">Author: <a data-file="close-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-coin"></div>
<div class="class-name">.flaticon-coin</div>
<div class="author-name">Author: <a data-file="coin" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-coins"></div>
<div class="class-name">.flaticon-coins</div>
<div class="author-name">Author: <a data-file="coins" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-compress"></div>
<div class="class-name">.flaticon-compress</div>
<div class="author-name">Author: <a data-file="compress" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-cursor"></div>
<div class="class-name">.flaticon-cursor</div>
<div class="author-name">Author: <a data-file="cursor" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-cursor-1"></div>
<div class="class-name">.flaticon-cursor-1</div>
<div class="author-name">Author: <a data-file="cursor-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-cursor-2"></div>
<div class="class-name">.flaticon-cursor-2</div>
<div class="author-name">Author: <a data-file="cursor-2" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-down-arrow"></div>
<div class="class-name">.flaticon-down-arrow</div>
<div class="author-name">Author: <a data-file="down-arrow" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-down-arrow-1"></div>
<div class="class-name">.flaticon-down-arrow-1</div>
<div class="author-name">Author: <a data-file="down-arrow-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-download"></div>
<div class="class-name">.flaticon-download</div>
<div class="author-name">Author: <a data-file="download" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-download-1"></div>
<div class="class-name">.flaticon-download-1</div>
<div class="author-name">Author: <a data-file="download-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-download-2"></div>
<div class="class-name">.flaticon-download-2</div>
<div class="author-name">Author: <a data-file="download-2" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-download-3"></div>
<div class="class-name">.flaticon-download-3</div>
<div class="author-name">Author: <a data-file="download-3" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-download-4"></div>
<div class="class-name">.flaticon-download-4</div>
<div class="author-name">Author: <a data-file="download-4" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-edit"></div>
<div class="class-name">.flaticon-edit</div>
<div class="author-name">Author: <a data-file="edit" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-expand"></div>
<div class="class-name">.flaticon-expand</div>
<div class="author-name">Author: <a data-file="expand" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-export"></div>
<div class="class-name">.flaticon-export</div>
<div class="author-name">Author: <a data-file="export" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-folder"></div>
<div class="class-name">.flaticon-folder</div>
<div class="author-name">Author: <a data-file="folder" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-forbidden"></div>
<div class="class-name">.flaticon-forbidden</div>
<div class="author-name">Author: <a data-file="forbidden" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-head"></div>
<div class="class-name">.flaticon-head</div>
<div class="author-name">Author: <a data-file="head" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-headphones"></div>
<div class="class-name">.flaticon-headphones</div>
<div class="author-name">Author: <a data-file="headphones" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-home"></div>
<div class="class-name">.flaticon-home</div>
<div class="author-name">Author: <a data-file="home" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-inbox"></div>
<div class="class-name">.flaticon-inbox</div>
<div class="author-name">Author: <a data-file="inbox" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-left-arrow"></div>
<div class="class-name">.flaticon-left-arrow</div>
<div class="author-name">Author: <a data-file="left-arrow" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-left-arrow-1"></div>
<div class="class-name">.flaticon-left-arrow-1</div>
<div class="author-name">Author: <a data-file="left-arrow-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-left-arrow-2"></div>
<div class="class-name">.flaticon-left-arrow-2</div>
<div class="author-name">Author: <a data-file="left-arrow-2" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-levels"></div>
<div class="class-name">.flaticon-levels</div>
<div class="author-name">Author: <a data-file="levels" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-levels-1"></div>
<div class="class-name">.flaticon-levels-1</div>
<div class="author-name">Author: <a data-file="levels-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-link"></div>
<div class="class-name">.flaticon-link</div>
<div class="author-name">Author: <a data-file="link" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-list"></div>
<div class="class-name">.flaticon-list</div>
<div class="author-name">Author: <a data-file="list" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-login"></div>
<div class="class-name">.flaticon-login</div>
<div class="author-name">Author: <a data-file="login" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-login-1"></div>
<div class="class-name">.flaticon-login-1</div>
<div class="author-name">Author: <a data-file="login-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-mail"></div>
<div class="class-name">.flaticon-mail</div>
<div class="author-name">Author: <a data-file="mail" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-move"></div>
<div class="class-name">.flaticon-move</div>
<div class="author-name">Author: <a data-file="move" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-musical-note"></div>
<div class="class-name">.flaticon-musical-note</div>
<div class="author-name">Author: <a data-file="musical-note" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-muted"></div>
<div class="class-name">.flaticon-muted</div>
<div class="author-name">Author: <a data-file="muted" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-next"></div>
<div class="class-name">.flaticon-next</div>
<div class="author-name">Author: <a data-file="next" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-next-1"></div>
<div class="class-name">.flaticon-next-1</div>
<div class="author-name">Author: <a data-file="next-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-next-2"></div>
<div class="class-name">.flaticon-next-2</div>
<div class="author-name">Author: <a data-file="next-2" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-next-3"></div>
<div class="class-name">.flaticon-next-3</div>
<div class="author-name">Author: <a data-file="next-3" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-padlock"></div>
<div class="class-name">.flaticon-padlock</div>
<div class="author-name">Author: <a data-file="padlock" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-padlock-1"></div>
<div class="class-name">.flaticon-padlock-1</div>
<div class="author-name">Author: <a data-file="padlock-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-paper-clip"></div>
<div class="class-name">.flaticon-paper-clip</div>
<div class="author-name">Author: <a data-file="paper-clip" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-previous"></div>
<div class="class-name">.flaticon-previous</div>
<div class="author-name">Author: <a data-file="previous" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-previous-1"></div>
<div class="class-name">.flaticon-previous-1</div>
<div class="author-name">Author: <a data-file="previous-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-previous-2"></div>
<div class="class-name">.flaticon-previous-2</div>
<div class="author-name">Author: <a data-file="previous-2" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-push-pin"></div>
<div class="class-name">.flaticon-push-pin</div>
<div class="author-name">Author: <a data-file="push-pin" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-refresh"></div>
<div class="class-name">.flaticon-refresh</div>
<div class="author-name">Author: <a data-file="refresh" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-reload"></div>
<div class="class-name">.flaticon-reload</div>
<div class="author-name">Author: <a data-file="reload" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-repeat"></div>
<div class="class-name">.flaticon-repeat</div>
<div class="author-name">Author: <a data-file="repeat" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-repeat-1"></div>
<div class="class-name">.flaticon-repeat-1</div>
<div class="author-name">Author: <a data-file="repeat-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-repeat-2"></div>
<div class="class-name">.flaticon-repeat-2</div>
<div class="author-name">Author: <a data-file="repeat-2" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-repeat-3"></div>
<div class="class-name">.flaticon-repeat-3</div>
<div class="author-name">Author: <a data-file="repeat-3" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-right-arrow"></div>
<div class="class-name">.flaticon-right-arrow</div>
<div class="author-name">Author: <a data-file="right-arrow" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-right-arrow-1"></div>
<div class="class-name">.flaticon-right-arrow-1</div>
<div class="author-name">Author: <a data-file="right-arrow-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-search"></div>
<div class="class-name">.flaticon-search</div>
<div class="author-name">Author: <a data-file="search" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-send"></div>
<div class="class-name">.flaticon-send</div>
<div class="author-name">Author: <a data-file="send" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-settings"></div>
<div class="class-name">.flaticon-settings</div>
<div class="author-name">Author: <a data-file="settings" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-share"></div>
<div class="class-name">.flaticon-share</div>
<div class="author-name">Author: <a data-file="share" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-shield"></div>
<div class="class-name">.flaticon-shield</div>
<div class="author-name">Author: <a data-file="shield" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-shuffle"></div>
<div class="class-name">.flaticon-shuffle</div>
<div class="author-name">Author: <a data-file="shuffle" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-shuffle-1"></div>
<div class="class-name">.flaticon-shuffle-1</div>
<div class="author-name">Author: <a data-file="shuffle-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-sort"></div>
<div class="class-name">.flaticon-sort</div>
<div class="author-name">Author: <a data-file="sort" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-speaker"></div>
<div class="class-name">.flaticon-speaker</div>
<div class="author-name">Author: <a data-file="speaker" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-speech-bubble"></div>
<div class="class-name">.flaticon-speech-bubble</div>
<div class="author-name">Author: <a data-file="speech-bubble" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-speech-bubble-1"></div>
<div class="class-name">.flaticon-speech-bubble-1</div>
<div class="author-name">Author: <a data-file="speech-bubble-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-speech-bubble-2"></div>
<div class="class-name">.flaticon-speech-bubble-2</div>
<div class="author-name">Author: <a data-file="speech-bubble-2" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-speech-bubble-3"></div>
<div class="class-name">.flaticon-speech-bubble-3</div>
<div class="author-name">Author: <a data-file="speech-bubble-3" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-sticker"></div>
<div class="class-name">.flaticon-sticker</div>
<div class="author-name">Author: <a data-file="sticker" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-target"></div>
<div class="class-name">.flaticon-target</div>
<div class="author-name">Author: <a data-file="target" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-telephone"></div>
<div class="class-name">.flaticon-telephone</div>
<div class="author-name">Author: <a data-file="telephone" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-telephone-1"></div>
<div class="class-name">.flaticon-telephone-1</div>
<div class="author-name">Author: <a data-file="telephone-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-transfer"></div>
<div class="class-name">.flaticon-transfer</div>
<div class="author-name">Author: <a data-file="transfer" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-transfer-1"></div>
<div class="class-name">.flaticon-transfer-1</div>
<div class="author-name">Author: <a data-file="transfer-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-transfer-2"></div>
<div class="class-name">.flaticon-transfer-2</div>
<div class="author-name">Author: <a data-file="transfer-2" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-up-arrow"></div>
<div class="class-name">.flaticon-up-arrow</div>
<div class="author-name">Author: <a data-file="up-arrow" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-up-arrow-1"></div>
<div class="class-name">.flaticon-up-arrow-1</div>
<div class="author-name">Author: <a data-file="up-arrow-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-up-arrow-2"></div>
<div class="class-name">.flaticon-up-arrow-2</div>
<div class="author-name">Author: <a data-file="up-arrow-2" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-upload"></div>
<div class="class-name">.flaticon-upload</div>
<div class="author-name">Author: <a data-file="upload" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-upload-1"></div>
<div class="class-name">.flaticon-upload-1</div>
<div class="author-name">Author: <a data-file="upload-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-upload-2"></div>
<div class="class-name">.flaticon-upload-2</div>
<div class="author-name">Author: <a data-file="upload-2" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-upload-3"></div>
<div class="class-name">.flaticon-upload-3</div>
<div class="author-name">Author: <a data-file="upload-3" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-user"></div>
<div class="class-name">.flaticon-user</div>
<div class="author-name">Author: <a data-file="user" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-user-1"></div>
<div class="class-name">.flaticon-user-1</div>
<div class="author-name">Author: <a data-file="user-1" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-wrench"></div>
<div class="class-name">.flaticon-wrench</div>
<div class="author-name">Author: <a data-file="wrench" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-zoom-in"></div>
<div class="class-name">.flaticon-zoom-in</div>
<div class="author-name">Author: <a data-file="zoom-in" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
<div class="glyph"><div class="glyph-icon flaticon-zoom-out"></div>
<div class="class-name">.flaticon-zoom-out</div>
<div class="author-name">Author: <a data-file="zoom-out" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a> </div>
</div>
</section>
<section class="attribution wrapper" style="text-align:center;">
<div class="title">License and attribution:</div><div class="attrDiv">Font generated by <a href="http://www.flaticon.com">flaticon.com</a>. <div><p>Under <a href="http://creativecommons.org/licenses/by/3.0/">CC</a>: <a data-file="export" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a></p> </div>
</div>
<div class="title">Copy the Attribution License:</div>
<textarea onclick="this.focus();this.select();">Font generated by &lt;a href=&quot;http://www.flaticon.com&quot;&gt;flaticon.com&lt;/a&gt;. <p>Under <a href="http://creativecommons.org/licenses/by/3.0/">CC</a>: <a data-file="export" href="http://www.flaticon.com/authors/gregor-cresnar">Gregor Cresnar</a></p>
</textarea>
</section>
<section class="iconsuse">
<div class="title">Examples:</div>
<div class="image">
<p>
<i class="glyph-icon flaticon-add"></i>
<span>&lt;i class=&quot;flaticon-add&quot;&gt;&lt;/i&gt;</span>
</p>
</div>
<div class="image">
<p>
<i class="glyph-icon flaticon-basket"></i>
<span>&lt;i class=&quot;flaticon-basket&quot;&gt;&lt;/i&gt;</span>
</p>
</div>
<div class="image">
<p>
<i class="glyph-icon flaticon-bookmark"></i>
<span>&lt;i class=&quot;flaticon-bookmark&quot;&gt;&lt;/i&gt;</span>
</p>
</div>
<div class="image">
<p>
<i class="glyph-icon flaticon-broken-link"></i>
<span>&lt;i class=&quot;flaticon-broken-link&quot;&gt;&lt;/i&gt;</span>
</p>
</div>
</div>
</section>
<div id="footer">
<div>Generated by <a href="http://www.flaticon.com">flaticon.com</a>
</div>
</div>
</body>
</html>

View File

@ -1,179 +0,0 @@
.funnel {
.funnel-success {
background-color: lighten($blue, 20%);
}
.funnel-dropped {
background-color: $gray-200;
}
table {
table-layout: fixed;
border-top: 0;
margin-bottom: 0;
td {
padding: 0 0.75rem;
}
}
}
.svg-funnel-js {
.svg-funnel-js__container {
width: 100%;
height: 100%;
}
.svg-funnel-js__labels {
width: 100%;
box-sizing: border-box;
.svg-funnel-js__label {
flex: 1 1 0;
position: relative;
.label__value {
display: none;
}
.label__title {
margin-top: 1rem;
font-size: 12px;
font-weight: 300;
}
.label__percentage {
font-size: 16px;
font-weight: bold;
}
.label__segment-percentages {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 100%;
left: 0;
padding: 8px 24px;
box-sizing: border-box;
background-color: $percentage-hover;
margin-top: 24px;
opacity: 0;
transition: opacity 0.1s ease;
cursor: default;
ul {
margin: 0;
padding: 0;
list-style-type: none;
li {
font-size: 13px;
line-height: 16px;
color: $white;
margin: 18px 0;
.percentage__list-label {
font-weight: bold;
color: $primary;
}
}
}
}
&:hover {
.label__segment-percentages {
opacity: 1;
}
}
}
}
&:not(.svg-funnel-js--vertical) {
padding-top: 64px;
padding-bottom: 16px;
.svg-funnel-js__label {
padding-left: 24px;
&:not(:first-child) {
border-left: 1px solid $gray-300;
}
}
}
&.svg-funnel-js--vertical {
padding-left: 120px;
padding-right: 16px;
.svg-funnel-js__label {
padding-top: 24px;
&:not(:first-child) {
border-top: 1px solid $secondary;
}
.label__segment-percentages {
margin-top: 0;
margin-left: 106px;
width: calc(100% - 106px);
.segment-percentage__list {
display: flex;
justify-content: space-around;
}
}
}
}
.svg-funnel-js__subLabels {
display: flex;
justify-content: center;
margin-top: 24px;
position: absolute;
width: 100%;
left: 0;
.svg-funnel-js__subLabel {
display: flex;
font-size: 12px;
color: $white;
line-height: 16px;
&:not(:first-child) {
margin-left: 16px;
}
.svg-funnel-js__subLabel--color {
width: 12px;
height: 12px;
border-radius: 50%;
margin: 2px 8px 2px 0;
}
}
}
}
.card-funnel {
min-width: 250px;
}
.funnel-step {
.card {
flex-direction: row;
}
.funnel-step-side {
background: rgba(0, 0, 0, 0.03);
width: 40px;
padding: 11px 12px;
text-align: center;
}
.select-box {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
}
.select-box > a {
margin-top: 3px;
}
.select-box-select {
width: 300px;
margin-right: 16px;
}
}

View File

@ -1,13 +0,0 @@
.Toastify__toast-container {
width: 530px;
opacity: 1;
transform: none;
}
.Toastify__toast {
padding: 1rem 2rem;
border-radius: 3px;
color: $body-color;
}
.Toastify__progress-bar--default {
background: $green;
}

50
frontend/src/vars.scss Normal file
View File

@ -0,0 +1,50 @@
/* This file contains the main styling configuration for PostHog. We also use AntD which uses LESS,
when changing any base config here, please remember to update antd.less too */
// Main colors
$primary: #5375ff;
$primary_alt: #35416b;
$success: #77b96c;
$warning: #f7a501;
$danger: #f96132;
// Text colors
$text_default: #2d2d2d;
$text_light: rgba(255, 255, 255, 0.88);
$text_muted: #d9d9d9;
$text_muted_alt: #35416b;
// Background colors
$bg_navy: #35416b;
$bg_charcoal: #2d2d2d;
$bg_mid: #f2f2f2;
$bg_light: #ffffff;
$bg_depth: #0f0f0f;
$bg_menu: #2c3035;
// Border colors
$border_light: #f0f0f0;
$border: #d9d9d9;
$border_dark: #bdbdbd;
.bg-mid {
background-color: $bg_mid !important;
}
// Blue Swatch
$blue_700: #35416b;
$blue_500: #597dce;
$blue_300: #8da9e7;
$blue_100: #b8cefd;
// Purple Swatch
$purple_700: #7c4286;
$purple_500: #c278cf;
$purple_300: #dcb1e3;
// Additional style configurations
.mixin-elevated {
box-shadow: 0px 80px 80px rgba(0, 0, 0, 0.075), 0px 10px 10px rgba(0, 0, 0, 0.035) !important;
}
$default_spacing: 16px;
$radius: 2px;

View File

@ -36,7 +36,6 @@
"@mariusandra/simmerjs": "0.7.1-posthog.1",
"antd": "^4.1.1",
"babel-preset-nano-react-app": "^0.1.0",
"bootstrap": "^4.4.1",
"chart.js": "^2.9.3",
"core-js": "3.6.5",
"d3": "^5.15.0",
@ -103,6 +102,8 @@
"html-webpack-plugin": "^4.4.1",
"husky": "~4.2.5",
"kea-typegen": "^0.3.5",
"less": "^3.12.2",
"less-loader": "^7.0.2",
"lint-staged": "~10.2.13",
"mini-css-extract-plugin": "^0.11.0",
"nodemon": "^2.0.4",

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