Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added button with history of flag/value #39

Merged
merged 3 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion featureflags/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.1.0"
__version__ = "1.1.1"
66 changes: 66 additions & 0 deletions featureflags/graph/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,52 @@ async def value_project(ids: list[int]) -> list[int]:
return ids


@pass_context
async def get_flag_last_action_timestamp(
ctx: dict, fields: list[Field]
) -> list[str | None]:
if not ctx[GraphContext.USER_SESSION].is_authenticated:
return []

[field] = fields
opts = field.options
flag_id = UUID(opts["id"])

result = await exec_scalar(
ctx[GraphContext.DB_ENGINE],
(
select([Changelog.timestamp])
.where(Changelog.flag == flag_id)
.order_by(Changelog.timestamp.desc())
.limit(1)
),
)
return [str(result) if result else None]


@pass_context
async def get_value_last_action_timestamp(
ctx: dict, fields: list[Field]
) -> list[str | None]:
if not ctx[GraphContext.USER_SESSION].is_authenticated:
return []

[field] = fields
opts = field.options
value_id = UUID(opts["id"])

result = await exec_scalar(
ctx[GraphContext.DB_ENGINE],
(
select([ValueChangelog.timestamp])
.where(ValueChangelog.value == value_id)
.order_by(ValueChangelog.timestamp.desc())
.limit(1)
),
)
return [str(result) if result else None]


ID_FIELD = Field("id", None, id_field)

flag_fq = FieldsQuery(GraphContext.DB_ENGINE, Flag.__table__)
Expand All @@ -260,6 +306,8 @@ async def value_project(ids: list[int]) -> list[int]:
Field("name", None, flag_fq),
Field("project", None, flag_fq),
Field("enabled", None, flag_fq),
Field("created_timestamp", None, flag_fq),
Field("reported_timestamp", None, flag_fq),
],
)

Expand All @@ -274,6 +322,8 @@ async def value_project(ids: list[int]) -> list[int]:
Field("enabled", None, value_fq),
Field("value_default", None, value_fq),
Field("value_override", None, value_fq),
Field("created_timestamp", None, value_fq),
Field("reported_timestamp", None, value_fq),
],
)

Expand Down Expand Up @@ -413,6 +463,8 @@ async def value_project(ids: list[int]) -> list[int]:
None,
flag_sg.c(if_some([S.enabled, S.this.enabled], True, False)),
),
Field("created_timestamp", None, flag_sg),
Field("reported_timestamp", None, flag_sg),
],
)

Expand Down Expand Up @@ -449,6 +501,8 @@ async def value_project(ids: list[int]) -> list[int]:
),
Field("value_default", None, value_sg),
Field("value_override", None, value_sg),
Field("created_timestamp", None, value_sg),
Field("reported_timestamp", None, value_sg),
],
)

Expand Down Expand Up @@ -538,6 +592,18 @@ async def value_project(ids: list[int]) -> list[int]:

RootNode = Root(
[
Field(
"flagLastActionTimestamp",
Optional[String],
get_flag_last_action_timestamp,
options=[Option("id", String)],
),
Field(
"valueLastActionTimestamp",
Optional[String],
get_value_last_action_timestamp,
options=[Option("id", String)],
),
Link(
"flag",
Optional["Flag"],
Expand Down
23 changes: 23 additions & 0 deletions featureflags/http/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
notify server about new projects/variables/flags
TODO: refactor.
"""
from datetime import datetime
from uuid import UUID, uuid4

from aiopg.sa import SAConnection
Expand Down Expand Up @@ -129,6 +130,14 @@ async def _insert_flag(
return await result.scalar()


async def _update_flag_report_timestamp(flag_id: UUID, *, conn: SAConnection):
await conn.execute(
Flag.__table__.update()
.where(Flag.id == flag_id)
.values({Flag.reported_timestamp: datetime.utcnow()})
)


async def _get_or_create_flag(
project: UUID,
flag: str,
Expand All @@ -146,6 +155,9 @@ async def _get_or_create_flag(
id_ = await _select_flag(project, flag, conn=conn)
assert id_ is not None # must be in db
entity_cache.flag[project][flag] = id_

await _update_flag_report_timestamp(id_, conn=conn)

return id_


Expand Down Expand Up @@ -187,6 +199,14 @@ async def _insert_value(
return await result.scalar()


async def _update_value_report_timestamp(value_id: UUID, *, conn: SAConnection):
await conn.execute(
Value.__table__.update()
.where(Value.id == value_id)
.values({Value.reported_timestamp: datetime.utcnow()})
)


async def _get_or_create_value(
project: UUID,
value: str,
Expand All @@ -205,6 +225,9 @@ async def _get_or_create_value(
id_ = await _select_value(project, value, conn=conn)
assert id_ is not None # must be in db
entity_cache.value[project][value] = id_

await _update_value_report_timestamp(id_, conn=conn)

return id_


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import sqlalchemy as sa

from alembic import op
from sqlalchemy.dialects import postgresql


revision = "4d42cf3d11de"
down_revision = "1876f90b58e8"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"flag",
sa.Column("created_timestamp", postgresql.TIMESTAMP(), nullable=True),
)
op.add_column(
"flag",
sa.Column("reported_timestamp", postgresql.TIMESTAMP(), nullable=True),
)
op.add_column(
"value",
sa.Column("created_timestamp", postgresql.TIMESTAMP(), nullable=True),
)
op.add_column(
"value",
sa.Column("reported_timestamp", postgresql.TIMESTAMP(), nullable=True),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("value", "reported_timestamp")
op.drop_column("value", "created_timestamp")
op.drop_column("flag", "reported_timestamp")
op.drop_column("flag", "created_timestamp")
# ### end Alembic commands ###
14 changes: 14 additions & 0 deletions featureflags/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ class Flag(Base):
id = Column(UUID(as_uuid=True), primary_key=True)
name = Column(String, nullable=False)
enabled = Column(Boolean)
created_timestamp = Column(
TIMESTAMP,
default=datetime.utcnow(),
nullable=True,
)
reported_timestamp = Column(
TIMESTAMP, default=datetime.utcnow(), nullable=True
)

project: UUID = Column(ForeignKey("project.id"), nullable=False)

Expand Down Expand Up @@ -193,6 +201,12 @@ class Value(Base):
enabled = Column(Boolean)
value_default = Column(String, nullable=False)
value_override = Column(String, nullable=False)
created_timestamp = Column(
TIMESTAMP, default=datetime.utcnow(), nullable=True
)
reported_timestamp = Column(
TIMESTAMP, default=datetime.utcnow(), nullable=True
)

project: UUID = Column(ForeignKey("project.id"), nullable=False)

Expand Down
86 changes: 74 additions & 12 deletions ui/src/Dashboard/Flag.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Divider,
Popconfirm,
message,
Modal,
} from 'antd';
import { useEffect, useState } from 'react';
import {
Expand All @@ -26,12 +27,14 @@ import './Flag.less';
import {
FlagContext,
useFlagState,
useProject
useProject,
} from './context';
import { Conditions } from './Conditions';
import { TYPES, KIND_TO_TYPE, KIND, TYPE_TO_KIND } from './constants';
import { useActions } from './actions';
import { copyToClipboard, replaceValueInArray } from './utils';
import { copyToClipboard, formatTimestamp, replaceValueInArray } from './utils';
import { useLazyQuery } from "@apollo/client";
import { FLAG_LAST_ACTION_TIMESTAMP_QUERY } from "./queries";


const ResetButton = ({ onClick, disabled }) => {
Expand Down Expand Up @@ -105,20 +108,72 @@ const Buttons = ({ onReset, onCancel, onSave, onDelete, onToggle }) => {
);
}

const FlagName = ({ name }) => {
const FlagTitle = ({ name, flagId, createdTimestamp, reportedTimestamp }) => {
const [ isModalVisible, setIsModalVisible ] = useState(false);
const [ flagHistory, setFlagHistory ] = useState({
lastAction: "Loading...",
});

const [ loadLastActionTimestamp ] = useLazyQuery(FLAG_LAST_ACTION_TIMESTAMP_QUERY, {
fetchPolicy: "network-only",
variables: { id: flagId },
onCompleted: (data) => {
setFlagHistory({ lastAction: `${data?.flagLastActionTimestamp || "N/A"}` });
},
onError: () => {
message.error("Error fetching last action");
setFlagHistory({ lastAction: "N/A", });
},
});

const getFlagHistory = () => {
loadLastActionTimestamp();
setIsModalVisible(true);
};

const handleOk = () => {
setIsModalVisible(false);
};

const copyFlag = () => {
copyToClipboard(name, `Flag ${name} copied to clipboard`);
}

const TimestampRow = ({ label, timestamp }) => (
<p>
{label}: <b style={{ color: 'green' }}>{formatTimestamp(timestamp)}</b>
</p>
);

return (
<div
className='flag-name'
onClick={copyFlag}
>
<Space size={8}>
<CopyOutlined />
{name}
</Space>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div
className='flag-name'
onClick={copyFlag}
>
<Space size={8}>
<CopyOutlined />
{name}
</Space>
</div>
<Button onClick={getFlagHistory}>
HISTORY
</Button>
<Modal
title="Flag History"
visible={isModalVisible}
onOk={handleOk}
onCancel={handleOk}
footer={[
<Button key="ok" type="primary" onClick={handleOk}>
OK
</Button>,
]}
>
<TimestampRow label="Created" timestamp={createdTimestamp} />
<TimestampRow label="Last Reported" timestamp={reportedTimestamp} />
<TimestampRow label="Last Action" timestamp={flagHistory.lastAction} />
</Modal>
</div>
)
}
Expand All @@ -131,6 +186,8 @@ const getInitialFlagState = (flag) => ({
enabled: flag.enabled,
// TODO sort conditions, because after save, the order is not guaranteed now
conditions: flag.conditions.map((c) => c.id),
createdTimestamp: flag.created_timestamp,
reportedTimestamp: flag.reported_timestamp,
});

const getInitialConditions = (flag) => {
Expand Down Expand Up @@ -403,7 +460,12 @@ export const Flag = ({ flag }) => {
<Card
size="small"
className={saveFlagFailed ? 'invalid' : ''}
title={<FlagName name={flag.name}/>}
title={<FlagTitle
name={flag.name}
flagId={flag.id}
createdTimestamp={flag.createdTimestamp}
reportedTimestamp={flag.reportedTimestamp}
/>}
style={{ width: 800, borderRadius: '5px' }}
>
<FlagContext.Provider value={ctx}>
Expand Down
Loading
Loading