mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-21 21:49:51 +01:00
parent
eb4817b8f3
commit
cd251df713
@ -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'],
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
@ -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
20
frontend/src/antd.less
Normal 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
334
frontend/src/global.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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'
|
||||
|
@ -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)
|
||||
|
@ -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 %}
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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%;
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
@ -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={{
|
||||
|
@ -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 = [
|
||||
|
@ -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={
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
@ -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>
|
||||
),
|
||||
|
||||
|
10
frontend/src/lib/components/CloseButton.tsx
Normal file
10
frontend/src/lib/components/CloseButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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')
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -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%;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -125,7 +125,7 @@ function DatePickerDropdown(props) {
|
||||
}, [calendarOpen])
|
||||
|
||||
return (
|
||||
<div className="dropdown" ref={dropdownRef}>
|
||||
<div ref={dropdownRef}>
|
||||
<a
|
||||
style={{
|
||||
margin: '0 1rem',
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
15
frontend/src/lib/components/PageHeader.tsx
Normal file
15
frontend/src/lib/components/PageHeader.tsx
Normal 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>}
|
||||
</>
|
||||
)
|
||||
}
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -118,7 +118,6 @@ export function SaveToDashboardModal({
|
||||
name="name"
|
||||
required
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Users who did x"
|
||||
autoFocus={!name}
|
||||
value={name}
|
||||
|
@ -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">×</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) {
|
||||
|
@ -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>
|
||||
|
@ -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 glad to have you here! Let's get you started with 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"
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 PostHog’s 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>
|
||||
)
|
||||
}
|
||||
|
@ -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)}
|
||||
|
@ -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 & usage information <span style={{ fontSize: 12, color: '#F7A501' }}>BETA</span>
|
||||
</h1>
|
||||
<PageHeader
|
||||
title={
|
||||
<>
|
||||
Billing & usage information <span style={{ fontSize: 12, color: '#F7A501' }}>BETA</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="space-top" />
|
||||
<Card title="Current usage">
|
||||
{user?.billing?.current_usage && (
|
||||
|
@ -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 ? (
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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)}
|
||||
|
@ -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>
|
||||
|
@ -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%);
|
@ -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}
|
||||
|
18
frontend/src/scenes/funnels/FunnelPeople.scss
Normal file
18
frontend/src/scenes/funnels/FunnelPeople.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
})
|
||||
|
87
frontend/src/scenes/funnels/FunnelViz.scss
Normal file
87
frontend/src/scenes/funnels/FunnelViz.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 />
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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()}
|
||||
|
@ -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
|
||||
|
@ -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 }}>↳</span>
|
||||
<div
|
||||
className="btn btn-sm btn-light ml-2"
|
||||
<span style={{ color: '#C4C4C4', fontSize: 18, paddingLeft: 6, paddingRight: 2 }}>↳</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>
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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 })} />
|
||||
|
@ -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 <></>
|
||||
}
|
||||
|
@ -1,11 +1,3 @@
|
||||
.filter-action-only {
|
||||
display: none;
|
||||
}
|
||||
.filter-action:hover {
|
||||
.filter-action-only {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
.graph-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
@ -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"
|
||||
|
@ -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={
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ export function ChangePassword(): JSX.Element {
|
||||
<Form
|
||||
onFinish={submit}
|
||||
labelCol={{
|
||||
span: 8,
|
||||
span: 4,
|
||||
}}
|
||||
wrapperCol={{
|
||||
span: 16,
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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 website's HTML. Ideally, put it just above the
|
||||
<code>{'<head>'}</code> 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 website's HTML. Ideally, put it just above the
|
||||
<code>{'<head>'}</code> 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>
|
||||
)
|
||||
}
|
||||
|
@ -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 />
|
||||
|
@ -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;
|
||||
}
|
@ -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',
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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={{
|
||||
|
@ -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)
|
||||
|
@ -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.
Binary file not shown.
@ -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';
|
@ -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';
|
||||
}
|
@ -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">
|
||||
<<span class="red">head</span>>
|
||||
<br/><span class="dots">...</span>
|
||||
<br/><<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>>
|
||||
<br/><span class="dots">...</span>
|
||||
<br/></<span class="red">head</span>>
|
||||
</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><<span class="red">i</span> <span class="green">class</span>=<span class="yellow">"flaticon-airplane49"</span>></<span class="red">i</span>></code> or <code><<span class="red">span</span> <span class="green">class</span>=<span class="yellow">"flaticon-airplane49"</span>></<span class="red">span</span>></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 <a href="http://www.flaticon.com">flaticon.com</a>. <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><i class="flaticon-add"></i></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="image">
|
||||
<p>
|
||||
<i class="glyph-icon flaticon-basket"></i>
|
||||
<span><i class="flaticon-basket"></i></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="image">
|
||||
<p>
|
||||
<i class="glyph-icon flaticon-bookmark"></i>
|
||||
<span><i class="flaticon-bookmark"></i></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="image">
|
||||
<p>
|
||||
<i class="glyph-icon flaticon-broken-link"></i>
|
||||
<span><i class="flaticon-broken-link"></i></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<div id="footer">
|
||||
<div>Generated by <a href="http://www.flaticon.com">flaticon.com</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
50
frontend/src/vars.scss
Normal 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;
|
@ -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
Loading…
Reference in New Issue
Block a user